Skip to content

[volume-3] 도메인 구현 - 김진수#116

Open
plan11plan wants to merge 108 commits intoLoopers-dev-lab:plan11planfrom
plan11plan:main
Open

[volume-3] 도메인 구현 - 김진수#116
plan11plan wants to merge 108 commits intoLoopers-dev-lab:plan11planfrom
plan11plan:main

Conversation

@plan11plan
Copy link

@plan11plan plan11plan commented Feb 27, 2026

Summary

  • 배경: 6개 도메인(User, Brand, Product, Like, Order, Cart)이 미구현 상태로, 비즈니스 규칙과 레이어 간 책임 경계가 없었음
  • 목표: 레이어드 아키텍처(interfaces / application / domain / infrastructure) 의 규칙 만들기 및 지키기
  • 결과: 6개 도메인 × 전 레이어 구현 및 Cross-Domain 조합 책임을 Facade로 분리하여 레이어 경계 정립

개인 목표 및 회고

목표

  • 레이어별 컨벤션 만들기
  • AI 코드의 컨벤션 준수율 100%에 가깝게 만들기
  • 연관관계 매핑을 했으면 왜 이렇게 했는지 스스로 납득할 수 있기

행동

  • AI에게 코딩을 맡기고, 방향성을 수정하며 아키텍처 문서를 계속 업데이트했다.
  • 컨텍스트가 길어질수록 컨벤션 준수율이 떨어지는 문제를 발견했다. 이를 개선하기 위해 시행착오를 거치며 서브 에이전트를 두는 방식으로 결정했다.
    • 기능 구현 완료 후, 커밋 전에 컨벤션 리뷰 에이전트(Sonnet)가 코드를 검수한다.
    • 컨벤션 문서를 참조 자료로 활용하여 위반 항목을 식별하고 수정한다.
  • JPA 연관관계 매핑 지식이 부족한 상태에서 출발했기 때문에, 초기 행동에는 근거가 없었다.
  • 학습하면서 도메인 간 같은 바운더리에 둬야 할지, 분리해야 할지를 다음 기준으로 고민했다.
    • 같은 생명주기인가?
    • 비즈니스적으로 별도로 분리해야 하는가?
    • 함께 변경되는가?

아쉬웠던 점

  • 도메인 간 협력 흐름을 설계하고, 필요한 로직을 도메인 서비스로 분리하는 것을 내가 주도적으로 했는가? 그렇지 않은것 같다.
    기본 Application 컨벤션이 "도메인 안의 XXService(Repository 존재)를 조합하는 것"이다 보니, 일단 도메인들을 전부 패키지 분리로 시작하고 이후 합치는 상황을 고려하게 되었다.

예를 들어 Order-OrderItem, Brand-Product는 생명주기가 같은가? 같으면 같은 패키지로 두고, 다르면 "비즈니스적으로 항상 같이 보여주는가?"라는 관점으로 합치고 연관관계를 맺을지 고민했다.

Brand-Product의 경우, 비즈니스 특성상 Brand는 한번 생성되면 거의 바뀌지 않고, Application 레이어에서 조회 성능 이점을 가져갈 수 있어서 패키지 분리를 택했다. 이러다 보니 생명주기가 같지 않는 이상 항상 무조건 쪼개는 패턴으로 흐를까 봐 걱정이다.

  • 개인적으로 DIP나 레이어별 책임이나 이런 고민들은, AI와 코딩하며, 그때 그때 컨벤션 문서를 업데이트해나가는식으로 개선해갔고, 레이버별 책임이 탄탄해진 것 같으나, 누가 내게 물어보면 말을 할 수 있겠는데, 문서 없이는 힘들 것 같아서 내가 왜 이렇게 규칙을 정했는지 계속 생각하고 읽어봐야겠다.

  • 도메인간 경계를 무너뜨리고 합칠지, 분리할지가 이번 미션에서 나의 큰 관심사중 하나였다. Brand 1: N Product는 같은 패키지안에 둬야하나, 분리해야하나, Product에 like 필드를 지금 상황에서는 두는게 맞는것같은데, 미래를 생각해서 또는 비즈니스적 가치가 높은가로 엔티티로 쪼갤까 말까 하는 등등. 이게 현재 상황에서는 최소화가 맞는 것 같은데, 어느시점까지 이 정책들을 유지할 수 있는지 그런게 좀 감이 잘 안잡힌다.

Context & Decision

선택지와 결정

1. 레이어 간 책임 분리

  • 고려한 대안:
    • A: Service에 모든 로직 집중 (도메인 서비스 없이)
    • B: 단일 엔티티 귀속 규칙은 엔티티에, 도메인 간 조합은 Facade에 위임
  • 최종 결정: B — 엔티티가 상태 변경의 주체가 되고(검증, 재고 차감 등), Facade는 Service만 호출
  • 트레이드오프: Facade가 Service만 의존하는 규칙을 엄격히 지키면, 간단한 조회에도 Service 래핑이 필요해져 thin wrapper가 생길 수 있음. 그러나 혼용 시 매번 어디서 허용할지 결정하는 오버헤드가 더 크다고 판단하여 시스템적으로 통일

2. 도메인 간 참조 방식

  • 고려한 대안:
    • A: 객체 참조 + DB FK 제약 (@ManyToOne + @JoinColumn)
    • B: ID 참조 (private Long brandId)
    • C: 객체 참조 + FK 없음 (@JoinColumn(foreignKey = NO_CONSTRAINT))
  • 최종 결정: B (전체 ID 참조 통일)
    • 초기에는 Brand-Product에 C(객체 참조)를 적용했으나, product.getBrand().getName() 편의보다 일관성이 더 중요하다고 판단하여 ID 참조로 리팩토링
    • Brand 이름은 BrandService.getActiveNameMapByIds()로 Facade에서 조합
  • 트레이드오프: 매번 별도 조회 필요하지만, FK 잠금 전파에 의한 데드락 위험 제거, 도메인 간 결합도 최소화

3. 좋아요 수 집계 방식

  • 고려한 대안:
    • A: Product에 likeCount 캐시 필드 (Like 도메인에서 원자적 증감)
    • B: COUNT(*) 실시간 쿼리 (Batch COUNT + Facade 조합)
  • 최종 결정: B (COUNT 실시간 쿼리)
    • 초기에는 A(likeCount 캐시)로 구현했으나, Like → Product 단방향 결합이 생기고, 탈퇴/삭제 시 동기화 관리가 필요했음. 좋아요가 핵심 비즈니스가 아닌 상황에서 이 결합은 과도하다고 판단하여 B로 전환
  • 트레이드오프: 모든 상품 조회에 Like 테이블 COUNT 쿼리 추가. 그러나 도메인 완전 독립 유지

4. likes_desc 정렬 방식

  • 고려한 대안:
    • A: DB JOIN + ORDER BY (Like 테이블과 Product 테이블 JOIN)
    • B: Application 레벨 정렬 (전체 로딩 → 메모리 정렬 → 수동 페이지네이션)
  • 최종 결정: B (Application 레벨 정렬)
  • 트레이드오프: 전체 상품 로딩 필요하지만, Like-Product DB 레벨 결합을 피할 수 있음. 데이터 대량화 시 DB JOIN 또는 캐시 도입 검토

Design Overview

변경 범위

  • 영향 받는 모듈/도메인: commerce-api — User, Brand, Product, Like, Order (+ Cart 설계 완료, 구현 예정)
  • 신규 추가: 6개 도메인 전체 레이어 (Entity, Service, Facade, Repository 어댑터, Controller, DTO) + 테스트
  • 제거/대체: 전체 도메인 VO 제거 → Entity 원시값 필드로 전환, 객체 참조 → ID 참조 전환

아키텍처 구조

interfaces/    → Controller, ApiSpec, DTO (V1Dto)
application/   → Facade, Criteria, Result
domain/        → Model(Entity), Service, Repository(interface), ErrorCode
infrastructure/→ JpaRepository, RepositoryImpl

의존 방향: interfaces → application → domain ← infrastructure

주요 컴포넌트 책임

Facade (Cross-Domain 오케스트레이션):

  • BrandFacade: Brand CRUD, 목록 조회
  • ProductFacade: Product CRUD + Brand 이름 조합 + Like 수 enrichment + likes_desc 정렬
  • OrderFacade: 주문 생성 (재고 차감, 가격 검증, 스냅샷 생성) + 소유권 검증
  • LikeFacade: 좋아요 등록/취소 + Product 존재 검증
  • UserFacade: 회원가입 (암호화) + 내 정보 조회 + 비밀번호 변경

Domain Service (단일 도메인 규칙):

  • BrandService: 중복 이름 검증, 활성 브랜드 필터링, ID → 이름 맵 제공
  • ProductService: 재고 차감, 가격 검증, 브랜드별/전체 조회
  • OrderService: 주문 생성, 소유권 검증, 기간별 조회
  • ProductLikeService: 좋아요 등록/취소, 일괄 좋아요 수 집계
  • UserService: 회원가입, 비밀번호 변경, 로그인 ID 조회

Domain Model (비즈니스 규칙 캡슐화):

  • ProductModel: decreaseStock(), validateExpectedPrice(), isSoldOut() — 엔티티가 상태 변경의 주체
  • OrderModel: validateOwner() — 소유권 검증은 엔티티 책임
  • OrderItemModel: productName, brandName 스냅샷 필드 — 주문 시점 기록 보존
  • ProductLikeModel: userId + productId 유니크 제약 — DB 레벨 중복 방지

도메인 관계

graph TB
    User["User"]
    Brand["Brand"]
    Product["Product"]
    Like["Like"]
    Order["Order"]
    OrderItem["OrderItem"]

    Brand ---|"ID 참조"| Product
    User ---|"ID 참조"| Like
    User ---|"ID 참조"| Order
    Product ---|"ID 참조"| Like
    Product -->|"스냅샷"| OrderItem
    Order --- OrderItem
Loading
모든 도메인 간 참조는 ID 참조
DB FK 제약 사용하지 않음 — 무결성은 애플리케이션에서 보장
DB 유니크 제약은 사용 (테이블 내부 제약)
삭제 전략: Soft Delete (BaseEntity.deletedAt)

API 엔드포인트

도메인 사용자 API 관리자 API
User 회원가입, 내 정보 조회, 비밀번호 변경 -
Brand 상세 조회 생성, 수정, 삭제, 목록/상세 조회
Product 목록 조회 (정렬/필터링), 상세 조회 생성, 수정, 삭제, 목록/상세 조회
Like 좋아요 등록/취소, 내 찜 목록 조회 -
Order 주문 생성, 내 주문 목록/상세 조회 전체 주문 목록/상세 조회

Flow Diagram

주문 생성 (Cross-Domain: 재고 차감 + 가격 검증 + 스냅샷)

sequenceDiagram
    autonumber
    participant Client
    participant Controller
    participant OrderFacade
    participant ProductService
    participant BrandService
    participant OrderService

    Client->>Controller: POST /api/v1/orders
    Controller->>OrderFacade: createOrder(userId, criteria)
    OrderFacade->>ProductService: getAllByIds(productIds)
    ProductService-->>OrderFacade: List<ProductModel>

    loop 각 주문 항목
        Note over OrderFacade: product.validateExpectedPrice(expectedPrice)
        Note over OrderFacade: product.decreaseStock(quantity)
    end

    OrderFacade->>BrandService: getNameMapByIds(brandIds)
    BrandService-->>OrderFacade: Map<brandId, name>
    Note over OrderFacade: OrderItem 생성 (productName, brandName 스냅샷)
    OrderFacade->>OrderService: createOrder(command)
    OrderService-->>OrderFacade: OrderModel
    OrderFacade-->>Controller: OrderSummary
    Controller-->>Client: ApiResponse
Loading

상품 목록 조회 (Cross-Domain: Brand 이름 + Like 수 조합)

sequenceDiagram
    autonumber
    participant Client
    participant Controller
    participant ProductFacade
    participant ProductService
    participant BrandService
    participant LikeService

    Client->>Controller: GET /api/v1/products
    Controller->>ProductFacade: getProductsWithActiveBrand(pageable)
    ProductFacade->>ProductService: getAll(pageable)
    ProductService-->>ProductFacade: Page<ProductModel>
    ProductFacade->>BrandService: getActiveNameMapByIds(brandIds)
    BrandService-->>ProductFacade: Map<brandId, name>
    ProductFacade->>LikeService: countLikesByProductIds(productIds)
    LikeService-->>ProductFacade: Map<productId, count>
    Note over ProductFacade: 활성 브랜드 필터 + likeCount enrichment
    ProductFacade-->>Controller: Page<ProductResult>
    Controller-->>Client: ListResponse
Loading

구현 범위 요약

테스트

  • 전체 247개 테스트 통과
  • 단위 테스트: Domain Model (6개), Domain Service (Fake 기반 TFD, 8개), Facade (Mockito 기반 TFD, 4개)
  • E2E 테스트: Brand, Product, User, Like, Order (TestContainers — MySQL, Redis)
  • 아키텍처 테스트: ArchUnit (레이어 의존 방향, 네이밍 컨벤션, 도메인 순수성, 서비스 레이어 규칙)

주요 리팩토링 이력

리팩토링 배경
전체 도메인 VO 제거 → Entity 원시값 필드 @Embeddable JPA 마찰(null 전파, @AttributeOverride) 대비 이점이 적었음
Product → Brand 객체참조 → ID 참조 프로젝트 전체 일관성 + FK 잠금 전파 위험 제거
likeCount 캐시 → COUNT(*) 실시간 쿼리 좋아요가 핵심 비즈니스가 아닌 상황에서 Like → Product 결합은 과도
toggleLike → like/unlike 분리 토글은 의도가 불명확. 명시적 행위 분리가 API/도메인 모두 자연스러움
Facade private 메서드 → Service로 이동 Facade는 Service 조합만 담당, 자체 로직 금지 컨벤션 확립
Facade IN절 일괄 조회 원칙 N+1 방지. 목록 조회 시 연관 데이터는 ID 추출 → IN절 일괄 조회 → Map 조합

plan11plan and others added 30 commits February 9, 2026 18:11
- Password VO를 EncryptedPassword로 변경하여 암호화된 비밀번호임을 명확히 표현
- 서비스 계층에 SignupCommand, ChangePasswordCommand, UserInfo DTO 도입
- 컨트롤러에서 엔티티(UserModel) 직접 노출 제거
- Example 관련 코드 삭제

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- EncryptedPassword.of() 오버로드 제거 → 하나만 유지 (형식 검증 + 암호화)
- UserModel 생성자에서 rawPassword + encoder를 받아 birthDate 교차 검증 수행
- 생성/변경 두 경로 모두 UserModel이 검증 → 일관성 확보
- 패스워드 설계 결정 문서 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
plan11plan and others added 11 commits February 25, 2026 17:09
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Feb 27, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

plan11plan and others added 18 commits February 27, 2026 16:43
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…티 전파 보장

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant