diff --git a/.claude/skills/requirement-analysis/SKILL.md b/.claude/skills/requirement-analysis/SKILL.md new file mode 100644 index 00000000..3485a8af --- /dev/null +++ b/.claude/skills/requirement-analysis/SKILL.md @@ -0,0 +1,77 @@ +--- +name: requirements-analysis +description: + 제공된 요구사항을 분석하고, 개발자와의 질문/대답을 통해 애매한 요구사항을 명확히 하여 정리합니다. + 모든 정리가 끝나면, 시퀀스 다이어그램, 클래스 다이어그램, ERD 등을 Mermaid 문법으로 작성한다. + 요구사항이 제공되었을 때, 코드를 작성하기 전 이를 명확히 하는 데에 사용합니다. +--- +요구사항을 분석할 때 반드시 다음 흐름을 따른다. +### 1️⃣ 요구사항을 그대로 믿지 말고, 문제 상황으로 다시 설명한다. +- 요구사항 문장을 정리하는 데서 끝내지 않는다. +- "무엇을 만들까?"가 아니라 "지금 어떤 문제가 있고, 그걸 왜 해결하려는가?" 로 재해석한다. +- 다음 관점을 분리해서 정리한다: + - 사용자 관점 + - 비즈니스 관점 + - 시스템 관점 +> 예시 +> "주문 실패 시 결제를 취소한다" → "결제 성공/실패와 주문 상태가 어긋나지 않도록 일관성을 유지하려는 문제" + +### 2️⃣ 애매한 요구사항을 숨기지 말고 드러낸다 +- 추측하거나 알아서 결정하지 않는다. +- 요구사항에서 결정되지 않은 부분을 명시적으로 나열한다. + **다음 유형의 질문을 반드시 포함한다:** +- 정책 질문: 기준 시점, 성공/실패 조건, 예외 처리 규칙 +- 경계 질문: 어디까지가 한 책임인가, 어디서 분리되는가 +- 확장 질문: 나중에 바뀔 가능성이 있는가 + +### 3️⃣ 요구사항 명확화를 위한 질문을 개발자 답변이 쉬운 형태로 제시한다 +- 질문은 우선순위를 가진다 (중요한 것부터). +- 선택지가 있는 경우, 옵션 + 영향도를 함께 제시한다. +> 형식 예시: +- 선택지 A: 하나의 트랜잭션으로 처리 → 구현 단순, 확장성 낮음 +- 선택지 B: 단계별 분리 → 구조 복잡, 확장/보상 처리 유리 + +### 4️⃣ 합의된 내용을 바탕으로 개념 모델부터 잡는다 +- 바로 코드나 기술 얘기로 들어가지 않는다. +- 먼저 다음을 정의한다: + - 액터 (사용자, 외부 시스템) + - 핵심 도메인 + - 보조/외부 시스템 +- 이 단계는 “구현”이 아니라 설계 사고 정렬이 목적이다. + +### 5️⃣ 다이어그램은 항상 이유 → 다이어그램 → 해석 순서로 제시한다 +**다이어그램을 그리기 전에 반드시 설명한다** +- 왜 이 다이어그램이 필요한지 +- 이 다이어그램으로 무엇을 검증하려는지 + +**다이어그램은 Mermaid 문법으로 작성한다** +사용 기준: +- **시퀀스 다이어그램** + - 책임 분리 + - 호출 순서 + - 트랜잭션 경계 확인 +- **클래스 다이어그램** + - 도메인 책임 + - 의존 방향 + - 응집도 확인 +- **ERD** + - 영속성 구조 + - 관계의 주인 + - 정규화 여부 + +### 6️⃣ 다이어그램을 던지고 끝내지 말고 읽는 법을 짚어준다 +- "이 구조에서 특히 봐야 할 포인트"를 2~3줄로 설명한다. +- 설계 의도가 드러나도록 해석을 붙인다. + +### 7️⃣ 설계의 잠재 리스크를 반드시 언급한다 +- 현재 설계가 가질 수 있는 위험을 숨기지 않는다. + - 트랜잭션 비대화 + - 도메인 간 결합도 증가 + - 정책 변경 시 영향 범위 확대 +- 해결책은 정답처럼 말하지 않고 선택지로 제시한다. + +### 톤 & 스타일 가이드 +- 강의처럼 설명하지 말고 설계 리뷰 톤을 유지한다 +- 정답이라고 제시하기보다, 다른 선택지가 있다면 이를 제공하도록 한다. +- 코드보다 의도, 책임, 경계를 더 중요하게 다룬다 +- 구현 전에 생각해야 할 것을 끌어내는 데 집중한다 \ No newline at end of file diff --git a/.docs/design/01-requirements.md b/.docs/design/01-requirements.md new file mode 100644 index 00000000..431df28f --- /dev/null +++ b/.docs/design/01-requirements.md @@ -0,0 +1,354 @@ +# 이커머스 요구사항 정의서 + +## 1. 목적 및 범위 + +### 1.1 목적 + +온라인 커머스 플랫폼을 구축한다. +고객은 브랜드별 상품을 탐색하고, 관심 상품에 좋아요를 표시하며, 주문을 통해 상품을 구매할 수 있다. +관리자는 브랜드와 상품을 등록/관리하고, 주문 현황을 조회할 수 있다. + +### 1.2 시스템 사용자 + +| 사용자 유형 | 설명 | +|-------------------|------| +| **비회원 (Visitor)** | 회원가입 전 상태의 사용자. 공개 정보 조회만 가능하다 | +| **회원 (User)** | 회원가입을 완료한 사용자. 좋아요, 주문 등 인증이 필요한 기능을 사용한다 | +| **관리자 (Admin)** | 내부 운영자. 브랜드/상품 관리 및 주문 조회를 수행한다 | + +### 1.3 범위 + +본 문서는 아래 도메인의 기능적 요구사항을 정의한다. + +- **브랜드**: 조회(고객), 등록/수정/삭제(관리자) +- **상품**: 조회(고객), 등록/수정/삭제(관리자) +- **좋아요**: 상품에 대한 좋아요 등록/취소/목록 조회 +- **장바구니**: 상품 담기/수량 변경/제거/조회 +- **주문**: 주문 생성, 주문 내역 조회 + +> 회원 도메인(가입, 인증, 내 정보 관리)은 이미 구현 완료되었으므로 본 문서의 범위에서 제외한다. + +### 1.4 유비쿼터스 언어 + +본 프로젝트에서 사용하는 도메인 용어를 아래와 같이 정의한다. +모든 문서(요구사항, ERD, API 명세), 코드(클래스명, 변수명), 구두 커뮤니케이션에서 동일한 용어를 사용한다. + +#### 사용자 + +| 한국어 | 영문 | 정의 | +|--------|-------|------| +| 회원 | User | 회원가입을 완료하여 인증된 상태로 서비스를 이용하는 사용자 | +| 관리자 | Admin | 내부 운영 권한을 가진 사용자. 브랜드/상품 관리, 주문 조회를 수행한다 | + +#### 브랜드 + +| 한국어 | 영문 | 정의 | +|--------|------|------| +| 브랜드 | Brand | 상품을 제공하는 주체. 하나의 브랜드는 여러 상품을 가질 수 있다 | +| 브랜드명 | Brand Name | 브랜드를 식별하는 이름 | + +#### 상품 + +| 한국어 | 영문 | 정의 | +|--------|------|------| +| 상품 | Product | 고객이 구매할 수 있는 판매 단위 | +| 상품명 | Product Name | 상품을 식별하는 이름 | +| 가격 | Price | 상품 1개의 판매 금액 | +| 재고 | Stock | 현재 판매 가능한 상품의 수량 | + +#### 좋아요 + +| 한국어 | 영문 | 정의 | +|--------|------|------| +| 좋아요 | Like | 회원이 특정 상품에 대해 관심을 표시하는 행위. 회원당 상품당 하나만 존재한다 | + +#### 장바구니 + +| 한국어 | 영문 | 정의 | +|--------|------|------| +| 장바구니 | Cart | 회원이 구매를 고려하는 상품을 임시로 모아두는 공간. 회원당 하나 존재한다 | +| 장바구니 항목 | Cart Item | 장바구니에 담긴 개별 상품과 그 수량 | + +#### 주문 + +| 한국어 | 영문 | 정의 | +|--------|------|------| +| 주문 | Order | 회원이 하나 이상의 상품을 구매하기 위해 생성하는 거래 단위 | +| 주문 항목 | Order Item | 주문에 포함된 개별 상품과 그 수량. 주문 시점의 상품 정보를 스냅샷으로 보존한다 | +| 수량 | Quantity | 주문 항목에서 해당 상품을 구매하는 개수 | +| 스냅샷 | Snapshot | 주문 시점의 상품 정보(상품명, 가격, 브랜드 등) 사본. 원본이 변경되어도 주문 기록은 유지된다 | + +> **용어 사용 원칙**: "아이템"이 아닌 "상품", "취소"가 아닌 "좋아요 취소"처럼, 위 표에 정의된 용어만을 사용한다. 새로운 용어가 필요한 경우 본 표에 먼저 추가한 후 사용한다. + +--- + +## 2. 유저 시나리오 + +### 2.1 브랜드 (Brand) + +#### US-B01: 브랜드 정보 조회 (고객) + +> 고객(비회원/회원)은 특정 브랜드의 정보를 조회할 수 있다. + +#### US-B02: 브랜드 목록 조회 (관리자) + +> 관리자는 등록된 브랜드 목록을 조회할 수 있다. + +- 관리에 필요한 상세 정보를 확인할 수 있다. + +#### US-B03: 브랜드 상세 조회 (관리자) + +> 관리자는 특정 브랜드의 상세 정보를 조회할 수 있다. + +#### US-B04: 브랜드 등록 (관리자) + +> 관리자는 새로운 브랜드를 등록할 수 있다. + +#### US-B05: 브랜드 정보 수정 (관리자) + +> 관리자는 기존 브랜드의 정보를 수정할 수 있다. + +#### US-B06: 브랜드 삭제 (관리자) + +> 관리자는 브랜드를 삭제할 수 있다. + +- 브랜드 삭제 시, 해당 브랜드에 속한 모든 상품도 함께 삭제된다. + +--- + +### 2.2 상품 (Product) + +#### US-P01: 상품 목록 조회 (고객) + +> 고객(비회원/회원)은 상품 목록을 탐색할 수 있다. + +- 특정 브랜드의 상품만 필터링하여 볼 수 있다. +- 정렬 기준을 선택할 수 있다 (최신순은 필수, 가격순/좋아요순은 선택). +- 페이지 단위로 조회한다. + +#### US-P02: 상품 상세 조회 (고객) + +> 고객(비회원/회원)은 특정 상품의 상세 정보를 조회할 수 있다. + +#### US-P03: 상품 목록 조회 (관리자) + +> 관리자는 등록된 상품 목록을 조회할 수 있다. + +- 브랜드별로 필터링하여 조회할 수 있다. +- 관리에 필요한 상세 정보를 확인할 수 있다. + +#### US-P04: 상품 상세 조회 (관리자) + +> 관리자는 특정 상품의 상세 정보를 조회할 수 있다. + +#### US-P05: 상품 등록 (관리자) + +> 관리자는 새로운 상품을 등록할 수 있다. + +- 상품은 반드시 이미 등록된 브랜드에 속해야 한다. + +#### US-P06: 상품 정보 수정 (관리자) + +> 관리자는 기존 상품의 정보를 수정할 수 있다. + +- 단, 상품의 소속 브랜드는 변경할 수 없다. + +#### US-P07: 상품 삭제 (관리자) + +> 관리자는 상품을 삭제할 수 있다. + +--- + +### 2.3 좋아요 (Like) + +#### US-L01: 상품 좋아요 등록 + +> 회원은 관심 있는 상품에 좋아요를 등록할 수 있다. + +#### US-L02: 상품 좋아요 취소 + +> 회원은 이전에 등록한 좋아요가 있다면, 한 번 더 클릭하여 좋아요를 취소할 수 있다. + +#### US-L03: 좋아요한 상품 목록 조회 + +> 회원은 자신이 좋아요를 등록한 상품 목록을 조회할 수 있다. + +--- + +### 2.4 장바구니 (Cart) + +#### US-C01: 장바구니에 상품 담기 + +> 회원은 구매를 고려하는 상품을 장바구니에 담을 수 있다. + +- 담을 상품과 수량을 지정한다. +- 이미 장바구니에 있는 상품을 다시 담으면, 수량이 누적된다. + +#### US-C02: 장바구니 조회 + +> 회원은 자신의 장바구니에 담긴 상품 목록을 조회할 수 있다. + +#### US-C03: 장바구니 상품 수량 변경 + +> 회원은 장바구니에 담긴 상품의 수량을 변경할 수 있다. + +#### US-C04: 장바구니 상품 제거 + +> 회원은 장바구니에서 특정 상품을 제거할 수 있다. + +--- + +### 2.5 주문 (Order) + +#### US-O01: 주문 생성 + +> 회원은 하나 이상의 상품을 선택하고 수량을 지정하여 주문할 수 있다. + +- 주문 시점의 상품 정보가 주문에 스냅샷으로 보존된다. +- 주문이 완료되면, 해당 수량만큼 상품 재고가 차감된다. + +#### US-O02: 주문 목록 조회 (회원) + +> 회원은 자신의 주문 내역을 기간을 지정하여 조회할 수 있다. + +- 조회 시작일과 종료일을 지정한다. + +#### US-O03: 주문 상세 조회 (회원) + +> 회원은 자신의 특정 주문의 상세 내역을 조회할 수 있다. + +- 주문 당시의 브랜드,상품 정보를 확인할 수 있다. + +#### US-O04: 주문 목록 조회 (관리자) + +> 관리자는 전체 주문 목록을 페이지 단위로 조회할 수 있다. + +#### US-O05: 주문 상세 조회 (관리자) + +> 관리자는 특정 주문의 상세 내역을 조회할 수 있다. + +--- + +## 3. 비즈니스 규칙 + +### 3.1 인증 및 권한 + +| 규칙 ID | 규칙 | 비고 | +|---------|------|------| +| BR-A01 | 회원 전용 기능은 인증된 사용자만 접근할 수 있다 | 좋아요, 주문 등 | +| BR-A02 | 관리자 전용 기능은 관리자 인증을 통과한 사용자만 접근할 수 있다 | 브랜드/상품 관리, 주문 관리 | +| BR-A03 | 공개 기능(상품 조회, 브랜드 조회)은 인증 없이 접근할 수 있다 | | + +### 3.2 브랜드 + +| 규칙 ID | 규칙 | 비고 | +|---------|------|------| +| BR-B01 | 브랜드를 삭제하면, 해당 브랜드에 속한 모든 상품도 함께 삭제된다 | 연쇄 삭제 | +| BR-B02 | 고객에게 제공되는 브랜드 정보와 관리자에게 제공되는 브랜드 정보는 다를 수 있다 | 노출 범위 구분 | + +### 3.3 상품 + +| 규칙 ID | 규칙 | 비고 | +|---------|------|-------------------------------------------------------| +| BR-P01 | 상품은 반드시 이미 등록된 브랜드에 속해야 한다 | 등록 시 검증 | +| BR-P02 | 상품의 소속 브랜드는 등록 이후 변경할 수 없다 | 불변 속성 | +| BR-P03 | 상품 목록은 최신순 정렬을 기본으로 제공한다 | 필수 정렬 | +| BR-P04 | 가격순, 좋아요순 정렬은 선택적으로 제공할 수 있다 | 선택 구현 | +| BR-P05 | 고객에게 제공되는 상품 정보와 관리자에게 제공되는 상품 정보는 다를 수 있다 | 노출 범위 구분 (예: 회원 및 비회원은 재고 여부만 확인, 관리자는 재고 수량까지 확인 가능) | + +### 3.4 좋아요 + +| 규칙 ID | 규칙 | 비고 | +|---------|------|------| +| BR-L01 | 한 회원은 동일 상품에 좋아요를 한 번만 등록할 수 있다 | 중복 방지 | +| BR-L02 | 좋아요를 등록하지 않은 상품에 대해 취소할 수 없다 | | +| BR-L03 | 회원은 자신의 좋아요 목록만 조회할 수 있다 | | + +### 3.5 장바구니 + +| 규칙 ID | 규칙 | 비고 | +|---------|------|------| +| BR-C01 | 회원은 하나의 장바구니를 가진다 | | +| BR-C02 | 이미 장바구니에 있는 상품을 다시 담으면, 기존 수량에 누적된다 | | +| BR-C03 | 장바구니 항목의 수량은 1 이상이어야 한다 | | +| BR-C04 | 회원은 자신의 장바구니만 조회/수정할 수 있다 | | + +### 3.6 주문 + +| 규칙 ID | 규칙 | 비고 | +|---------|------|------| +| BR-O01 | 주문은 하나 이상의 상품 항목을 포함해야 한다 | | +| BR-O02 | 각 상품 항목의 수량은 1 이상이어야 한다 | | +| BR-O03 | 주문 시점에 상품 재고가 충분해야 한다 | 재고 부족 시 주문 실패 | +| BR-O04 | 주문이 성공하면, 주문한 수량만큼 상품 재고가 차감된다 | | +| BR-O05 | 주문에는 당시 상품 정보가 스냅샷으로 보존된다 | 이후 상품 정보가 변경되어도 주문 기록은 유지 | +| BR-O06 | 회원은 자신의 주문만 조회할 수 있다 | | +| BR-O07 | 관리자는 전체 주문을 조회할 수 있다 | | +| BR-O08 | 주문 목록 조회 시, 기간(시작일~종료일)을 지정하여 필터링한다 | 회원 조회 시 | + +--- + +## 4. 예외 및 정책 + +### 4.1 공통 + +| 예외 상황 | 정책 | +|-----------|------| +| 인증되지 않은 사용자가 인증 필요 기능에 접근 | 접근을 거부하고, 인증 필요 사유를 안내한다 | +| 관리자 인증 없이 관리자 기능에 접근 | 접근을 거부한다 | + +### 4.2 브랜드 + +| 예외 상황 | 정책 | +|-----------|------| +| 존재하지 않는 브랜드 조회/수정/삭제 시도 | 요청을 거부하고, 존재하지 않음을 안내한다 | + +### 4.3 상품 + +| 예외 상황 | 정책 | +|-----------|------| +| 존재하지 않는 브랜드에 상품 등록 시도 | 등록을 거부하고, 브랜드가 존재하지 않음을 안내한다 | +| 상품의 소속 브랜드 변경 시도 | 수정을 거부한다 | +| 존재하지 않는 상품 조회/수정/삭제 시도 | 요청을 거부하고, 존재하지 않음을 안내한다 | + +### 4.4 좋아요 + +| 예외 상황 | 정책 | +|-----------|------| +| 이미 좋아요한 상품에 다시 좋아요 시도 | 등록을 거부하고, 이미 좋아요 상태임을 안내한다 | +| 좋아요하지 않은 상품에 좋아요 취소 시도 | 취소를 거부하고, 좋아요 상태가 아님을 안내한다 | +| 존재하지 않는 상품에 좋아요 시도 | 등록을 거부한다 | + +### 4.5 장바구니 + +| 예외 상황 | 정책 | +|-----------|------| +| 존재하지 않는 상품을 장바구니에 담으려는 경우 | 담기를 거부한다 | +| 장바구니에 없는 상품의 수량을 변경하려는 경우 | 변경을 거부한다 | +| 수량을 0 이하로 변경하려는 경우 | 변경을 거부한다 | + +### 4.6 주문 + +| 예외 상황 | 정책 | +|-----------|------| +| 주문 상품의 재고가 부족한 경우 | 주문을 거부하고, 재고 부족 사유를 안내한다 | +| 존재하지 않는 상품을 주문에 포함한 경우 | 주문을 거부한다 | +| 주문 항목이 비어있는 경우 | 주문을 거부한다 | +| 수량이 0 이하인 항목이 포함된 경우 | 주문을 거부한다 | +| 다른 회원의 주문을 조회하려는 경우 | 접근을 거부한다 | + +--- + +## 5. 이번 범위에서 제외하는 것들 + +| 항목 | 사유 | +|------|------| +| **회원 도메인** | 이미 구현 완료 (가입, 인증, 내 정보 관리, 비밀번호 변경) | +| **결제** | 추후 별도 단계에서 개발 예정 | +| **주문 취소/환불** | 결제 기능과 연계하여 추후 개발 | +| **주문 상태 관리** (배송 중, 배송 완료 등) | 현재 범위에서는 주문 생성과 조회만 다룬다 | +| **상품 이미지 관리** | 파일 업로드/스토리지는 현재 범위에 포함하지 않는다 | +| **상품 카테고리** | 현재는 브랜드 단위로만 상품을 분류한다 | +| **검색 (키워드 기반 상품 검색)** | 현재는 브랜드 필터링과 정렬만 제공한다 | +| **회원 탈퇴** | 현재 범위에 포함하지 않는다 | +| **관리자 등록/관리** | 관리자 계정 관리는 현재 범위에 포함하지 않는다 | diff --git a/.docs/design/02-sequence-diagrams.md b/.docs/design/02-sequence-diagrams.md new file mode 100644 index 00000000..c1791774 --- /dev/null +++ b/.docs/design/02-sequence-diagrams.md @@ -0,0 +1,1211 @@ +# 시퀀스 다이어그램 + +## 공통 사항 + +### 인증/인가 + +- 회원 전용 기능(좋아요, 장바구니, 주문)과 관리자 전용 기능(브랜드/상품 관리)은 AuthInterceptor에서 인증을 선처리한다. +- 인증 실패 시 Controller에 도달하기 전에 요청이 거부된다. +- 아래 다이어그램은 **인증이 통과된 이후의 흐름**만 표현한다. + +### 계층 간 데이터 흐름 + +- 각 계층 경계에서 DTO가 변환된다 (Request DTO → Command, Entity → Info → Response DTO). +- DTO 변환은 각 컴포넌트의 내부 책임이므로 시퀀스 다이어그램에 표현하지 않는다. + +--- + +## 2.1 브랜드 (Brand) + +### US-B01: 브랜드 정보 조회 (고객) + +#### 검증 목적 + +고객의 브랜드 조회 요청이 각 계층을 통과하는 순서와, "브랜드가 없다"를 예외로 판단하는 책임이 어느 계층에 있는지 확인한다. + +#### 시퀀스 다이어그램 + +```mermaid +sequenceDiagram + autonumber + actor 고객 + participant Controller as BrandV1Controller + participant Facade as BrandFacade + participant Service as BrandService + participant Repository as BrandRepository + + 고객->>Controller: 브랜드 정보 조회 요청 + Controller->>Facade: 브랜드 조회 위임 + Facade->>Service: 브랜드 조회 + Service->>Repository: 브랜드 존재 여부 확인 + + alt 브랜드가 존재하는 경우 + Repository-->>Service: 브랜드 정보 + Service-->>Facade: 브랜드 정보 + Facade-->>Controller: 브랜드 정보 + Controller-->>고객: 브랜드 정보 응답 + else 브랜드가 존재하지 않는 경우 + Repository-->>Service: 없음 + Service->>Service: 비즈니스 예외 발생 + Service-->>Facade: 예외 전파 + Facade-->>Controller: 예외 전파 + Controller-->>고객: 존재하지 않음 안내 + end +``` + +#### 봐야 할 포인트 + +1. **예외 발생 위치**: Repository는 "없음"만 반환하고, 비즈니스 예외로 변환하는 책임은 Service에 있다. +2. **Facade의 역할**: 단일 도메인 조회이므로 Facade는 Service 호출을 위임만 한다. + +--- + +### US-B02: 브랜드 목록 조회 (관리자) + +#### 검증 목적 + +목록 조회는 결과가 비어있어도 정상 응답이므로 예외 분기가 없다. 단건 조회와의 차이를 확인한다. + +#### 시퀀스 다이어그램 + +```mermaid +sequenceDiagram + autonumber + actor 관리자 + participant Controller as BrandAdminV1Controller + participant Facade as BrandAdminFacade + participant Service as BrandService + participant Repository as BrandRepository + + 관리자->>Controller: 브랜드 목록 조회 요청 + Controller->>Facade: 브랜드 목록 조회 위임 + Facade->>Service: 브랜드 목록 조회 + Service->>Repository: 브랜드 목록 조회 + Repository-->>Service: 브랜드 목록 + Service-->>Facade: 브랜드 목록 + Facade-->>Controller: 브랜드 목록 + Controller-->>관리자: 브랜드 목록 응답 +``` + +#### 봐야 할 포인트 + +1. **예외 분기 없음**: 결과가 비어있어도 빈 목록으로 정상 응답한다. +2. **BR-B02**: 관리자용 브랜드 정보는 고객용과 다를 수 있다. 차이는 Controller의 DTO 변환에서 결정된다. + +--- + +### US-B03: 브랜드 상세 조회 (관리자) + +#### 검증 목적 + +관리자 단건 조회가 고객 조회(US-B01)와 동일한 계층 흐름을 따르되, 응답 범위만 다른지 확인한다. + +#### 시퀀스 다이어그램 + +```mermaid +sequenceDiagram + autonumber + actor 관리자 + participant Controller as BrandAdminV1Controller + participant Facade as BrandAdminFacade + participant Service as BrandService + participant Repository as BrandRepository + + 관리자->>Controller: 브랜드 상세 조회 요청 + Controller->>Facade: 브랜드 조회 위임 + Facade->>Service: 브랜드 조회 + Service->>Repository: 브랜드 존재 여부 확인 + + alt 브랜드가 존재하는 경우 + Repository-->>Service: 브랜드 정보 + Service-->>Facade: 브랜드 정보 + Facade-->>Controller: 브랜드 정보 + Controller-->>관리자: 브랜드 상세 정보 응답 + else 브랜드가 존재하지 않는 경우 + Repository-->>Service: 없음 + Service->>Service: 비즈니스 예외 발생 + Service-->>Facade: 예외 전파 + Facade-->>Controller: 예외 전파 + Controller-->>관리자: 존재하지 않음 안내 + end +``` + +#### 봐야 할 포인트 + +1. **US-B01과 동일한 흐름**: 차이는 액터(관리자)와 응답 DTO의 범위뿐이다. BR-B02에 따라 관리자용 정보가 더 상세할 수 있다. + +--- + +### US-B04: 브랜드 등록 (관리자) + +#### 검증 목적 + +브랜드 등록 시 브랜드명 중복 여부를 확인한 후 저장하는 흐름을 검증한다. 다른 도메인에 대한 의존은 없지만, 도메인 내부 유일성 제약이 존재한다. + +#### 시퀀스 다이어그램 + +```mermaid +sequenceDiagram + autonumber + actor 관리자 + participant Controller as BrandAdminV1Controller + participant Facade as BrandAdminFacade + participant Service as BrandService + participant Repository as BrandRepository + + 관리자->>Controller: 브랜드 등록 요청 + Controller->>Facade: 브랜드 등록 위임 + Facade->>Service: 브랜드 등록 + Service->>Repository: 브랜드명 중복 여부 확인 + + alt 브랜드명 중복 시 + Repository-->>Service: 브랜드명 중복 + Service-->>Facade: 중복 예외 + Facade-->>Controller: 중복 예외 전파 + Controller-->>관리자: 409 Conflict + end + + Repository-->>Service: 중복되지 않음 + Service->>Repository: 브랜드 정보 저장 + Repository-->>Service: 저장된 브랜드 정보 + Service-->>Facade: 브랜드 정보 + Facade-->>Controller: 브랜드 정보 + Controller-->>관리자: 브랜드 등록 완료 응답 +``` + +#### 봐야 할 포인트 + +1. **브랜드명 중복 검증**: Service가 저장 전에 동일 브랜드명의 존재 여부를 확인한다. 중복 시 CONFLICT(409)로 응답한다. +2. **early-return 패턴**: 중복 검증 실패 시 즉시 예외를 반환하고, 정상 흐름은 `alt` 블록 이후에 계속된다. + +--- + +### US-B05: 브랜드 정보 수정 (관리자) + +#### 검증 목적 + +브랜드 수정 시 "존재 여부 확인 → 브랜드명 중복 확인 → 수정" 순서를 확인한다. US-B04와 마찬가지로 브랜드명 유일성 제약이 적용된다. + +#### 시퀀스 다이어그램 + +```mermaid +sequenceDiagram + autonumber + actor 관리자 + participant Controller as BrandAdminV1Controller + participant Facade as BrandAdminFacade + participant Service as BrandService + participant Repository as BrandRepository + + 관리자->>Controller: 브랜드 수정 요청 + Controller->>Facade: 브랜드 수정 위임 + Facade->>Service: 브랜드 수정 + Service->>Repository: 브랜드 존재 여부 확인 + + alt 브랜드가 존재하지 않는 경우 + Repository-->>Service: 없음 + Service->>Service: 비즈니스 예외 발생 + Service-->>Facade: 예외 전파 + Facade-->>Controller: 예외 전파 + Controller-->>관리자: 존재하지 않음 안내 + end + + Repository-->>Service: 브랜드 정보 + Service->>Repository: 변경할 브랜드명 중복 여부 확인 + + alt 브랜드명 중복 시 + Repository-->>Service: 브랜드명 중복 + Service-->>Facade: 중복 예외 + Facade-->>Controller: 중복 예외 전파 + Controller-->>관리자: 409 Conflict + end + + Repository-->>Service: 중복되지 않음 + Service->>Repository: 브랜드 수정 + Repository-->>Service: 수정된 브랜드 정보 + Service-->>Facade: 브랜드 정보 + Facade-->>Controller: 브랜드 정보 + Controller-->>관리자: 브랜드 수정 완료 응답 +``` + +#### 봐야 할 포인트 + +1. **이중 검증**: 존재 확인 → 브랜드명 중복 확인 → 수정의 3단계. US-B04(등록)의 단일 검증보다 한 단계가 추가된다. +2. **자기 자신 제외**: 수정 시 브랜드명 중복 확인은 자기 자신을 제외한 다른 브랜드와 비교해야 한다. 이 구분은 Repository 쿼리에서 처리된다. + +--- + +### US-B06: 브랜드 삭제 (관리자) + +#### 검증 목적 + +BR-B01(연쇄 삭제)의 책임이 어느 계층에 있는지 확인한다. 브랜드 삭제 → 상품 삭제 → 좋아요 삭제의 3단계 연쇄가 발생하며, Facade가 BrandService, ProductService, LikeService를 조율한다. + +#### 시퀀스 다이어그램 + +```mermaid +sequenceDiagram + autonumber + actor 관리자 + participant Controller as BrandAdminV1Controller + participant Facade as BrandAdminFacade + participant BrandService + participant BrandRepository + participant LikeService + participant LikeRepository + participant CartService + participant CartRepository + participant ProductService + participant ProductRepository + + 관리자->>Controller: 브랜드 삭제 요청 + Controller->>Facade: 브랜드 삭제 위임 + Facade->>BrandService: 브랜드 존재 확인 + BrandService->>BrandRepository: 브랜드 존재 여부 확인 + + alt 브랜드가 존재하지 않는 경우 + BrandRepository-->>BrandService: 없음 + BrandService->>BrandService: 비즈니스 예외 발생 + BrandService-->>Facade: 예외 전파 + Facade-->>Controller: 예외 전파 + Controller-->>관리자: 존재하지 않음 안내 + end + + BrandRepository-->>BrandService: 브랜드 정보 + BrandService-->>Facade: 브랜드 정보 + Facade->>LikeService: 해당 브랜드 상품 좋아요 전체 삭제 + LikeService->>LikeRepository: 좋아요 전체 삭제 (hard delete) + LikeRepository-->>LikeService: 삭제 완료 + LikeService-->>Facade: 삭제 완료 + Facade->>CartService: 해당 브랜드 상품 장바구니 항목 전체 삭제 + CartService->>CartRepository: 장바구니 항목 전체 삭제 (hard delete) + CartRepository-->>CartService: 삭제 완료 + CartService-->>Facade: 삭제 완료 + Facade->>ProductService: 해당 브랜드 상품 전체 삭제 + ProductService->>ProductRepository: 상품 전체 삭제 (soft delete) + ProductRepository-->>ProductService: 삭제 완료 + ProductService-->>Facade: 삭제 완료 + Facade->>BrandService: 브랜드 삭제 + BrandService->>BrandRepository: 브랜드 삭제 (soft delete) + BrandRepository-->>BrandService: 삭제 완료 + BrandService-->>Facade: 삭제 완료 + Facade-->>Controller: 삭제 완료 + Controller-->>관리자: 브랜드 삭제 완료 응답 +``` + +#### 봐야 할 포인트 + +1. **Facade의 4-서비스 조율**: BrandService, LikeService, CartService, ProductService는 서로를 모른다. 도메인 간 삭제 순서를 Facade가 결정한다. +2. **삭제 순서와 정책**: 좋아요(hard delete) → 장바구니 항목(hard delete) → 상품(soft delete) → 브랜드(soft delete). 종속 데이터를 먼저 정리해야 상위 엔티티 삭제 후 고아 데이터가 남지 않는다. + +#### 잠재 리스크 + +- **트랜잭션 범위**: 좋아요 삭제, 장바구니 항목 삭제, 상품 삭제, 브랜드 삭제가 하나의 트랜잭션으로 묶여야 한다. 4개 서비스를 포함하므로 트랜잭션이 넓다. +- **Soft Delete 연쇄 정책**: 브랜드 복원 시 상품도 함께 복원해야 하는지, 복원된 상품의 좋아요와 장바구니 항목은 이미 hard delete되어 복원 불가능한 점을 어떻게 다룰지 정책 결정이 필요하다. + +--- + +## 2.2 상품 (Product) + +### US-P01: 상품 목록 조회 (고객) + +#### 검증 목적 + +상품 목록 조회에는 브랜드 필터링, 정렬(BR-P03, BR-P04), 페이징이 포함된다. 필터/정렬 조건의 처리 책임이 어느 계층에 있는지 확인한다. + +#### 시퀀스 다이어그램 + +```mermaid +sequenceDiagram + autonumber + actor 고객 + participant Controller as ProductV1Controller + participant Facade as ProductFacade + participant Service as ProductService + participant Repository as ProductRepository + + 고객->>Controller: 상품 목록 조회 요청 (필터, 정렬, 페이징) + Controller->>Facade: 상품 목록 조회 위임 + Facade->>Service: 상품 목록 조회 + Service->>Repository: 조건부 목록 조회 + Repository-->>Service: 상품 목록 (페이징) + Service-->>Facade: 상품 목록 + Facade-->>Controller: 상품 목록 + Controller-->>고객: 상품 목록 응답 +``` + +#### 봐야 할 포인트 + +1. **필터/정렬 책임**: 브랜드 필터링과 정렬 조건은 Repository 계층의 쿼리로 처리된다. Service는 조건을 전달만 한다. +2. **BR-P05**: 고객에게는 재고 여부(있음/없음)만 노출하고, 구체적 재고 수량은 숨긴다. DTO 변환에서 결정된다. + +--- + +### US-P02: 상품 상세 조회 (고객) + +#### 검증 목적 + +단건 조회의 정상/예외 분기를 확인한다. 브랜드 조회(US-B01)와 동일한 패턴을 따른다. + +#### 시퀀스 다이어그램 + +```mermaid +sequenceDiagram + autonumber + actor 고객 + participant Controller as ProductV1Controller + participant Facade as ProductFacade + participant Service as ProductService + participant Repository as ProductRepository + + 고객->>Controller: 상품 상세 조회 요청 + Controller->>Facade: 상품 조회 위임 + Facade->>Service: 상품 조회 + Service->>Repository: 상품 존재 여부 확인 + + alt 상품이 존재하는 경우 + Repository-->>Service: 상품 정보 + Service-->>Facade: 상품 정보 + Facade-->>Controller: 상품 정보 + Controller-->>고객: 상품 상세 정보 응답 + else 상품이 존재하지 않는 경우 + Repository-->>Service: 없음 + Service->>Service: 비즈니스 예외 발생 + Service-->>Facade: 예외 전파 + Facade-->>Controller: 예외 전파 + Controller-->>고객: 존재하지 않음 안내 + end +``` + +#### 봐야 할 포인트 + +1. **US-B01과 동일한 패턴**: 단건 조회의 존재/부재 분기는 모든 도메인에서 동일하게 적용된다. + +--- + +### US-P03: 상품 목록 조회 (관리자) + +#### 검증 목적 + +관리자 목록 조회가 고객 목록 조회(US-P01)와 동일한 흐름을 따르되, 응답 정보의 범위만 다른지 확인한다. + +#### 시퀀스 다이어그램 + +```mermaid +sequenceDiagram + autonumber + actor 관리자 + participant Controller as ProductAdminV1Controller + participant Facade as ProductAdminFacade + participant Service as ProductService + participant Repository as ProductRepository + + 관리자->>Controller: 상품 목록 조회 요청 (브랜드 필터) + Controller->>Facade: 상품 목록 조회 위임 + Facade->>Service: 상품 목록 조회 + Service->>Repository: 조건부 목록 조회 + Repository-->>Service: 상품 목록 + Service-->>Facade: 상품 목록 + Facade-->>Controller: 상품 목록 + Controller-->>관리자: 상품 목록 응답 +``` + +#### 봐야 할 포인트 + +1. **BR-P05**: 관리자에게는 재고 수량까지 노출된다. 고객용/관리자용 차이는 Controller의 DTO 변환에서 결정된다. + +--- + +### US-P04: 상품 상세 조회 (관리자) + +#### 검증 목적 + +관리자 단건 조회가 US-P02와 동일한 흐름을 따르는지 확인한다. + +#### 시퀀스 다이어그램 + +```mermaid +sequenceDiagram + autonumber + actor 관리자 + participant Controller as ProductAdminV1Controller + participant Facade as ProductAdminFacade + participant Service as ProductService + participant Repository as ProductRepository + + 관리자->>Controller: 상품 상세 조회 요청 + Controller->>Facade: 상품 조회 위임 + Facade->>Service: 상품 조회 + Service->>Repository: 상품 존재 여부 확인 + + alt 상품이 존재하는 경우 + Repository-->>Service: 상품 정보 + Service-->>Facade: 상품 정보 + Facade-->>Controller: 상품 정보 + Controller-->>관리자: 상품 상세 정보 응답 + else 상품이 존재하지 않는 경우 + Repository-->>Service: 없음 + Service->>Service: 비즈니스 예외 발생 + Service-->>Facade: 예외 전파 + Facade-->>Controller: 예외 전파 + Controller-->>관리자: 존재하지 않음 안내 + end +``` + +#### 봐야 할 포인트 + +1. **US-P02와 동일한 흐름**: 액터와 응답 DTO 범위만 다르다. + +--- + +### US-P05: 상품 등록 (관리자) + +#### 검증 목적 + +상품 등록 시 두 가지를 검증한다: BR-P01(브랜드 존재 여부)은 Facade가 도메인 간 조율로 처리하고, 같은 브랜드 내 상품명 중복은 ProductService가 내부에서 처리한다. + +#### 시퀀스 다이어그램 + +```mermaid +sequenceDiagram + autonumber + actor 관리자 + participant Controller as ProductAdminV1Controller + participant Facade as ProductAdminFacade + participant BrandService + participant BrandRepository + participant ProductService + participant ProductRepository + + 관리자->>Controller: 상품 등록 요청 + Controller->>Facade: 상품 등록 위임 + Facade->>BrandService: 브랜드 존재 확인 + BrandService->>BrandRepository: 브랜드 존재 여부 확인 + + alt 브랜드가 존재하지 않는 경우 + BrandRepository-->>BrandService: 없음 + BrandService->>BrandService: 비즈니스 예외 발생 + BrandService-->>Facade: 예외 전파 + Facade-->>Controller: 예외 전파 + Controller-->>관리자: 브랜드가 존재하지 않음 안내 + end + + BrandRepository-->>BrandService: 브랜드 정보 + BrandService-->>Facade: 브랜드 정보 + Facade->>ProductService: 상품 등록 + ProductService->>ProductRepository: 같은 브랜드 내 상품명 중복 여부 확인 + + alt 상품명 중복 시 + ProductRepository-->>ProductService: 상품명 중복 + ProductService-->>Facade: 중복 예외 + Facade-->>Controller: 중복 예외 전파 + Controller-->>관리자: 409 Conflict + end + + ProductRepository-->>ProductService: 중복되지 않음 + ProductService->>ProductRepository: 상품 정보 저장 + ProductRepository-->>ProductService: 저장된 상품 정보 + ProductService-->>Facade: 상품 정보 + Facade-->>Controller: 상품 정보 + Controller-->>관리자: 상품 등록 완료 응답 +``` + +#### 봐야 할 포인트 + +1. **검증 책임의 분리**: 브랜드 존재 확인(도메인 간)은 Facade가, 상품명 중복 확인(도메인 내)은 ProductService가 처리한다. US-B04의 브랜드명 중복 검증과 동일한 패턴. +2. **중복 범위**: 상품명 중복은 전체가 아닌 **같은 브랜드 내**에서만 확인한다. 다른 브랜드에 동일한 상품명이 존재하는 것은 허용된다. + +--- + +### US-P06: 상품 정보 수정 (관리자) + +#### 검증 목적 + +상품 수정 시 "존재 여부 확인 → 같은 브랜드 내 상품명 중복 확인 → 수정" 순서를 확인한다. BR-P02(소속 브랜드 변경 불가)는 도메인 모델 수준에서 보장된다. + +#### 시퀀스 다이어그램 + +```mermaid +sequenceDiagram + autonumber + actor 관리자 + participant Controller as ProductAdminV1Controller + participant Facade as ProductAdminFacade + participant Service as ProductService + participant Repository as ProductRepository + + 관리자->>Controller: 상품 수정 요청 + Controller->>Facade: 상품 수정 위임 + Facade->>Service: 상품 수정 + Service->>Repository: 상품 존재 여부 확인 + + alt 상품이 존재하지 않는 경우 + Repository-->>Service: 없음 + Service->>Service: 비즈니스 예외 발생 + Service-->>Facade: 예외 전파 + Facade-->>Controller: 예외 전파 + Controller-->>관리자: 존재하지 않음 안내 + end + + Repository-->>Service: 상품 정보 + Service->>Repository: 같은 브랜드 내 상품명 중복 여부 확인 + + alt 상품명 중복 시 + Repository-->>Service: 상품명 중복 + Service-->>Facade: 중복 예외 + Facade-->>Controller: 중복 예외 전파 + Controller-->>관리자: 409 Conflict + end + + Repository-->>Service: 중복되지 않음 + Service->>Repository: 상품 수정 + Repository-->>Service: 수정된 상품 정보 + Service-->>Facade: 상품 정보 + Facade-->>Controller: 상품 정보 + Controller-->>관리자: 상품 수정 완료 응답 +``` + +#### 봐야 할 포인트 + +1. **이중 검증**: US-B05(브랜드 수정)와 동일한 패턴. 존재 확인 → 상품명 중복 확인 → 수정. +2. **자기 자신 제외**: 상품명 중복 확인 시 자기 자신은 제외해야 한다. Repository 쿼리에서 처리. +3. **BR-P02 브랜드 불변성**: 소속 브랜드 변경은 도메인 모델이 브랜드 변경 setter를 제공하지 않는 방식으로 보장한다. 시퀀스 다이어그램의 관심사(컴포넌트 간 흐름)가 아닌 모델 설계의 관심사이다. + +--- + +### US-P07: 상품 삭제 (관리자) + +#### 검증 목적 + +상품 삭제 시 해당 상품의 좋아요도 함께 정리해야 한다. 상품은 soft delete, 좋아요는 hard delete로 삭제 정책이 다르므로 Facade가 ProductService와 LikeService를 조율하는 cross-domain 처리이다. + +#### 시퀀스 다이어그램 + +```mermaid +sequenceDiagram + autonumber + actor 관리자 + participant Controller as ProductAdminV1Controller + participant Facade as ProductAdminFacade + participant ProductService + participant ProductRepository + participant LikeService + participant LikeRepository + participant CartService + participant CartRepository + + 관리자->>Controller: 상품 삭제 요청 + Controller->>Facade: 상품 삭제 위임 + Facade->>ProductService: 상품 존재 확인 + ProductService->>ProductRepository: 상품 존재 여부 확인 + + alt 상품이 존재하지 않는 경우 + ProductRepository-->>ProductService: 없음 + ProductService->>ProductService: 비즈니스 예외 발생 + ProductService-->>Facade: 예외 전파 + Facade-->>Controller: 예외 전파 + Controller-->>관리자: 존재하지 않음 안내 + end + + ProductRepository-->>ProductService: 상품 정보 + ProductService-->>Facade: 상품 정보 + Facade->>LikeService: 해당 상품 좋아요 전체 삭제 + LikeService->>LikeRepository: 좋아요 전체 삭제 (hard delete) + LikeRepository-->>LikeService: 삭제 완료 + LikeService-->>Facade: 삭제 완료 + Facade->>CartService: 해당 상품 장바구니 항목 전체 삭제 + CartService->>CartRepository: 장바구니 항목 전체 삭제 (hard delete) + CartRepository-->>CartService: 삭제 완료 + CartService-->>Facade: 삭제 완료 + Facade->>ProductService: 상품 삭제 + ProductService->>ProductRepository: 상품 삭제 (soft delete) + ProductRepository-->>ProductService: 삭제 완료 + ProductService-->>Facade: 삭제 완료 + Facade-->>Controller: 삭제 완료 + Controller-->>관리자: 상품 삭제 완료 응답 +``` + +#### 봐야 할 포인트 + +1. **삭제 정책의 혼합**: 좋아요(hard delete) → 장바구니 항목(hard delete) → 상품(soft delete) 순서로 처리한다. 종속 데이터를 먼저 정리해야 soft delete된 상품에 고아 데이터가 남는 불일치를 방지한다. +2. **US-B06과 동일한 패턴**: 브랜드 삭제 시 상품을 정리하듯, 상품 삭제 시 좋아요와 장바구니 항목을 정리한다. Facade가 도메인 간 삭제 순서를 결정한다. + +#### 상품 도메인 잠재 리스크 + +- **검증 위치의 기준**: 도메인 간 검증(브랜드 존재 확인)은 Facade가, 도메인 내부 규칙(상품명 중복, 브랜드 불변성)은 Service/Model이 처리한다. + +--- + +## 2.3 좋아요 (Like) + +> **삭제 정책**: 좋아요는 **hard delete**를 사용한다. 브랜드/상품과 달리 이력으로서 보존할 가치가 없으므로 물리적으로 삭제한다. + +### US-L01: 상품 좋아요 등록 + +#### 검증 목적 + +좋아요 등록 시 세 가지를 처리한다: 상품 존재 확인(도메인 간), 중복 좋아요 확인(BR-L01), 그리고 상품의 좋아요 수 업데이트. Facade가 LikeService와 ProductService를 조율하는 흐름을 확인한다. + +#### 시퀀스 다이어그램 + +```mermaid +sequenceDiagram + autonumber + actor 회원 + participant Controller as LikeV1Controller + participant Facade as LikeFacade + participant ProductService + participant ProductRepository + participant LikeService + participant LikeRepository + + 회원->>Controller: 좋아요 등록 요청 + Controller->>Facade: 좋아요 등록 위임 + Facade->>ProductService: 상품 존재 확인 + ProductService->>ProductRepository: 상품 존재 여부 확인 + + alt 상품이 존재하지 않는 경우 + ProductRepository-->>ProductService: 없음 + ProductService->>ProductService: 비즈니스 예외 발생 + ProductService-->>Facade: 예외 전파 + Facade-->>Controller: 예외 전파 + Controller-->>회원: 상품이 존재하지 않음 안내 + end + + ProductRepository-->>ProductService: 상품 정보 + ProductService-->>Facade: 상품 정보 + Facade->>LikeService: 좋아요 등록 + LikeService->>LikeRepository: 좋아요 존재 여부 확인 + + alt 이미 좋아요한 상품인 경우 + LikeRepository-->>LikeService: 좋아요 존재 + LikeService->>LikeService: 비즈니스 예외 발생 + LikeService-->>Facade: 예외 전파 + Facade-->>Controller: 예외 전파 + Controller-->>회원: 이미 좋아요 상태임 안내 + end + + LikeRepository-->>LikeService: 없음 + LikeService->>LikeRepository: 좋아요 저장 + LikeRepository-->>LikeService: 저장 완료 + LikeService-->>Facade: 좋아요 등록 완료 + Facade->>ProductService: 좋아요 수 증가 + ProductService->>ProductRepository: 좋아요 수 업데이트 + ProductRepository-->>ProductService: 업데이트 완료 + ProductService-->>Facade: 업데이트 완료 + Facade-->>Controller: 등록 완료 + Controller-->>회원: 좋아요 등록 완료 응답 +``` + +#### 봐야 할 포인트 + +1. **Facade의 3단계 조율**: 상품 존재 확인(ProductService) → 좋아요 등록(LikeService) → 좋아요 수 증가(ProductService). 세 단계를 하나의 트랜잭션으로 묶어야 한다. +2. **좋아요 수 업데이트 시점**: 좋아요 등록이 성공한 후에 수를 증가시킨다. 등록 실패(중복) 시에는 수를 변경하지 않는다. + +--- + +### US-L02: 상품 좋아요 취소 + +#### 검증 목적 + +좋아요 취소 시 좋아요 레코드를 hard delete하고, 상품의 좋아요 수를 감소시키는 흐름을 확인한다. 좋아요 취소는 도메인 간 조율이 필요한 cross-domain 처리이다. + +#### 시퀀스 다이어그램 + +```mermaid +sequenceDiagram + autonumber + actor 회원 + participant Controller as LikeV1Controller + participant Facade as LikeFacade + participant LikeService + participant LikeRepository + participant ProductService + participant ProductRepository + + 회원->>Controller: 좋아요 취소 요청 + Controller->>Facade: 좋아요 취소 위임 + Facade->>LikeService: 좋아요 취소 + LikeService->>LikeRepository: 좋아요 존재 여부 확인 + + alt 좋아요가 존재하지 않는 경우 + LikeRepository-->>LikeService: 없음 + LikeService->>LikeService: 비즈니스 예외 발생 + LikeService-->>Facade: 예외 전파 + Facade-->>Controller: 예외 전파 + Controller-->>회원: 좋아요 상태가 아님 안내 + end + + LikeRepository-->>LikeService: 좋아요 정보 + LikeService->>LikeRepository: 좋아요 삭제 (hard delete) + LikeRepository-->>LikeService: 삭제 완료 + LikeService-->>Facade: 삭제 완료 + Facade->>ProductService: 좋아요 수 감소 + ProductService->>ProductRepository: 좋아요 수 업데이트 + ProductRepository-->>ProductService: 업데이트 완료 + ProductService-->>Facade: 업데이트 완료 + Facade-->>Controller: 취소 완료 + Controller-->>회원: 좋아요 취소 완료 응답 +``` + +#### 봐야 할 포인트 + +1. **hard delete**: 좋아요 레코드는 물리적으로 삭제된다. soft delete와 달리 복원이 불가능하지만, 좋아요는 이력 보존이 불필요하다. +2. **좋아요 수 감소**: 삭제 성공 후 상품의 좋아요 수를 감소시킨다. US-L01의 역연산. + +--- + +### US-L03: 좋아요한 상품 목록 조회 + +#### 검증 목적 + +회원이 자신의 좋아요 목록만 조회할 수 있는지(BR-L03) 확인한다. 목록 조회이므로 예외 분기가 없다. + +#### 시퀀스 다이어그램 + +```mermaid +sequenceDiagram + autonumber + actor 회원 + participant Controller as LikeV1Controller + participant Facade as LikeFacade + participant Service as LikeService + participant Repository as LikeRepository + + 회원->>Controller: 좋아요 목록 조회 요청 + Controller->>Facade: 좋아요 목록 조회 위임 + Facade->>Service: 좋아요 목록 조회 + Service->>Repository: 회원의 좋아요 목록 조회 + Repository-->>Service: 좋아요 목록 + Service-->>Facade: 좋아요 목록 + Facade-->>Controller: 좋아요 목록 + Controller-->>회원: 좋아요 목록 응답 +``` + +#### 봐야 할 포인트 + +1. **BR-L03 소유권 제한**: 인증된 회원 ID를 기준으로 자신의 좋아요만 조회한다. Controller에서 인증 정보를 추출하여 전달한다. + +#### 좋아요 도메인 잠재 리스크 + +- **좋아요 수와 실제 레코드의 정합성**: 좋아요 수(Product 컬럼)와 실제 Like 레코드 수가 어긋날 수 있다. 트랜잭션 내에서 원자적으로 처리하되, 장기적으로는 배치로 보정하는 방어 전략도 고려할 수 있다. +- **좋아요 수 동시성**: 동일 상품에 여러 회원이 동시에 좋아요하면 좋아요 수 컬럼에 경합이 발생한다. 낙관적 잠금(@Version) 또는 원자적 증감(UPDATE SET count = count + 1)으로 방어가 필요하다. +- **동시 좋아요 요청**: 같은 회원이 동일 상품에 동시에 좋아요 요청을 보내면 중복이 발생할 수 있다. 유니크 제약 조건(DB 레벨)으로 방어하는 것이 안전하다. + +--- + +## 2.4 장바구니 (Cart) + +### US-C01: 장바구니에 상품 담기 + +#### 검증 목적 + +BR-C02에 따라 이미 장바구니에 있는 상품을 다시 담으면 수량이 누적된다. "신규 추가"와 "수량 누적"의 분기 처리 책임이 어느 계층에 있는지 확인한다. + +#### 시퀀스 다이어그램 + +```mermaid +sequenceDiagram + autonumber + actor 회원 + participant Controller as CartV1Controller + participant Facade as CartFacade + participant ProductService + participant ProductRepository + participant CartService + participant CartRepository + + 회원->>Controller: 장바구니 담기 요청 (상품, 수량) + Controller->>Facade: 장바구니 담기 위임 + Facade->>ProductService: 상품 존재 확인 + ProductService->>ProductRepository: 상품 존재 여부 확인 + + alt 상품이 존재하지 않는 경우 + ProductRepository-->>ProductService: 없음 + ProductService->>ProductService: 비즈니스 예외 발생 + ProductService-->>Facade: 예외 전파 + Facade-->>Controller: 예외 전파 + Controller-->>회원: 상품이 존재하지 않음 안내 + end + + ProductRepository-->>ProductService: 상품 정보 + ProductService-->>Facade: 상품 정보 + Facade->>CartService: 장바구니에 상품 담기 + CartService->>CartRepository: 장바구니 항목 조회 + + alt 이미 장바구니에 있는 상품인 경우 + CartRepository-->>CartService: 기존 장바구니 항목 + CartService->>CartRepository: 수량 누적 후 저장 + CartRepository-->>CartService: 저장 완료 + else 새로운 상품인 경우 + CartRepository-->>CartService: 없음 + CartService->>CartRepository: 새 장바구니 항목 저장 + CartRepository-->>CartService: 저장 완료 + end + + CartService-->>Facade: 담기 완료 + Facade-->>Controller: 담기 완료 + Controller-->>회원: 장바구니 담기 완료 응답 +``` + +#### 봐야 할 포인트 + +1. **수량 누적 판단의 책임**: CartService가 CartRepository를 통해 기존 항목 존재 여부를 확인하고, 존재하면 수량을 누적, 없으면 새 항목을 생성한다. Facade는 "담기"를 요청할 뿐, 신규/누적 분기를 알 필요가 없다. +2. **상품 검증은 Facade 책임**: 도메인 간 검증(상품 존재)은 US-P05, US-L01과 동일하게 Facade가 조율한다. + +--- + +### US-C02: 장바구니 조회 + +#### 검증 목적 + +회원이 자신의 장바구니만 조회하는 흐름(BR-C04)을 확인한다. + +#### 시퀀스 다이어그램 + +```mermaid +sequenceDiagram + autonumber + actor 회원 + participant Controller as CartV1Controller + participant Facade as CartFacade + participant Service as CartService + participant Repository as CartRepository + + 회원->>Controller: 장바구니 조회 요청 + Controller->>Facade: 장바구니 조회 위임 + Facade->>Service: 장바구니 조회 + Service->>Repository: 회원의 장바구니 항목 조회 + Repository-->>Service: 장바구니 항목 목록 + Service-->>Facade: 장바구니 항목 목록 + Facade-->>Controller: 장바구니 항목 목록 + Controller-->>회원: 장바구니 조회 응답 +``` + +#### 봐야 할 포인트 + +1. **BR-C04 소유권 제한**: 인증된 회원 ID 기준으로 자신의 장바구니만 조회한다. +2. **빈 장바구니도 정상 응답**: 장바구니에 항목이 없어도 빈 목록으로 정상 응답한다. + +--- + +### US-C03: 장바구니 상품 수량 변경 + +#### 검증 목적 + +수량 변경 시 BR-C03(수량 1 이상)과 장바구니 항목 존재 여부를 어느 계층에서 검증하는지 확인한다. + +#### 시퀀스 다이어그램 + +```mermaid +sequenceDiagram + autonumber + actor 회원 + participant Controller as CartV1Controller + participant Facade as CartFacade + participant Service as CartService + participant Repository as CartRepository + + 회원->>Controller: 수량 변경 요청 (상품, 새 수량) + Controller->>Facade: 수량 변경 위임 + Facade->>Service: 수량 변경 + Service->>Repository: 장바구니 항목 조회 + + alt 장바구니에 해당 상품이 없는 경우 + Repository-->>Service: 없음 + Service->>Service: 비즈니스 예외 발생 + Service-->>Facade: 예외 전파 + Facade-->>Controller: 예외 전파 + Controller-->>회원: 장바구니에 해당 상품 없음 안내 + end + + Repository-->>Service: 장바구니 항목 + Service->>Repository: 수량 변경 + Repository-->>Service: 변경 완료 + Service-->>Facade: 변경 완료 + Facade-->>Controller: 변경 완료 + Controller-->>회원: 수량 변경 완료 응답 +``` + +#### 봐야 할 포인트 + +1. **BR-C03 수량 검증 위치**: 수량이 1 이상인지 검증은 Service 또는 도메인 모델에서 처리한다. Controller의 요청 검증(@Valid)에서 먼저 걸러낼 수도 있다. + +--- + +### US-C04: 장바구니 상품 제거 + +#### 검증 목적 + +장바구니 항목 제거의 흐름을 확인한다. 존재하지 않는 항목 제거 시도에 대한 예외 처리를 검증한다. + +#### 시퀀스 다이어그램 + +```mermaid +sequenceDiagram + autonumber + actor 회원 + participant Controller as CartV1Controller + participant Facade as CartFacade + participant Service as CartService + participant Repository as CartRepository + + 회원->>Controller: 장바구니 상품 제거 요청 + Controller->>Facade: 상품 제거 위임 + Facade->>Service: 장바구니 항목 제거 + Service->>Repository: 장바구니 항목 조회 + + alt 장바구니에 해당 상품이 없는 경우 + Repository-->>Service: 없음 + Service->>Service: 비즈니스 예외 발생 + Service-->>Facade: 예외 전파 + Facade-->>Controller: 예외 전파 + Controller-->>회원: 장바구니에 해당 상품 없음 안내 + end + + Repository-->>Service: 장바구니 항목 + Service->>Repository: 항목 삭제 + Repository-->>Service: 삭제 완료 + Service-->>Facade: 제거 완료 + Facade-->>Controller: 제거 완료 + Controller-->>회원: 상품 제거 완료 응답 +``` + +#### 봐야 할 포인트 + +1. **US-C03과 동일한 전제**: 장바구니 항목의 존재 여부를 먼저 확인한다. early-return으로 예외를 빼고 정상 흐름은 블록 바깥에 둔다. + +#### 장바구니 도메인 잠재 리스크 + +- **수량 누적의 상한**: BR-C02에서 수량 누적에 상한이 없다. 재고보다 많은 수량을 장바구니에 담는 것을 허용할지, 담기 시점에 재고를 검증할지 결정이 필요하다. + +--- + +## 2.5 주문 (Order) + +### US-O01: 주문 생성 + +#### 검증 목적 + +가장 복잡한 흐름이다. BR-O01~O05가 모두 적용된다. 여러 상품의 재고 확인 → 주문 생성(스냅샷 포함) → 재고 차감이 하나의 트랜잭션으로 처리되어야 하며, Facade가 ProductService와 OrderService를 조율하는 흐름을 확인한다. + +#### 시퀀스 다이어그램 + +```mermaid +sequenceDiagram + autonumber + actor 회원 + participant Controller as OrderV1Controller + participant Facade as OrderFacade + participant ProductService + participant ProductRepository + participant OrderService + participant OrderRepository + + 회원->>Controller: 주문 요청 (상품 목록, 수량) + Controller->>Facade: 주문 생성 위임 + Facade->>ProductService: 상품 존재 및 재고 확인 + ProductService->>ProductRepository: 상품 존재 여부 확인 + + alt 상품이 존재하지 않는 경우 + ProductRepository-->>ProductService: 없음 + ProductService->>ProductService: 비즈니스 예외 발생 + ProductService-->>Facade: 예외 전파 + Facade-->>Controller: 예외 전파 + Controller-->>회원: 존재하지 않는 상품 안내 + end + + ProductRepository-->>ProductService: 상품 정보 + ProductService->>ProductService: 재고 충분 여부 확인 + + alt 재고가 부족한 경우 + ProductService->>ProductService: 비즈니스 예외 발생 + ProductService-->>Facade: 예외 전파 + Facade-->>Controller: 예외 전파 + Controller-->>회원: 재고 부족 안내 + end + + ProductService-->>Facade: 상품 정보 (스냅샷용) + Facade->>OrderService: 주문 생성 (스냅샷 포함) + OrderService->>OrderRepository: 주문 정보 저장 + OrderRepository-->>OrderService: 저장된 주문 정보 + OrderService-->>Facade: 주문 정보 + Facade->>ProductService: 재고 차감 + ProductService->>ProductRepository: 재고 수량 업데이트 + ProductRepository-->>ProductService: 업데이트 완료 + ProductService-->>Facade: 차감 완료 + Facade-->>Controller: 주문 정보 + Controller-->>회원: 주문 완료 응답 +``` + +#### 봐야 할 포인트 + +1. **Facade의 핵심 조율**: 이 시나리오에서 Facade의 존재 의미가 가장 명확하다. 재고 확인 → 주문 생성 → 재고 차감을 하나의 유스케이스로 조율한다. +2. **스냅샷 생성 시점**: ProductService에서 받은 상품 정보를 OrderService에 전달하여 스냅샷으로 보존한다. 스냅샷은 주문 시점의 상품명, 가격, 브랜드 등을 포함한다(BR-O05). +3. **재고 차감 순서**: 주문 생성 후 재고를 차감한다. 만약 재고 차감이 먼저라면, 주문 생성 실패 시 차감을 복원해야 하는 보상 로직이 필요해진다. + +--- + +### US-O02: 주문 목록 조회 (회원) + +#### 검증 목적 + +BR-O06(자신의 주문만 조회)과 BR-O08(기간 필터)이 적용되는 흐름을 확인한다. + +#### 시퀀스 다이어그램 + +```mermaid +sequenceDiagram + autonumber + actor 회원 + participant Controller as OrderV1Controller + participant Facade as OrderFacade + participant Service as OrderService + participant Repository as OrderRepository + + 회원->>Controller: 주문 목록 조회 요청 (시작일, 종료일) + Controller->>Facade: 주문 목록 조회 위임 + Facade->>Service: 주문 목록 조회 + Service->>Repository: 회원의 주문 목록 조회 (기간 필터) + Repository-->>Service: 주문 목록 + Service-->>Facade: 주문 목록 + Facade-->>Controller: 주문 목록 + Controller-->>회원: 주문 목록 응답 +``` + +#### 봐야 할 포인트 + +1. **BR-O06 + BR-O08**: 인증된 회원 ID와 기간 조건을 조합하여 조회한다. 기간이 지정되지 않은 경우의 기본값 정책도 결정이 필요하다. + +--- + +### US-O03: 주문 상세 조회 (회원) + +#### 검증 목적 + +회원이 자신의 주문만 조회할 수 있는지(BR-O06), 타인의 주문 접근 시 거부되는지 확인한다. + +#### 시퀀스 다이어그램 + +```mermaid +sequenceDiagram + autonumber + actor 회원 + participant Controller as OrderV1Controller + participant Facade as OrderFacade + participant Service as OrderService + participant Repository as OrderRepository + + 회원->>Controller: 주문 상세 조회 요청 + Controller->>Facade: 주문 조회 위임 + Facade->>Service: 주문 조회 + Service->>Repository: 주문 조회 + + alt 주문이 존재하지 않는 경우 + Repository-->>Service: 없음 + Service->>Service: 비즈니스 예외 발생 + Service-->>Facade: 예외 전파 + Facade-->>Controller: 예외 전파 + Controller-->>회원: 존재하지 않음 안내 + end + + Repository-->>Service: 주문 정보 + Service->>Service: 본인 주문 여부 확인 + + alt 다른 회원의 주문인 경우 + Service->>Service: 접근 거부 예외 발생 + Service-->>Facade: 예외 전파 + Facade-->>Controller: 예외 전파 + Controller-->>회원: 접근 거부 안내 + end + + Service-->>Facade: 주문 정보 (스냅샷 포함) + Facade-->>Controller: 주문 정보 + Controller-->>회원: 주문 상세 정보 응답 +``` + +#### 봐야 할 포인트 + +1. **이중 검증**: 존재 확인 → 소유권 확인의 2단계 early-return. "존재하지 않음"(NOT_FOUND)과 "접근 거부"(FORBIDDEN)는 다른 예외 타입이다. +2. **스냅샷 정보 포함**: 주문 상세에는 주문 당시의 상품/브랜드 정보(스냅샷)가 포함된다. + +--- + +### US-O04: 주문 목록 조회 (관리자) + +#### 검증 목적 + +관리자가 전체 주문을 페이지 단위로 조회하는 흐름(BR-O07)을 확인한다. + +#### 시퀀스 다이어그램 + +```mermaid +sequenceDiagram + autonumber + actor 관리자 + participant Controller as OrderAdminV1Controller + participant Facade as OrderAdminFacade + participant Service as OrderService + participant Repository as OrderRepository + + 관리자->>Controller: 전체 주문 목록 조회 요청 (페이징) + Controller->>Facade: 주문 목록 조회 위임 + Facade->>Service: 전체 주문 목록 조회 + Service->>Repository: 전체 주문 목록 조회 (페이징) + Repository-->>Service: 주문 목록 + Service-->>Facade: 주문 목록 + Facade-->>Controller: 주문 목록 + Controller-->>관리자: 주문 목록 응답 +``` + +#### 봐야 할 포인트 + +1. **소유권 제한 없음**: 관리자는 BR-O07에 따라 전체 주문을 조회할 수 있다. 회원 조회(US-O02)와 달리 소유권 필터가 없다. + +--- + +### US-O05: 주문 상세 조회 (관리자) + +#### 검증 목적 + +관리자의 단건 주문 조회를 확인한다. 회원 조회(US-O03)와 달리 소유권 확인이 불필요하다. + +#### 시퀀스 다이어그램 + +```mermaid +sequenceDiagram + autonumber + actor 관리자 + participant Controller as OrderAdminV1Controller + participant Facade as OrderAdminFacade + participant Service as OrderService + participant Repository as OrderRepository + + 관리자->>Controller: 주문 상세 조회 요청 + Controller->>Facade: 주문 조회 위임 + Facade->>Service: 주문 조회 + Service->>Repository: 주문 조회 + + alt 주문이 존재하지 않는 경우 + Repository-->>Service: 없음 + Service->>Service: 비즈니스 예외 발생 + Service-->>Facade: 예외 전파 + Facade-->>Controller: 예외 전파 + Controller-->>관리자: 존재하지 않음 안내 + end + + Repository-->>Service: 주문 정보 + Service-->>Facade: 주문 정보 (스냅샷 포함) + Facade-->>Controller: 주문 정보 + Controller-->>관리자: 주문 상세 정보 응답 +``` + +#### 봐야 할 포인트 + +1. **US-O03과의 차이**: 소유권 검증이 없다. 관리자는 모든 주문에 접근 가능하다. US-O03의 이중 검증과 달리 존재 확인만 수행한다. + +#### 주문 도메인 잠재 리스크 + +- **재고 차감의 원자성**: US-O01에서 재고 확인 → 주문 생성 → 재고 차감이 하나의 트랜잭션이어야 한다. 동시에 여러 주문이 같은 상품을 주문하면 재고가 음수가 될 수 있으므로, 비관적 잠금(SELECT FOR UPDATE) 또는 낙관적 잠금(@Version) 전략이 필요하다. +- **트랜잭션 범위의 비대화**: 주문 생성 트랜잭션이 ProductService(재고 확인/차감)와 OrderService(주문 생성)를 모두 포함하므로 범위가 넓다. 상품 수가 많으면 잠금 시간이 길어질 수 있다. +- **스냅샷 데이터의 정합성**: 스냅샷은 주문 시점의 데이터 사본이다. ProductService에서 상품 정보를 조회한 시점과 실제 저장 시점 사이에 상품 정보가 변경될 가능성은 트랜잭션으로 방어한다. diff --git a/.docs/design/03-class-diagrams.md b/.docs/design/03-class-diagrams.md new file mode 100644 index 00000000..cb4f4f1a --- /dev/null +++ b/.docs/design/03-class-diagrams.md @@ -0,0 +1,370 @@ +# 클래스 다이어그램 + +## 공통 사항 + +### BaseEntity + +모든 soft delete 정책 엔티티가 상속하는 추상 클래스이다. + +- `id` (Long): `@GeneratedValue(IDENTITY)` 자동 생성 +- `createdAt`, `updatedAt`: `@PrePersist`/`@PreUpdate` 자동 관리 +- `deletedAt`: soft delete 마커 (`delete()`, `restore()` 멱등 연산) +- `guard()`: 하위 클래스가 재정의하여 `@PrePersist`/`@PreUpdate` 시점에 불변 조건을 검증 + +### VO 설계 원칙 + +- **ID 없음**: 고유 식별자를 갖지 않는다. 속성 값으로만 동등성을 판단한다. +- **불변**: 생성 후 내부 상태가 변하지 않는다. 상태 변경이 필요하면 새 인스턴스를 반환한다. +- **분리 기준**: 2개 이상 도메인에서 재사용되거나, 자체 행위/검증 규칙이 있을 때 VO로 분리한다. + +| VO | 사용처 | 분리 근거 | +|----|--------|----------| +| Money | Product.price, OrderItem.price | 2곳 재사용 + 음수 불가 검증 | +| Stock | Product.stock | 자체 행위 3개 (decrease, increase, hasEnough) → Product 책임 분산 | +| Quantity | CartItem.quantity, OrderItem.quantity | 2곳 재사용 + >= 1 검증 (BR-C03, BR-O02) | + +### 연관 관계 원칙 + +- **모두 단방향**, ID 참조(Long)로 느슨하게 연결한다. +- 유일한 composition: **Order ◆── OrderItem** (Order가 Aggregate Root) +- 양방향 관계는 사용하지 않는다. + +--- + +## 전체 도메인 관계도 + +### 검증 목적 + +엔티티 간 관계, BaseEntity 상속 여부, VO 소속을 한눈에 파악한다. + +### 다이어그램 + +```mermaid +classDiagram + direction TB + + class BaseEntity { + <> + #Long id + #ZonedDateTime createdAt + #ZonedDateTime updatedAt + #ZonedDateTime deletedAt + +guard() void + +delete() void + +restore() void + } + + BaseEntity <|-- Brand + BaseEntity <|-- Product + BaseEntity <|-- Order + BaseEntity <|-- OrderItem + + Brand "1" <.. "*" Product : brandId + Product "1" <.. "*" Like : productId + Product "1" <.. "*" CartItem : productId + Product "1" <.. "*" OrderItem : productId + Order "1" *-- "1..*" OrderItem : orderItems + + Product *-- Money : price + Product *-- Stock : stock + CartItem *-- Quantity : quantity + OrderItem *-- Quantity : quantity + OrderItem *-- Money : price +``` + +### 봐야 할 포인트 + +1. **Like, CartItem은 BaseEntity 미상속**: 둘 다 hard delete 정책이므로 `deletedAt`이 불필요하다. BaseEntity를 상속하면 사용하지 않는 `deletedAt` 컬럼과 `delete()`/`restore()` 메서드가 노출되어 상속 계약을 위반한다. 나머지 4개 엔티티(Brand, Product, Order, OrderItem)는 soft delete를 사용한다. +2. **ID 참조**: 점선 화살표(`<..`)는 Long 타입 ID로 참조하는 느슨한 연관이다. JPA `@ManyToOne`이 아닌 `Long brandId` 필드로 표현된다. +3. **유일한 composition**: Order → OrderItem만 실선 다이아몬드(`*--`)로 표현한다. OrderItem은 Order 없이 존재할 수 없다. + +--- + +## 브랜드 (Brand) + +### 검증 목적 + +가장 단순한 엔티티로, BaseEntity 상속과 `guard()` 재정의 패턴의 기본 형태를 확인한다. + +### 다이어그램 + +```mermaid +classDiagram + class BaseEntity { + <> + #Long id + #ZonedDateTime createdAt + #ZonedDateTime updatedAt + #ZonedDateTime deletedAt + +guard() void + +delete() void + +restore() void + } + + class Brand { + -String name + +Brand(String name) + +update(String name) void + #guard() void + } + + BaseEntity <|-- Brand +``` + +### 봐야 할 포인트 + +1. **단일 필드**: `name` 하나만 관리한다. VO로 분리할 만큼 복잡한 검증 규칙이 없으므로 primitive `String`으로 유지한다. +2. **guard() 재정의**: `@PrePersist`/`@PreUpdate` 시점에 `name`이 null이거나 비어있지 않은지 검증한다. +3. **update() 메서드**: `name`만 수정 가능하다. 시퀀스 다이어그램(US-B05)에서 브랜드명 중복 확인은 Service 책임이며, 모델은 값의 유효성만 검증한다. + +--- + +## 상품 (Product + Money VO + Stock VO) + +### 검증 목적 + +4개의 관심사(name, price, stock, likeCount)를 가진 엔티티에서 VO를 통해 책임을 분산하는 패턴을 확인한다. BR-P02(브랜드 불변성)가 메서드 시그니처 수준에서 어떻게 보장되는지 확인한다. + +### 다이어그램 + +```mermaid +classDiagram + class BaseEntity { + <> + #Long id + #ZonedDateTime createdAt + #ZonedDateTime updatedAt + #ZonedDateTime deletedAt + +guard() void + +delete() void + +restore() void + } + + class Product { + -Long brandId + -String name + -Money price + -Stock stock + -int likeCount + +Product(Long brandId, String name, Money price, Stock stock) + +update(String name, Money price, Stock stock) void + +decreaseStock(Quantity quantity) void + +increaseStock(Quantity quantity) void + +increaseLikeCount() void + +decreaseLikeCount() void + #guard() void + } + + class Money { + <> + -int amount + +Money(int amount) + } + + class Stock { + <> + -int quantity + +Stock(int quantity) + +decrease(Quantity quantity) Stock + +increase(Quantity quantity) Stock + +hasEnough(Quantity quantity) boolean + } + + class Quantity { + <> + -int value + +Quantity(int value) + } + + BaseEntity <|-- Product + Product *-- Money : price + Product *-- Stock : stock + Stock ..> Quantity : uses + Product ..> Quantity : uses +``` + +### 봐야 할 포인트 + +1. **BR-P02 브랜드 불변성**: `update()` 메서드의 파라미터에 `brandId`가 없다. `brandId`를 변경할 수 있는 메서드가 존재하지 않으므로, 메서드 시그니처 수준에서 불변성이 보장된다. +2. **Money VO**: `amount`는 0 이상이어야 한다. 생성자에서 음수 검증을 수행한다. `Product.price`와 `OrderItem.price`에서 재사용된다. +3. **Stock VO**: 재고 관련 행위 3개(`decrease`, `increase`, `hasEnough`)를 캡슐화한다. 불변이므로 `decrease`/`increase`는 새 `Stock` 인스턴스를 반환한다. Product에서 재고 관련 로직을 분리하여 책임을 분산한다. +4. **Quantity VO**: `value >= 1` 검증을 생성자에서 수행한다 (BR-C03, BR-O02). `CartItem`과 `OrderItem`에서 재사용된다. +5. **likeCount는 primitive 유지**: 단순 증감만 수행하므로 VO로 분리할 근거가 없다. 증감 메서드(`increaseLikeCount`, `decreaseLikeCount`)를 Product가 직접 제공한다. +6. **Product.decreaseStock / increaseStock**: Stock VO에 위임하되, Product의 행위 메서드로 외부에 노출한다. Product가 "재고를 줄인다"는 도메인 의미를 유지하면서, 실제 로직은 Stock VO가 처리한다. + +--- + +## 좋아요 (Like) + +### 검증 목적 + +BaseEntity를 상속하지 않는 유일한 엔티티이다. hard delete 정책에 따라 자체적으로 `id`와 `createdAt`만 보유하는 구조를 확인한다. + +### 다이어그램 + +```mermaid +classDiagram + class Like { + -Long id + -Long userId + -Long productId + -ZonedDateTime createdAt + +Like(Long userId, Long productId) + } + + note for Like "BaseEntity 미상속\nhard delete 정책\nupdatedAt/deletedAt 불필요" +``` + +### 봐야 할 포인트 + +1. **BaseEntity 미상속 이유**: Like는 (1) hard delete 정책이므로 `deletedAt`이 불필요하고, (2) 생성 후 수정이 없으므로 `updatedAt`이 불필요하다. BaseEntity의 4개 필드 중 `id`와 `createdAt`만 필요하므로 자체 필드로 선언한다. +2. **행위 메서드 없음**: Like는 생성과 삭제만 존재한다. 상태 변경이 없으므로 행위 메서드가 불필요하다. +3. **유니크 제약**: `(userId, productId)` 조합이 유일해야 한다 (BR-L01). 이는 DB 유니크 제약과 Service 레벨 중복 검증으로 이중 방어한다. +4. **ID 참조**: `userId`와 `productId`는 Long 타입으로 느슨하게 참조한다. User, Product 엔티티에 대한 직접 참조(`@ManyToOne`)를 사용하지 않는다. + +--- + +## 장바구니 (CartItem + Quantity VO) + +### 검증 목적 + +CartItem이 Quantity VO를 통해 수량 검증(BR-C03)을 위임하는 구조와, 수량 누적(BR-C02)이 도메인 모델의 행위 메서드로 표현되는지 확인한다. Like와 마찬가지로 hard delete 정책이므로 BaseEntity를 상속하지 않는다. + +### 다이어그램 + +```mermaid +classDiagram + class CartItem { + -Long id + -Long userId + -Long productId + -Quantity quantity + -ZonedDateTime createdAt + +CartItem(Long userId, Long productId, Quantity quantity) + +addQuantity(Quantity quantity) void + +changeQuantity(Quantity quantity) void + } + + class Quantity { + <> + -int value + +Quantity(int value) + +add(Quantity other) Quantity + } + + CartItem *-- Quantity : quantity + + note for CartItem "BaseEntity 미상속\nhard delete 정책\ndeletedAt 불필요" +``` + +### 봐야 할 포인트 + +1. **BaseEntity 미상속 이유**: CartItem은 hard delete 정책이다. 회원이 장바구니에서 상품을 제거하면 물리 삭제하며, 관리자가 상품/브랜드를 삭제할 때도 해당 장바구니 항목을 물리 삭제한다. 이력 보존이 불필요하므로 `deletedAt`이 필요 없고, BaseEntity의 `delete()`/`restore()` 메서드가 노출되면 안 된다. +2. **addQuantity()**: BR-C02(수량 누적)를 구현한다. 이미 장바구니에 있는 상품을 다시 담으면, CartService가 기존 CartItem의 `addQuantity()`를 호출하여 수량을 누적한다. 내부적으로 Quantity VO의 `add()`에 위임한다. +3. **changeQuantity()**: US-C03(수량 변경)을 구현한다. 새 Quantity를 받아 교체한다. Quantity 생성자에서 `value >= 1` 검증이 수행되므로, 0 이하 수량은 VO 레벨에서 거부된다. +4. **Quantity.add()**: 불변 VO이므로 두 Quantity의 합산 결과를 새 인스턴스로 반환한다. `value >= 1` 검증은 생성자에서 수행되므로 `add()` 결과도 자동으로 유효하다. +5. **Cart 엔티티 없음**: BR-C01("회원은 하나의 장바구니를 가진다")이지만, 장바구니 자체를 엔티티로 두지 않고 `CartItem.userId`로 회원의 장바구니를 식별한다. CartItem의 집합이 곧 해당 회원의 장바구니이다. + +--- + +## 주문 (Order + OrderItem) + +### 검증 목적 + +가장 복잡한 도메인이다. Order가 Aggregate Root로서 OrderItem을 포함한다. OrderItem 자체가 주문 시점의 상품 정보를 보존하는 스냅샷 역할을 한다. BR-O01(최소 1개 항목), BR-O06(소유권 검증)이 도메인 모델에서 어떻게 보장되는지 확인한다. + +### 다이어그램 + +```mermaid +classDiagram + class BaseEntity { + <> + #Long id + #ZonedDateTime createdAt + #ZonedDateTime updatedAt + #ZonedDateTime deletedAt + +guard() void + +delete() void + +restore() void + } + + class Order { + -Long userId + -List~OrderItem~ orderItems + +Order(Long userId, List~OrderItem~ orderItems) + +isOwnedBy(Long userId) boolean + #guard() void + } + + class OrderItem { + -Long orderId + -Long productId + -Quantity quantity + -String productName + -String brandName + -Money price + +OrderItem(Long productId, Quantity quantity, String productName, String brandName, Money price) + #guard() void + } + + class Quantity { + <> + -int value + +Quantity(int value) + } + + class Money { + <> + -int amount + +Money(int amount) + } + + BaseEntity <|-- Order + BaseEntity <|-- OrderItem + Order "1" *-- "1..*" OrderItem : orderItems + OrderItem *-- Quantity : quantity + OrderItem *-- Money : price +``` + +### 봐야 할 포인트 + +1. **BR-O01 최소 항목 검증**: Order 생성자에서 `orderItems`가 비어있으면 예외를 발생시킨다. 빈 주문이 생성되는 것을 도메인 모델 수준에서 원천 차단한다. +2. **BR-O06 소유권 검증**: `isOwnedBy(userId)` 메서드로 주문의 소유자 여부를 판단한다. US-O03 시퀀스에서 Service가 이 메서드를 호출하여 타인의 주문 접근을 거부한다. +3. **OrderItem이 곧 스냅샷이다**: OrderItem이 `productName`, `brandName`, `price`를 직접 보유하여 주문 시점의 상품 정보를 보존한다 (BR-O05). OrderItem의 존재 이유 자체가 "주문 시점의 정보 보존"이므로, 별도 스냅샷 VO를 두지 않고 필드를 직접 갖는다. 원본 Product나 Brand가 이후 수정/삭제되어도 주문 기록에는 영향이 없다. +4. **Aggregate 경계**: Order가 Aggregate Root이고, OrderItem은 Order를 통해서만 접근한다. `Order.orderItems`는 `@OneToMany(cascade = ALL, orphanRemoval = true)`로 생명 주기를 함께 관리한다. +5. **OrderItem.orderId**: OrderItem이 BaseEntity를 상속하여 자체 id를 가진다. `orderId`는 DB 외래 키로 Order와 연결되지만, 도메인 모델에서는 Order가 `List`으로 직접 참조한다. +6. **Money 재사용**: `OrderItem.price`는 `Product.price`와 동일한 Money VO를 사용한다. VO 분리의 이점이 여기서 드러난다. + +--- + +## 잠재 리스크 + +### 책임 분산 점검표 + +| 엔티티 | 관심사 | 분산 방식 | 점검 | +|--------|--------|----------|------| +| Product | name | String (단순) | 검증만 필요, VO 불필요 | +| Product | price | Money VO | 음수 검증 위임 | +| Product | stock | Stock VO | 행위 3개(decrease, increase, hasEnough) 위임 | +| Product | likeCount | int (단순) | 증감만, VO 불필요 | +| CartItem | quantity | Quantity VO | >= 1 검증 + 수량 합산 위임 | +| OrderItem | quantity | Quantity VO | >= 1 검증 위임 | +| OrderItem | productName, brandName | String (단순) | 스냅샷 필드, OrderItem 자체가 스냅샷이므로 VO 불필요 | +| OrderItem | price | Money VO | Product.price와 동일 VO 재사용 | + +### VO 설계 리스크 + +| 리스크 | 설명 | 대응 | +|--------|------|------| +| **Stock 불변성과 JPA 매핑** | Stock VO가 불변이므로 `decrease()`가 새 인스턴스를 반환한다. JPA `@Embedded`로 매핑할 때 setter가 필요한지 확인이 필요하다 | `@Embedded` + `@Column`으로 매핑하되, JPA 접근용 protected 기본 생성자만 허용한다. 상태 변경은 `Product.decreaseStock()`이 새 Stock을 할당하는 방식으로 처리한다 | +| **Quantity 재사용 범위** | CartItem과 OrderItem에서 동일한 Quantity VO를 사용한다. 두 도메인의 수량 규칙이 달라질 가능성이 있다 | 현재는 동일한 규칙(>= 1)이므로 공유한다. 규칙이 분기되는 시점에 각 도메인 전용 VO로 분리한다 | +| **Money 확장 가능성** | 현재 `int amount`로 원화만 지원한다. 통화 단위가 추가되면 VO 구조가 변경된다 | 현재 범위에서는 원화 단일 통화로 충분하다. 다중 통화 요구가 확정되면 `currency` 필드를 추가한다 | + +### 도메인 간 정합성 리스크 + +| 리스크 | 관련 도메인 | 설명 | +|--------|------------|------| +| **좋아요 수 불일치** | Product ↔ Like | `Product.likeCount`와 실제 Like 레코드 수가 어긋날 수 있다. 트랜잭션 내 원자적 처리 + 배치 보정 전략이 필요하다 | +| ~~장바구니 상품 삭제~~ | ~~CartItem ↔ Product~~ | **해결됨**: 상품/브랜드 삭제 시 해당 장바구니 항목을 함께 물리 삭제한다. Like와 동일한 패턴 | +| **스냅샷 시점 정합성** | OrderItem ↔ Product | Facade에서 상품 정보를 조회한 시점과 Order를 저장하는 시점 사이에 상품 정보가 변경될 수 있다. 트랜잭션 격리 수준으로 방어한다 | +| **재고 동시성** | Product.stock ↔ Order | 동시 주문 시 재고가 음수가 될 수 있다. 비관적 잠금(SELECT FOR UPDATE) 또는 Stock VO의 `decrease()`에서 음수 검증으로 방어한다 | diff --git a/.docs/design/04-erd.md b/.docs/design/04-erd.md new file mode 100644 index 00000000..7f056cfc --- /dev/null +++ b/.docs/design/04-erd.md @@ -0,0 +1,337 @@ +# ERD + +## 공통 사항 + +### BaseEntity 컬럼 + +soft delete 정책 엔티티(brand, product, orders, order_item)는 아래 4개 컬럼을 공통으로 가진다. + +| 컬럼 | 타입 | 제약 | 설명 | +|------|------|------|------| +| id | BIGINT | PK, AUTO_INCREMENT | 고유 식별자 | +| created_at | DATETIME | NOT NULL | 생성 시각 (UTC) | +| updated_at | DATETIME | NOT NULL | 최종 수정 시각 (UTC) | +| deleted_at | DATETIME | nullable | soft delete 마커. NULL이면 활성 상태 | + +hard delete 정책 엔티티(likes, cart_item)는 `id`와 `created_at`만 자체 보유한다. + +### 컬럼 컨벤션 + +- 네이밍: snake_case (Spring Boot 기본 네이밍 전략) +- 타임존: UTC 저장 (`hibernate.timezone.default_storage: NORMALIZE_UTC`) +- FK 컬럼: `{참조 대상}_id` (예: `brand_id`, `user_id`) +- VO 매핑: `@Embedded`로 VO의 내부 값을 컬럼으로 펼친다 (예: Money.amount → `price`, Stock.quantity → `stock`) + +### FK 정책 + +- 본 ERD에서 "FK"로 표기된 컬럼은 **논리적 참조**를 의미한다. DB에 물리적 `FOREIGN KEY` 제약은 생성하지 않는다. +- 클래스 다이어그램에서 `@ManyToOne`이 아닌 `Long brandId`처럼 ID 값으로 느슨하게 참조하는 설계와 일치한다. +- 참조 무결성은 애플리케이션 레벨에서 보장한다: Facade가 존재 확인 → 저장 순서를 조율하고, 삭제 시 종속 데이터를 먼저 정리한다 (시퀀스 다이어그램 US-B06, US-P07 참고). + +--- + +## 전체 ERD + +### 검증 목적 + +모든 테이블의 관계, FK 방향, 카디널리티를 한눈에 파악한다. 클래스 다이어그램의 ID 참조가 실제 FK로 어떻게 표현되는지 확인한다. + +### 다이어그램 + +```mermaid +erDiagram + users { + bigint id PK + varchar login_id UK + varchar password + varchar name + date birthday + varchar email + datetime created_at + datetime updated_at + datetime deleted_at + } + + brand { + bigint id PK + varchar name + datetime created_at + datetime updated_at + datetime deleted_at + } + + product { + bigint id PK + bigint brand_id FK + varchar name + int price + int stock + int like_count + datetime created_at + datetime updated_at + datetime deleted_at + } + + likes { + bigint id PK + bigint user_id FK + bigint product_id FK + datetime created_at + } + + cart_item { + bigint id PK + bigint user_id FK + bigint product_id FK + int quantity + datetime created_at + } + + orders { + bigint id PK + bigint user_id FK + datetime created_at + datetime updated_at + datetime deleted_at + } + + order_item { + bigint id PK + bigint order_id FK + bigint product_id FK + int quantity + varchar product_name + varchar brand_name + int price + datetime created_at + datetime updated_at + datetime deleted_at + } + + users ||--o{ likes : "" + users ||--o{ cart_item : "" + users ||--o{ orders : "" + brand ||--o{ product : "" + product ||--o{ likes : "" + product ||--o{ cart_item : "" + product ||--o{ order_item : "" + orders ||--|{ order_item : "" +``` + +### 봐야 할 포인트 + +1. **users 테이블은 기존 구현**: 회원 도메인은 이미 구현되어 있으며 본 ERD에서는 FK 참조 대상으로만 포함한다. +2. **orders ↔ order_item**: 유일한 `||--|{` 관계(1:1이상). Order는 최소 1개의 OrderItem을 포함해야 한다 (BR-O01). 나머지는 모두 `||--o{`(1:0이상)이다. +3. **likes, cart_item에 deleted_at 없음**: hard delete 정책이므로 soft delete 컬럼이 불필요하다. +4. **order_item의 스냅샷 컬럼**: `product_name`, `brand_name`, `price`는 주문 시점의 상품 정보 사본이다. product, brand 테이블의 현재 값과 무관하게 주문 기록을 보존한다 (BR-O05). + +--- + +## 테이블 상세 + +### brand + +| 컬럼 | 타입 | 제약 | 설명 | +|------|------|------|------| +| id | BIGINT | PK, AUTO_INCREMENT | | +| name | VARCHAR(255) | NOT NULL | 브랜드명 | +| created_at | DATETIME | NOT NULL | | +| updated_at | DATETIME | NOT NULL | | +| deleted_at | DATETIME | | soft delete 마커 | + +**유일성 제약**: +- `name`은 활성 상태(deleted_at IS NULL) 브랜드 내에서 유일해야 한다 (US-B04, US-B05) +- soft delete 테이블이므로 DB UNIQUE 제약만으로는 보장할 수 없다 → **애플리케이션 레벨에서 검증** (자세한 내용은 [데이터 정합성 전략 > 유일성 제약](#유일성-제약) 참고) + +--- + +### product + +| 컬럼 | 타입 | 제약 | 설명 | +|------|------|------|------| +| id | BIGINT | PK, AUTO_INCREMENT | | +| brand_id | BIGINT | NOT NULL, FK → brand(id) | 소속 브랜드. 생성 후 변경 불가 (BR-P02) | +| name | VARCHAR(255) | NOT NULL | 상품명 | +| price | INT | NOT NULL, >= 0 | 가격 (Money VO) | +| stock | INT | NOT NULL, >= 0 | 재고 수량 (Stock VO) | +| like_count | INT | NOT NULL, DEFAULT 0, >= 0 | 좋아요 수 | +| created_at | DATETIME | NOT NULL | | +| updated_at | DATETIME | NOT NULL | | +| deleted_at | DATETIME | | soft delete 마커 | + +**유일성 제약**: +- `(brand_id, name)` 조합이 활성 상태 상품 내에서 유일해야 한다 (US-P05, US-P06) +- soft delete 테이블이므로 → **애플리케이션 레벨에서 검증** + +**인덱스**: +- `idx_product_brand_id` → `brand_id`: 브랜드별 상품 필터링 (US-P01, US-P03) + +--- + +### likes + +| 컬럼 | 타입 | 제약 | 설명 | +|------|------|------|------| +| id | BIGINT | PK, AUTO_INCREMENT | | +| user_id | BIGINT | NOT NULL, FK → users(id) | 좋아요 누른 회원 | +| product_id | BIGINT | NOT NULL, FK → product(id) | 좋아요 대상 상품 | +| created_at | DATETIME | NOT NULL | | + +**유일성 제약**: +- `uk_likes_user_product` → `UNIQUE(user_id, product_id)`: 한 회원은 동일 상품에 좋아요를 한 번만 등록 (BR-L01) +- hard delete 테이블이므로 **DB UNIQUE 제약으로 완전히 보장 가능**. 삭제 시 행이 물리적으로 제거되어 제약 슬롯이 해제된다 + +**인덱스**: +- `uk_likes_user_product`이 `(user_id, product_id)` 순서이므로, `user_id` 기준 조회(US-L03: 내 좋아요 목록)를 커버한다 + +--- + +### cart_item + +| 컬럼 | 타입 | 제약 | 설명 | +|------|------|------|------| +| id | BIGINT | PK, AUTO_INCREMENT | | +| user_id | BIGINT | NOT NULL, FK → users(id) | 장바구니 소유 회원 | +| product_id | BIGINT | NOT NULL, FK → product(id) | 담긴 상품 | +| quantity | INT | NOT NULL, >= 1 | 수량 (Quantity VO) | +| created_at | DATETIME | NOT NULL | | + +**유일성 제약**: +- `uk_cart_item_user_product` → `UNIQUE(user_id, product_id)`: 회원당 상품당 하나의 장바구니 항목만 존재. 같은 상품을 다시 담으면 기존 항목의 수량을 누적한다 (BR-C02) +- hard delete 테이블이므로 **DB UNIQUE 제약으로 완전히 보장 가능** + +**인덱스**: +- `uk_cart_item_user_product`이 `(user_id, product_id)` 순서이므로, `user_id` 기준 조회(US-C02: 내 장바구니)를 커버한다 + +--- + +### orders + +| 컬럼 | 타입 | 제약 | 설명 | +|------|------|------|------| +| id | BIGINT | PK, AUTO_INCREMENT | | +| user_id | BIGINT | NOT NULL, FK → users(id) | 주문한 회원 | +| created_at | DATETIME | NOT NULL | 주문 시각 | +| updated_at | DATETIME | NOT NULL | | +| deleted_at | DATETIME | | soft delete 마커 | + +**인덱스**: +- `idx_orders_user_id_created_at` → `(user_id, created_at)`: 회원의 기간별 주문 목록 조회 (US-O02, BR-O08) + +--- + +### order_item + +| 컬럼 | 타입 | 제약 | 설명 | +|------|------|------|------| +| id | BIGINT | PK, AUTO_INCREMENT | | +| order_id | BIGINT | NOT NULL, FK → orders(id) | 소속 주문 | +| product_id | BIGINT | NOT NULL, FK → product(id) | 주문 대상 상품 (원본 추적용) | +| quantity | INT | NOT NULL, >= 1 | 주문 수량 (Quantity VO) | +| product_name | VARCHAR(255) | NOT NULL | 주문 시점 상품명 (스냅샷) | +| brand_name | VARCHAR(255) | NOT NULL | 주문 시점 브랜드명 (스냅샷) | +| price | INT | NOT NULL, >= 0 | 주문 시점 가격 (스냅샷, Money VO) | +| created_at | DATETIME | NOT NULL | | +| updated_at | DATETIME | NOT NULL | | +| deleted_at | DATETIME | | soft delete 마커 | + +**인덱스**: +- `idx_order_item_order_id` → `order_id`: 주문 상세 조회 시 주문 항목 일괄 로딩 (US-O03, US-O05) + +--- + +## 데이터 정합성 전략 + +### 참조 무결성 + +물리적 FK 제약 없이 애플리케이션 레벨에서 참조 무결성을 보장한다. 아래 표는 논리적 참조 관계와 그 정합성 보장 방식을 정리한 것이다. + +| 논리적 참조 | 목적 | 정합성 보장 방식 | +|------------|------|----------------| +| product.brand_id → brand(id) | 상품은 반드시 존재하는 브랜드에 속한다 (BR-P01) | Facade가 브랜드 존재 확인 후 상품 등록 (US-P05) | +| likes.user_id → users(id) | 좋아요는 실존 회원만 가능 | AuthInterceptor가 인증된 회원만 허용 | +| likes.product_id → product(id) | 좋아요는 실존 상품만 가능 | Facade가 상품 존재 확인 후 좋아요 등록 (US-L01). 상품 삭제 시 likes를 먼저 hard delete (US-P07) | +| cart_item.user_id → users(id) | 장바구니는 실존 회원만 가능 | AuthInterceptor가 인증된 회원만 허용 | +| cart_item.product_id → product(id) | 장바구니는 실존 상품만 가능 | Facade가 상품 존재 확인 후 담기 (US-C01). 상품 삭제 시 cart_item을 먼저 hard delete (US-P07) | +| orders.user_id → users(id) | 주문은 실존 회원만 가능 | AuthInterceptor가 인증된 회원만 허용 | +| order_item.order_id → orders(id) | 주문 항목은 반드시 주문에 소속 | Order Aggregate가 OrderItem 생명주기를 관리 (cascade) | +| order_item.product_id → product(id) | 원본 상품 추적용 참조 | Facade가 상품 존재 및 재고 확인 후 주문 생성 (US-O01). 스냅샷 컬럼이 실제 데이터를 보존 | + +**삭제 시 FK 정합성 보장 순서:** + +상품/브랜드 삭제 시 Facade가 종속 데이터를 먼저 정리한다 (시퀀스 다이어그램 US-B06, US-P07 참고). + +``` +브랜드 삭제: likes(hard delete) → cart_item(hard delete) → product(soft delete) → brand(soft delete) +상품 삭제: likes(hard delete) → cart_item(hard delete) → product(soft delete) +``` + +종속 데이터(likes, cart_item)를 먼저 물리 삭제하므로, 상위 엔티티 soft delete 후에도 고아 FK가 남지 않는다. + +### 유일성 제약 + +삭제 정책에 따라 DB UNIQUE 제약의 적용 가능 여부가 달라진다. + +#### hard delete 테이블 → DB 레벨 UNIQUE 가능 + +| 테이블 | 제약 | 비즈니스 규칙 | +|--------|------|-------------| +| likes | `UNIQUE(user_id, product_id)` | BR-L01: 회원당 상품당 좋아요 1개 | +| cart_item | `UNIQUE(user_id, product_id)` | BR-C02: 회원당 상품당 장바구니 항목 1개 | + +행이 물리적으로 삭제되므로, 삭제 후 같은 조합으로 재등록이 가능하다. DB UNIQUE 제약이 완전하게 동작한다. + +#### soft delete 테이블 → 애플리케이션 레벨 검증 필요 + +| 테이블 | 유일 범위 | 비즈니스 규칙 | +|--------|----------|-------------| +| brand | `name` (활성 상태 내) | US-B04: 브랜드명 중복 불가 | +| product | `(brand_id, name)` (활성 상태 내) | US-P05: 같은 브랜드 내 상품명 중복 불가 | + +soft delete 테이블에서 단순 `UNIQUE(name)` 제약을 걸면 다음 문제가 발생한다: + +``` +1. 브랜드 "A" 등록 → name="A", deleted_at=NULL ← 정상 +2. 브랜드 "A" 삭제 (soft) → name="A", deleted_at=2024-... ← 행이 남아있음 +3. 브랜드 "A" 재등록 → name="A", deleted_at=NULL ← UNIQUE 위반! +``` + +삭제된 행이 물리적으로 남아있기 때문에, 같은 이름으로 재등록할 수 없게 된다. 따라서 Service 레벨에서 `WHERE deleted_at IS NULL AND name = ?` 조건으로 활성 데이터만 대상으로 중복을 검증한다. + +### 도메인 제약 + +VO의 검증 규칙이 DB 컬럼 제약으로도 방어된다. + +| 컬럼 | 제약 | 근거 | +|------|------|------| +| product.price | >= 0 | Money VO: 음수 불가 | +| product.stock | >= 0 | Stock VO: 음수 불가 | +| product.like_count | >= 0, DEFAULT 0 | 좋아요 수는 음수가 될 수 없다 | +| cart_item.quantity | >= 1 | Quantity VO: BR-C03 | +| order_item.quantity | >= 1 | Quantity VO: BR-O02 | +| order_item.price | >= 0 | Money VO: 음수 불가 | + +이중 방어 전략: VO 생성자에서 1차 검증 + DB CHECK 제약에서 2차 방어. 애플리케이션 버그로 잘못된 값이 전달되더라도 DB에서 최종 차단한다. + +### 인덱스 전략 + +시퀀스 다이어그램의 주요 조회 패턴에 맞춰 인덱스를 설계한다. + +| 인덱스 | 대상 조회 | 비고 | +|--------|----------|------| +| `idx_product_brand_id` | US-P01: 브랜드별 상품 필터링 | | +| `uk_likes_user_product` | US-L03: 내 좋아요 목록 | UNIQUE 제약이 인덱스 역할도 수행 | +| `uk_cart_item_user_product` | US-C02: 내 장바구니 조회 | UNIQUE 제약이 인덱스 역할도 수행 | +| `idx_orders_user_id_created_at` | US-O02: 기간별 주문 목록 | 복합 인덱스로 user_id 필터 + created_at 범위 검색을 커버 | +| `idx_order_item_order_id` | US-O03, O05: 주문 상세 | 주문 ID로 주문 항목 일괄 조회 | + +--- + +## 잠재 리스크 + +| 리스크 | 설명 | 대응 | +|--------|------|------| +| **soft delete 유일성 우회** | 애플리케이션 레벨 검증은 동시 요청 시 race condition이 발생할 수 있다 | 트랜잭션 격리 수준 또는 비관적 잠금으로 방어. 구현 시 결정 | +| **like_count 정합성** | Product.like_count와 likes 테이블의 실제 레코드 수가 어긋날 수 있다 | 트랜잭션 내 원자적 처리로 1차 방어. 필요 시 배치 보정으로 2차 방어 | +| **스냅샷 시점 정합성** | 주문 생성 중 상품 정보가 변경될 수 있다 | Facade 트랜잭션 내에서 상품 조회 → 주문 생성이 원자적으로 처리됨 | +| **재고 동시성** | 동시 주문 시 재고가 음수가 될 수 있다 | Stock >= 0 CHECK 제약이 DB 레벨 최종 방어선. 잠금 전략은 구현 시 결정 | diff --git a/build.gradle.kts b/build.gradle.kts index 9c8490b8..dc167f2e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -42,6 +42,7 @@ subprojects { dependencyManagement { imports { mavenBom("org.springframework.cloud:spring-cloud-dependencies:${project.properties["springCloudDependenciesVersion"]}") + mavenBom("org.testcontainers:testcontainers-bom:${project.properties["testcontainersVersion"]}") } } diff --git a/gradle.properties b/gradle.properties index 142d7120..5ae37ac9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,6 +10,7 @@ springBootVersion=3.4.4 springDependencyManagementVersion=1.1.7 springCloudDependenciesVersion=2024.0.1 ### Library versions ### +testcontainersVersion=2.0.2 springDocOpenApiVersion=2.7.0 springMockkVersion=4.0.2 mockitoVersion=5.14.0