- 패션 플랫폼인 ‘바로’의 ERD설계를 진행하면서 고민했던 부분에 대해 기록해 보려고 한다
- 이를 진행하면서 여러 방법들과 그에 대한 Trade - off를 고려하면서 설계하려고 했다
- 왜냐하면 ERD는 초기에 방향을 잘 잡지 않으면 이후 변경 과정에서 많은 리소스가 사용될 수 있기 때문이다
- 그래서 현재 요구사항만 만족하는 설계가 아니라 이후에 확장성까지 고려하여 설계하려고 하였다
- 막 대단한 내용은 없지만 고민해보면서 서비스에 대해 더 깊이 이해하게 되어 좋았다
- 아래 몇가지 기준을 생각하면서 진행했다
- 그리고 아래 링크에서 상세 ERD를 확인할 수 있다
https://dbdiagram.io/d/BARO_ERD-6870cfbaf413ba3508661df3
dbdiagram.io - Database Relationship Diagrams Design Tool
dbdiagram.io
설계 기준
1. 미래를 대비하는 유연한 확장성
- 서비스는 살아있는 생물과 같아서, 항상 변화하고 성장한다
- 따라서 초기 설계부터 미래의 비즈니스 변화를 예측하고 유연하게 대처할 수 있는 구조를 만드는 것이 무엇보다 중요하다
2. 성능과 데이터 무결성 확보
- 사용자가 자주 마주하는 기능은 빠르고 안정적이어야 한다
- 특히 시각적 정보와의 상호작용이 중요한 ‘바로’ 서비스에서는 이 부분이 핵심 경쟁력이라고 생각한다
3. 비용과 복잡도를 고려한 선택
- 무작정 서버를 늘린다거나 캐시를 사용하는 등 자원을 추가한다면 성능이 좋아질 수 밖에 없다
- 또한 상황 고려 없이 신기술 만을 사용하는 것도 좋은 방식이 아니다
- 팀이 처한 상황에 맞게 요구사항, 러닝 커브, 등등 여러가지를 고려하여 기술을 선택하고 자원적인 부분은 기존 환경에서 최대한 처리하다가 도무지 방법이 없을 때 사용하자
1. 룩 스와이프 기능(좋아요, 싫어요)을 위한 설계
😢 문제 상황
-- 초기 구상 (Before)
Table look_swipes {
member_id long [pk, ref: > members.id]
look_id long [pk, ref: > looks.id]
interaction_type swipe_type [not null] // 'LIKE' or 'DISLIKE'
created_at timestamp
}
초기 구상 look_swipes
테이블은 interaction_type
컬럼에 ENUM
('LIKE', 'DISLIKE') 타입을 사용하여 사용자의 모든 스와이프 기록을 단일 테이블에 저장하고 있다. 이 설계는 나쁘지 않아보이긴 하지만 ‘바로’ 서비스의 핵심 기능이라는 점을 고려할 때 다음과 같은 잠재적 문제점을 가지고 있다
- 잦은 데이터 변경(UPDATE) 발생 : 사용자가 '좋아요'를 눌렀다가 '싫어요'로 바꾸거나 그 반대의 경우,
swipe_type
을 변경하는UPDATE
쿼리가 발생한다. 또한 데이터베이스에서INSERT
에 비해UPDATE
는 트랜잭션 로그, 인덱스 재정렬 등 더 많은 부하를 유발하며, 테이블이 커질수록 성능 저하의 원인이 된다
이에 대해 조금 더 자세하게 설명하면,
- UPDATE 의 경우 InnoDB는 트랜잭션 롤백과 MVCC를 위한 Undo 로그와 Redo 로그를 기록해야 하며, 이는 추가적인 메모리 사용과 디스크 I/O를 유발한다. 즉, 사용자가 많이 사용할 것이라 예상되는 핵심 기능인 ‘룩 스와이프’에서는 이와 같은 로깅 오버헤드가 부담이 될 수 있다
- 또한 성능 개선을 위해 swipe_type을 인덱스에 포함시켰을 때, B+Tree 내에서 기존 엔트리를 삭제하고 새로운 엔트리를 삽입하는 인덱스 재정렬을 유발하여 추가적인 비용을 발생시킨다
이에 대한 내용은 다른 글에서 더 자세하게 다뤄보겠다
- 핵심 조회 기능의 성능 저하 우려 : ‘각 유저가 아직 스와이프하지 않은 룩 목록’을 보여주는 것이 핵심 기능이다. 이를 위해서는 매번
looks
테이블을 조회할 때마다look_swipes
테이블에서 현재 사용자가 스와이프한 모든look_id
를WHERE look_id NOT IN (...)
조건으로 제외해야 한다.look_swipes
테이블은 서비스가 성장함에 따라 가장 빠르게 데이터가 누적되는 테이블이 될 것이므로, 이 제외 목록을 만드는 과정 자체가 병목이 될 수 있다
정리하자면, 핵심 기능의 성능은 서비스 전체 경험에 직접적인 영향을 주므로, 높은 트래픽과 데이터 변경에 더 효율적으로 대응할 수 있는 구조 개선이 필요하다고 판단했습니다
🤔 여러 개선 방법 고민
방법 1 - 현재 구조 유지
기능 구현의 속도를 높이기 위해 현재 설계를 그대로 사용한다
장점
- 구조가 단순하고 이해하기 쉽다
단점
- 위에서 말한
UPDATE
부하 및 대용량 데이터 조회 시 성능 저하 문제가 그대로 남는다. 또한 시리즈 B라는 목표에 맞는 서비스의 미래 확장성을 고려할 때 가장 아쉬운 선택이다
방법 2 - 좋아요 / 싫어요 테이블 분리
look_swipes
테이블을 look_likes
와 look_dislikes
두 개의 테이블로 분리한다
장점
- UPDATE가 사라짐 : 사용자가 '좋아요'를 누르면
likes
테이블에INSERT
, '싫어요'를 누르면dislikes
테이블에INSERT
된다. 상태가 바뀌면 해당 테이블에서DELETE
후 다른 테이블에INSERT
하면 되므로, 복잡한UPDATE
로직이 사라지고 데이터 변경 과정이 단순해진다 - 조회 성능 향상 기대 : "스와이프하지 않은 룩 조회" 시,
likes
테이블과dislikes
테이블 양쪽에서look_id
를 가져와 제외해야 한다. 하지만 각 테이블의 크기가 단일 테이블일 때보다 작고 인덱스가 명확(user_id, look_id)하므로,NOT IN
또는LEFT JOIN
시 스캔 범위가 줄어들어 성능상 이점을 가질 수 있다.
단점
- 테이블 개수가 하나 늘어난다. 하지만 기능적으로 명확히 분리되므로 관리 부담은 크지 않다
- 또한 좋아요, 싫어요 타입외(그냥 그래요 등등)에 더 늘어날 상황은 적어보인다
방법 3 - NoSQL(Redis)을 활용한 상태 관리
스와이프처럼 휘발성이 강하고 응답 속도가 중요한 데이터는 RDB가 아닌 인메모리 DB인 Redis에서 관리하며, Redis의 Set 자료구조 활용한다
장점
- 빠른 속도 : RDB에 접근할 필요 없이 메모리에서 빠른 속도로 '좋아요/싫어요' 여부를 판단하고 기록할 수 있다
- RDB 부하 감소 : 가장 빈번한 트래픽을 Redis가 흡수하여 메인 DB의 부담을 크게 줄여준다
단점
- 아키텍처 복잡도 증가 : Redis라는 별도의 기술 스택 도입 및 관리가 필요하다
- 데이터 정합성 문제 : RDB와 Redis 간의 데이터 동기화 등 고려할 점이 늘어난다. 또한 서비스 초기 단계에서는 오버 엔지니어링 일 수 있다
✅ 최종 결정 및 근거
방법 2 - 좋아요 / 싫어요 테이블 분리
-- 최종 결정 (After)
// 룩 좋아요
Table look_likes {
user_id long [pk, ref: > users.id]
look_id long [pk, ref: > looks.id]
created_at timestamp
}
// 룩 싫어요
Table look_dislikes {
user_id long [pk, ref: > users.id]
look_id long [pk, ref: > looks.id]
created_at timestamp
}
방법 2는 예측되는 문제점을 해결하면서 아키텍처의 복잡도를 급격히 높이지 않는 가장 균형 잡힌 해결책이라 생각했다. 또한 Redis와 같은 비용적인 부분은 도입하기 전 어떠한 방법으로도 해결하지 못할 때 사용해야 한다고 생각한다
- 성능과 비용 :
UPDATE
를INSERT
/DELETE
로 바꿔 데이터 변경 부하를 줄이고, 테이블 분리를 통해 핵심 조회 기능의 성능 개선을 기대할 수 있다. 이는 Redis를 도입하는 것보다 훨씬 적은 비용으로 얻을 수 있는 합리적인 성능 향상이다 - 유지보수의 용이성 : 테이블의 역할이
좋아요
와싫어요
로 명확하게 분리되어 코드의 가독성과 향후 유지보수성이 향상된다. 새로운 개발자가 합류하더라도 구조를 쉽게 파악할 수 있다 - 점진적 확장 가능성 : 이 구조로 운영하다가 트래픽이 예상을 뛰어넘을 정도로 폭증한다면, 그때 가서 방법 3처럼 특정 테이블(
look_likes
)의 데이터만 Redis로 캐싱하거나 이전하는 전략을 점진적으로 도입할 수 있다. 위 설계가 미래의 확장성을 막지 않는다
2. '인기 순위' 조회의 성능 최적화를 위한 설계
😢 문제 상황
"인기 상품 조회" 와 "룩 좋아요" 기능은 사용자에게 매력적인 콘텐츠를 노출하는 우리 서비스의 핵심적인 기능이다. 현재 ERD에서 '상품의 인기'를 측정하는 가장 직접적인 데이터는 product_likes
테이블에 기록된 '좋아요' 수다. 마찬가지로 '룩의 인기'는 look_likes
테이블에 기록된 '좋아요' 데이터의 수가 될 것이다
'인기 상품 목록'을 조회하기 위해서는 다음과 같은 쿼리를 실행해야 한다
SELECT p.id, p.name, COUNT(pl.member_id) AS likes_count
FROM products p
LEFT JOIN product_likes pl ON p.id = pl.product_id
GROUP BY p.id
ORDER BY likes_count DESC;
이 방식은 서비스 초기에는 문제가 없지만, product_likes
테이블이 수백만, 수천만 건으로 증가할 경우 다음과 같은 심각한 성능 문제가 발생할 수 있다
- 실시간 집계 부하 :
COUNT
와GROUP BY
연산은 데이터가 많을수록 매우 비용이 큰 작업이다. 인기 상품 페이지를 조회할 때마다 이 연산을 수행하는 것은 DB에 큰 부담을 주며, 사용자에게는 느린 응답 시간으로 이어질 수 있다 - 인덱스 활용의 한계 :
ORDER BY
를 동적으로 계산된likes_count
에 대해 수행하므로, 일반적인 컬럼 인덱스를 효율적으로 활용하기 어렵다. 이는 Full Table Scan에 가까운 성능을 보일 수 있다 - 핵심 기능의 확장성 부재 : 현재 기능 명세서에는 없지만 향후 ‘특정 카테고리별 상품 필터링’이나 ‘사용자가 선호할만한 스타일 우선 노출’과 같은 기능이 추가될 수 있다. 현재 구조에서는 이러한 복합적인 정렬 로직을 추가할 때마다 쿼리가 더욱 복잡해지고 성능은 기하급수적으로 저하될 것이다
따라서 서비스의 핵심인 '탐색' 기능이 느려지는 것은 사용자 경험에 치명적이므로, 조회 시점의 실시간 집계를 피하고 더 빠른 방법으로 인기 순위를 제공할 수 있는 구조적 개선이 필요하다고 판단했다
🤔 여러 개선 방법 고민
방법 1 - 현재 구조 유지 (실시간 집계)
조회 요청이 올 때마다 COUNT
와 GROUP BY
를 통해 실시간으로 인기 순위를 계산
장점
- 데이터 정합성이 100% 보장되며, 별도의 스키마 변경이 필요 없다
단점
- 위에서 제기된 심각한 성능 및 확장성 문제를 해결할 수 없다
방법 2 - 비정규화를 통한 집계 컬럼 추가
products
테이블과 looks
테이블에 집계용 컬럼을 미리 추가해 두는 방식으로 products
테이블과 looks
테이블에 likes_count
컬럼 추가한다. 사용자가 '좋아요'를 누르거나 취소할 때, product_likes
테이블에 INSERT
/DELETE
하는 트랜잭션 내에서 관련 products
테이블의 likes_count
를 1 증가/감소 시킨다
장점
- 압도적인 조회 성능 : 인기순 조회가
ORDER BY likes_count DESC
한 줄로 끝난다. 집계 연산이 전혀 필요 없으므로 매우 빠르고, 해당 컬럼에 인덱스를 생성하면 성능을 극대화할 수 있다. - 확장 용이성 :
likes_count
외에도view_count
,share_count
등 다양한 지표를 컬럼으로 추가하여 복합적인 인기 알고리즘을 구현하기 용이하다
단점
- 데이터 불일치 위험 : 만약
likes_count
를 업데이트하는 로직에 버그가 있거나 트랜잭션 처리가 실패하면, 실제product_likes
의 개수와likes_count
값이 달라질 수 있다 (단, 정기적인 배치 작업으로 보정 가능) - 쓰기 비용 소폭 증가 :
INSERT
/DELETE
시UPDATE
쿼리가 한 번 더 실행된다. 하지만 이는 비효율적인SELECT
에 비하면 훨씬 저렴한 비용이다
방법 3 - 스케줄링된 배치(Batch) 작업 활용
방법 2
와 같이 집계 컬럼을 사용하되, 실시간으로 업데이트하지 않고 주기적인 배치 작업을 통해 업데이트한다. 예를 들어, 매일 새벽 3시에 스케줄러를 실행하여 모든 상품의 likes
수를 계산하고 products.likes_count
에 일괄 업데이트
장점
- 사용자 요청 처리 시에는 쓰기 부하가 전혀 없다. 데이터 불일치가 발생해도 다음 배치 시간에 자동 복구된다
단점
- 데이터가 실시간이 아니다. 즉, 방금 '좋아요'를 많이 받은 상품이 인기 목록에 즉시 반영되지 않는다. 따라서 트렌드에 민감한 패션 서비스에는 적합하지 않을 수 있다
✅ 최종 결정 및 근거
방법 2 - 비정규화를 통한 집계 컬럼 추가
-- 최종 결정 (After)
TABLE products (
id BIGINT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
likes_count INT NOT NULL DEFAULT 0,
-- ...
);
TABLE looks (
id BIGINT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
swipes_like_count INT NOT NULL DEFAULT 0,
-- ...
);
우리 서비스에서 '인기 순위'는 실시간 성격이 강하며 사용자들의 반응이 즉각적으로 반영될 때 가장 큰 가치를 가진다고 생각한다. 따라서 방법 3
의 데이터 지연은 이러한 가치를 훼손할 수 있다. 그리고 방법 1
은 서비스 성장에 발목을 잡을 명백한 기술 부채이다.
따라서 방법 2
가 성능, 실시간성, 확장성을 모두 고려했을 때 가장 합리적인 선택이다. UPDATE
비용 증가와 데이터 정합성 유지 측면에서 고려해야 할 것이 있지만, 빠른 읽기 성능이 주는 막대한 이점에 비하면 충분히 감수할 만한 트레이드오프다
3. 상품의 가격은 끊임없이 변경된다
😢 문제 상황
-- 초기 구상
TABLE order_items (
id BIGINT PRIMARY KEY,
order_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
quantity INT NOT NULL
-- 가격 정보가 없음!
);
전자상거래 서비스에서 상품의 가격은 시간이 지남에 따라 변동될 수 있다. 예를 들어, 할인 프로모션, 원자재 가격 변동, 시장 상황 변화 등으로 인해 상품의 판매 가격이 인상되거나 인하될 수 있다
orders
와 order_items
테이블을 설계하면서, 고객이 특정 시점에 구매한 상품의 가격을 정확하게 기록하고 보존하는 것이 중요하다는 점을 인지했다. 만약 order_items
테이블에서 price
정보를 직접 products
테이블의 price
컬럼에 의존하도록 설계한다면, 다음과 같은 문제들이 발생할 수 있었다
- 과거 주문 내역의 불일치 : 고객이 A 상품을 10,000원에 구매했는데, 나중에 A 상품의 가격이 12,000원으로 인상되면, 과거 10,000원에 구매했던 주문 내역을 조회했을 때 현재 가격인 12,000원이 표시되어 혼란을 줄 수 있다
- 재무 및 회계 데이터의 부정확성 : 주문이 발생한 시점의 실제 거래 가격과 현재 가격이 달라지므로, 정확한 매출 통계, 세금 계산, 환불 처리 등 재무 및 회계 관련 데이터를 산출하기 어렵다
- 데이터 무결성 훼손 : 상품 가격 변경이 주문 내역에 소급 적용되어, 이미 완료된 거래의 불변성 원칙이 훼손될 수 있다
따라서 이러한 문제들을 방지하고, 주문의 불변성과 데이터의 정확성을 확보하기 위한 설계가 필요했다
🤔 여러 개선 방법 고민
방법 1 - order_items
테이블에서 products
테이블의 price
를 참조
order_items
테이블에 price
컬럼을 따로 두지 않고, products.price
를 JOIN
하여 사용하는 방식이다
장점
- 데이터 중복 최소화 :
price
정보가products
테이블에만 존재하므로, 데이터 중복이 없다 - 저장 공간 절약 :
order_items
테이블에 추가 컬럼이 없어 저장 공간을 절약할 수 있다
단점
- 과거 가격 조회 불가능 : 상품 가격이 변경되면 과거 주문 시점의 가격을 알 수 없다. 이는 위에서 언급한 핵심 문제들을 발생시킨다
- 주문 데이터의 불변성 훼손 :
products.price
가 변경되면, 모든 과거 주문 내역의 가격이 소급 변경되는 치명적인 문제가 발생한다
방법 2 - order_items
테이블에 price_at_purchase
컬럼 추가
order_items
테이블에 price_at_purchase
라는 새로운 컬럼을 추가하고, 주문이 접수되는 시점에 products
테이블에서 가져온 가격을 이 컬럼에 저장하는 방식이다
장점
- 주문 가격의 불변성 보장 : 주문이 완료된 시점의 가격을 정확하게 기록하므로, 이후
products
테이블의 가격이 변동되어도 주문 내역은 영향을 받지 않는다 - 정확한 과거 데이터 유지 : 특정 주문에 대한 정확한 결제 가격 정보를 언제든 조회할 수 있어 재무, 회계, 고객 지원 등 모든 비즈니스 활동의 기초 데이터로 활용 가능하다
- 데이터 무결성 강화 : 거래의 핵심 요소인 가격 정보가 주문 시점에 확정되어 저장되므로 데이터의 신뢰성이 높아진다
단점
- 데이터 중복 발생:
products
테이블의price
와order_items
테이블의price_at_purchase
가 동일한 가격 정보를 갖게 되어 데이터 중복이 발생한다 - 저장 공간 증가: 추가 컬럼만큼 저장 공간이 소폭 증가한다
✅ 최종 결정 및 근거
방법 2 - order_items
테이블에 price_at_purchase
컬럼 추가
-- 최종 결정 (After)
TABLE order_items (
id BIGINT PRIMARY KEY,
order_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
quantity INT NOT NULL,
price_at_purchase INT NOT NULL, -- 주문 시점의 가격을 기록
FOREIGN KEY (order_id) REFERENCES orders(id),
FOREIGN KEY (product_id) REFERENCES products(id)
);
주문 데이터의 불변성과 정확성이 비즈니스 운영에 있어 가장 중요하다고 판단하여, order_items
테이블에 price_at_purchase
컬럼을 추가하는 방식을 최종적으로 선택했다. 비록 데이터 중복과 저장 공간 증가라는 단점이 있지만, 이는 다음과 같은 장점들에 비해 미미하다고 판단했다
- 재무적 정확성 : 매출, 비용, 이익 등 모든 재무 데이터를 정확하게 산출할 수 있다. 이는 기업의 의사 결정에 필수적이다
- 법적/회계적 요구사항 충족 : 실제 거래된 가격을 정확히 보관함으로써, 세무 감사나 법적 분쟁 발생 시 근거 자료로 활용될 수 있다
- 일관된 고객 경험 : 고객이 자신의 주문 내역을 조회했을 때, 구매 당시의 가격과 동일한 가격을 확인할 수 있다
- 유연한 시스템 : 상품 가격 변동 로직과 주문 처리 로직이 분리되어, 각자의 비즈니스 규칙에 따라 독립적으로 관리될 수 있다
4. 이미지 처리 설계
😢 문제 상황
-- 초기 구상 (Before)
TABLE products (
id BIGINT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
image_url VARCHAR(255), -- 대표 이미지 하나만 저장 가능
-- ...
);
- 사용자가 상품을 구매하거나 룩(코디)을 참고할 때, 단 한 장의 이미지만으로는 충분한 정보를 얻기 어렵다
- 예를 들어, 상품은 앞, 뒤, 옆모습과 상세 디테일 컷이 필요하고, 룩은 여러 각도에서 촬영된 사진이나 다양한 포즈의 사진을 통해 스타일을 더 잘 어필할 수 있다
- 초기 구상은 각 상품과 룩에 대해 단 하나의 대표 이미지만 저장할 수 있는 구조였다
- 이로 인해 여러 장의 이미지를 보여줘야 하는 비즈니스 요구사항을 충족시키지 못하는 한계가 있었다
🤔 여러 개선 방법 고민
방법 1 - 기존 테이블에 이미지 URL 컬럼 추가
products
테이블에 image_url_1
, image_url_2
, image_url_3
와 같이 필요한 만큼 컬럼을 직접 추가하는 방식이다
장점
- JOIN 불필요 : 관련 이미지를 가져오기 위해 JOIN 쿼리를 사용할 필요가 없어 쿼리가 단순하다
- 직관적인 구조 : 테이블 구조가 단순하여 이해하기 쉽다
단점
- 낮은 확장성 : 이미지 개수가 4개로 늘어나면
ALTER TABLE
을 통해 DB 스키마를 변경하고 배포해야 하는 매우 경직된 구조다 - 공간 낭비 : 최대 5개의 이미지를 가정하고
image_url_5
까지 만들어도, 대부분의 상품이 2~3개의 이미지만 가진다면 나머지 컬럼들은NULL
값으로 남아 저장 공간을 낭비한다 - 데이터 관리의 어려움 : 이미지 순서를 바꾸거나, 특정 이미지만 삭제하는 등의 작업이 복잡해진다. 또한, 이미지 개수를 세는 쿼리도 비효율적이다
방법 2 - 별도의 이미지 테이블로 분리 (정규화)
product_images
라는 별도의 테이블을 생성하여, 각 이미지를 독립적인 데이터로 관리하고 상품/룩과는 1:N 관계로 연결하는 방식이다
장점
- 높은 유연성 및 확장성 : 상품이나 룩당 이미지 개수에 제한이 없다. 스키마 변경 없이
INSERT
쿼리만으로 이미지를 무한정 추가할 수 있다 - 데이터 무결성 보장 :
product_id
에 Foreign Key 제약 조건을 설정하여, 존재하지 않는 상품에 이미지가 연결되는 것을 방지한다. 또한 상품 삭제 시 관련 이미지를 함께 삭제(ON DELETE CASCADE
)하는 등 정합성 관리가 용이하다 - 체계적인 데이터 관리 :
display_order
(노출 순서),is_thumbnail
(대표 이미지 여부) 등 이미지별로 다양한 메타데이터를 추가하여 관리할 수 있다
단점
- 쿼리 복잡도 증가 : 이미지 정보를 가져오려면 항상
JOIN
을 사용해야 한다 - 테이블 수 증가 : 스키마 관리를 위해 2개의 테이블이 추가된다
✅ 최종 결정 및 근거
방법 2 - 별도의 이미지 테이블로 분리하여 관리
-- 최종 결정 (After)
TABLE product_images (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
product_id BIGINT NOT NULL,
image_url VARCHAR(255) NOT NULL,
display_order INT NOT NULL DEFAULT 0, -- 이미지 노출 순서
is_thumbnail BOOLEAN NOT NULL DEFAULT FALSE, -- 대표 이미지 여부
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE
);
초기 개발의 단순함보다는 미래의 확장성, 데이터 무결성, 그리고 관리의 용이성에 더 높은 가치를 두어 이미지 테이블을 별도로 분리하는 정규화 방식을 선택했다
- 확장 가능한 구조 : "이미지 순서 변경 기능", "이미지별 설명 추가" 등 향후 추가될 수 있는 다양한 요구사항에 DB 스키마 변경 없이 신속하게 대응할 수 있다. 이는 운영 중인 서비스의 안정성과 유연성을 높일 수 있다
- 데이터 무결성 확보 : Foreign Key는 데이터베이스가 스스로 데이터의 정합성을 지키도록 한다. 또한 애플리케이션 로직의 실수로 인해 발생할 수 있는 Orphaned Data를 원천적으로 차단한다
- 성능 최적화:
JOIN
으로 인한 성능 저하는product_id
컬럼에 적절한 인덱스를 생성함으로써 상쇄할 수 있다
마치며
- 데이터베이스 설계에는 정답이 없다
- 하지만 확장성, 데이터 무결성, 성능 등 중요한 가치를 기준으로, 각 비즈니스 요구사항에 맞는 최적의 방식을 트레이드오프를 고려해 찾아가는 과정은 필수적이다
- 초기에 깊이 고민한다면, 이후의 과정에서 적은 리소스로 더 나은 서비스를 만드는 데 집중할 수 있을 것이다
'프로젝트' 카테고리의 다른 글
[바로] 반복되는 인증,인가 처리 없애버리기(Ft. AOP & ArgumentResolver) (0) | 2025.08.08 |
---|---|
[바로] 분산 시스템에서 ID가 유일하려면?(Ft. Snowflake VS TSID 성능테스트) (3) | 2025.07.28 |
[바로] JWT는 정말 괜찮은 방법일까? (Ft. 세션저장소 선택 이유) (2) | 2025.07.22 |
[바로] 스와이프로 찾는 내 스타일, ‘바로’를 기획하며..(Ft. 기술적 목표) (3) | 2025.07.22 |