새소식

Welcome to the tech blog of Junior Backend Developer Seulbin Kang!

Project

[Sansam E-commerce] 2. ERD 설계 : 확장성 있는 설계

  • -

👀 들어가기 전에

프로젝트를 시작할 때마다 공통적으로 해야 하는 작업들이 있다. ERD 작성, 요구사항 정의서, API 명세서 (Swagger), 역할 분배 (R&R) 그리고 Figma를 통한 화면 흐름 파악. 이번 프로젝트에서는 팀장을 맡아 일정과 역할을 분배했고, 프론트 담당자에게는 Figma 기반 화면 설계를 요청했다. 나는 백엔드 관점에서 프로젝트의 "골격"이 되는 작업(ERD / 요구사항 / API 명세 / R&R)을 먼저 단단히 잡는 데에 집중했다.


👀 본론

이번 포스팅은 그중에서도 ERD 설계에 집중해 정리하려 한다.

ERD는  이후 개발 속도/ 확장성 /운영 난이도를 좌우하는 아주 중요한 요소이기 때문에 그만큼 더 집중했고 지속해서 질문을 던졌다.

내가 ERD를 작성하며 계속 던졌던 질문은 아래와 같다.

  • 이 구조는 요구사항이 바뀌어도 버틸 수 있는가?
  • 화면/사용자 흐름을 DB 구조가 자연스럽게 받쳐줄 수 있는가?
  • 조회/정렬/필터링이 늘어났을 때, 쿼리 성능과 확장성은 괜찮은가?
  • 데이터가 커졌을 때, Update/Schema 변경 비용은 감당 가능한가?

그리고 여기서 가장 중요한 전제 하나.

지금은 토이 프로젝트일지 몰라도, 지금부터 하는 과정은 막 사업을 시작한 이커머스 스타트업이다.
즉, 지금부터 하는 하나하나가 다 돈이고 사업의 성패를 결정짓는다. 처음부터 확장성 있는 설계를 전제로 한다.

RDB로 갈 것인가? (그리고 왜 일단 RDB였나)

설계 과정에서 가장 많이 흔들렸던 고민 중 하나는 이거였다.

RDB로 조회를 계속하면 DB가 터질 수 있을텐데...

 

특히 채팅이나 로그성 데이터는 시시각각 쌓이기 때문에 NoSQL 또는 메시지 기반 구조를 떠올리는 것이 자연스럽다.

다만 당시 멘토링해주시던 강사님의 조언이 너무 명확했다. (지금까지도 너무 감사드립니다.)

RDB도 충분히 성능이 좋다. 일단 RDB로 설계하고 구현해보고 병목이 생기는 지점을 확인해서 하나하나 수정해 나가라.

그래서 모든 도메인을 우선 RDB 기반으로 설계하고, 성능 병목이 실제로 발생하는 지점에서 다음 선택(분리/캐시/NoSQL)을 하기로 했다.


최종 ERD

아마 진짜 빼곡해서 잘 안보일 수 있다...

ERD 링크 : https://www.erdcloud.com/d/p3bK5o7T4si4gLpAt

 

ERD가 빽빽해서 한 번에 보기 어렵겠지만, 실제로 설계 과정에서 고민만 하루를 쓴 적도 있다.

특히 어려웠던 도메인은 두가지였다.

  • 상품
  • 채팅

1) 상품 도메인 설계 : 옵션을 상품으로 볼 것인가? 상품 속성으로 볼 것인가?

 

상품 도메인이 어려웠던 이유는 단순히 컬럼이 많아서가 아니다.

"상품을 어떤 단위로 정의할 것인가"가 서비스마다 달랐기 때문이다.

 

쿠팡 vs 무신사 비교에서 시작한 고민

1. 쿠팡

쿠팡의 상품 화면
같은 옷인데 따로 구분되어 있다?

 

  • 같은 티셔츠라도 색상이 다르면 다른 상품처럼 노출되는 경우가 많다.
  • 카테고리가 대/중/소로 나뉘고, 검색 결과에서 상품 단위가 더 세분화되어 보인다.

2. 무신사

무신사 옷 검색

  • 한 상품 안에서 색상/사이즈를 옵션으로 제공하는 구조가 자연스럽다.
  • 즉, 상품 = 대표 정보 + 옵션 조합으로 구성되어 있는 것이다.

우리의 기획 요구사항은 무신사에 가까웠다. (아무래도 같은 옷/악세사리를 판매하는 E-commerce다 보니 더 유사했던 것 같다.)

상품은 하나이고, 색상/사이즈는 옵션으로 묶는다.

 

그래서 핵심 결론은 아래다.

상품 ERD

  • 상품의 고정 정보는 products (상품 자체!)
  • 변하는 선택지는 product_option (사이즈, 컬러)
  • 화면/콘텐츠 단위는 product_details
  • 이들을 연결하는 다대다 관계는 product_connect

로 구성했다.


왜 product_option을 분리했나?

 

처음에는 상품 테이블 하나에 색상1, 색상2, 사이즈1 같은 형태로 모두 넣고 싶었다.

하지만 그 방식은 문제가 너무 명확했다.

  • 옵션이 늘어날 때마다 컬럼이 늘어남 (스키마 변경 비용 발생)
  • 색상/사이즈/기타 옵션이 섞이며 정합성이 깨지기 쉬움
  • 특정 옵션만 필터링/검색하기 어려움 (ex. 파란색 반팔)

그래서 결국 옵션이 별도 테이블로 분리되었다. (아래에서 자세히 다루겠다.)

  • type : 사이즈/색상
  • name : L, XL, Black,White...
  • use_yn : 옵션이 없는 상품도 존재하므로 (악세사리와 같이) 사용 여부 판단

이 구조의 장점은 명확하다.

  • 옵션 타입이 늘어도 컬럼 추가가 아니라 row 추가로 확장
  • 옵션 기반 필터링/정렬/검색 확장 가능
  • 운영/관리 관점에서 옵션을 데이터! 로 볼 수 있다.

product_details와 map_name의 의미

 

상품은 옵션만큼이나 상세 콘텐츠가 중요하다

실제로 화면에서 상품은 단순 텍스트로 보이는 것이 아니라 이미지/디테일 구성으로 사용자는 구매를 결정한다.

 

그래서 상품 상세를 product_details로 분리했다.

  • 어떤 상품이 어떤 파일을 가지는지 (file_management_id)
  • 그리고 그 상세 파일이 무엇을 의미하는지를 map_name으로 남겼다. (ex. Thumbnail, Main, Detail_1 etc.)

이 row가 어떤걸 의미하는 row인지를 DB레벨에서 이해할 수 있도록 만들었다.


왜 product_connect가 필요했나?

 

우선, product_option 설계를 먼저 이야기하겠다.

  • 옵션은 고유한 식별자를 가진다.
  • 옵션의 값에는 사이즈/색상과 같은 도메인 값이 들어간다.
  • 옵션 타입은 ENUM으로 관리한다.
  • 하나의 상품에는 여러 옵션이 연결될 수 있다.

즉, 옵션 자체는 상품에 종속되지만 옵션이 의미하는 값은 독립적인 도메인 엔티티로 다루어야한다는 생각이 들었다.

그래서 결과적으로 product_option이 다음과 같은 구조를 가지게 된 것이다.

  • option_id (PK)
  • product_id (FK)
  • type (SIZE/COLOR)
  • name (L/XL/BLACK/WHITE..etc)

이렇게 되면 옵션 ID 하나가 상품 + 옵션값을 정확히 식별하게 된다.

실제로 하나하나 적어보며 어떤게 확장 가능성 있는지 실험해봤다.

 

여기서 자연스럽게 product_connect가 등장한 이유가 나오게 된다.

옵션값이 바뀌면, 어떤 상품 상세 (이미지, 파일, 콘텐츠)를 보여줘야 할까?

 

예를 들어, Black 옵션을 선택하면 Black의 실착 이미지를 보여줘야하고, White 옵션을 선택하면 White의 실착 이미지를 보여줘야 한다.

이때 필요한 것은 옵션이 선택되었을 때, 어떤 상세 콘텐츠를 매핑하여 보여줄 것인가다.

그래서 product_connect 테이블을 만들게 되었다.


2) 채팅 도메인 설계 : 채팅의 좌/우 배치는 DB에서 어떻게 무엇을 보장해야 하는가

채팅은 처음부터 RDB로 가고 싶지 않던 도메인 중 하나다.

데이터가 빠르게 쌓이고 스크롤 업(이전 메시지 조회)과 같이 조회 패턴도 빡세기 때문이다.

 

그럼에도 일단 RDB로 구현해보고 병목을 확인한다는 원칙 아래 설계했다.


가장 큰 고민 : 사용자와 상대방을 어떻게 구분할 것인가

 

채팅 UI에서 기본 요구사항은 단순하다

  • 내가 보낸 메시지 : 오른쪽
  • 상대가 보낸 메시지 : 왼쪽

이걸 구현하려면 DB/백엔드가 최소한 무엇을 보장해야 하는가?

각 메시지가 누구에 의해 작성되었는지가 메시지 단위로 식별 가능해야 한다.

 

처음에는 sender_id를 chat_room에 넣는 방식을 떠올렸지만, 얼마 안가 문제를 발견했다.

  • 방 단위에 sender가 있으면 메시지마다 누가 썼는지를 구분할 수 없다.
  • 그룹 채팅이 되어버리면 구조가 깨지게 된다.

그래서 결국 메시지에 sender_id를 두게 되었다.

채팅 ERD

 

이 구조로 아래와 같은 부분을 이룰 수 있었다.

  • 좌/우 배치 : sender_id == 로그인 유저 id면 오른쪽
  • 상대방 정보 : chat_member에서 내 id제외한 유저 찾기
  • 읽음 처리 : last_read_at 기준으로 읽지 않은 메시지 계산 가능 (몇개 안읽었는지)
  • 그룹 채팅 확장 : 참여자 수가 늘어난다 하더라도 기존 구조 유지가 가능하다.

구축 이후 느낀 변화

 

ERD를 설계하며 가장 크게 느낀건 두 가지다.

1. 화면/사용자 흐름과 DB 구조는 밀접하게 연결된다

2. DB 설계 하나로 "요구사항 변경"의 비용이 천차만별이다.

 

특히 운영 관점에서 무서운 것은 "나중에 바꾸는 것"이다.

데이터가 쌓인 이후의 Update/Schema 변경은 생각보다 훨씬 비싸다.

(특히 데이터가 커질수록 마이그레이션 비용은 기하 급수적으로 올라간다. 지난 번에 보니까 10만건에 1시간~2시간이 걸렸던 것 같다..)

 

처음 설계 단계에서 정규화/확장성을 고민하는 것이 개발 속도와 운영 안정성에 얼마나 큰 영향을 미치는지를 체감할 수 있었다.


👀 결론

ERD는 DB를 눈에 보이게 그리는 작업이 아니라 요구사항이 변해도 시스템이 무너지지 않게 만드는 설계였다.

다음 포스팅은 이 ERD를 바탕으로 내가 맡은 작업과 그에 따른 과정들을 이어갈 예정이다. 😄

 

Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.