Skip to content

[3주차] 도메인 주도 설계 구현 - 윤유탁#90

Open
yoon-yoo-tak wants to merge 11 commits intoLoopers-dev-lab:yoon-yoo-takfrom
yoon-yoo-tak:round3
Open

[3주차] 도메인 주도 설계 구현 - 윤유탁#90
yoon-yoo-tak wants to merge 11 commits intoLoopers-dev-lab:yoon-yoo-takfrom
yoon-yoo-tak:round3

Conversation

@yoon-yoo-tak
Copy link

@yoon-yoo-tak yoon-yoo-tak commented Feb 25, 2026

📌 Summary

  • 배경: Round 3 과제로 Product, Brand, Like, Cart, Order 도메인 구현이 요구됨. 기존 User 도메인은 CLAUDE.md 규칙과 불일치하는 부분이 있어 리팩터링 필요.
  • 목표: Layered Architecture + DIP 기반으로 5개 도메인의 풍부한 도메인 모델을 구현하고, 단위/통합/E2E 테스트로 정합성을 검증한다.
  • 결과: 6개 도메인(User, Brand, Product, Like, Cart, Order) 전 계층 구현 완료. ArchUnit으로 아키텍처 규칙을 자동 검증하며, 도메인 단위 테스트 > Integration > E2E 순으로 테스트 커버리지 확보.

🧭 Context & Decision

문제 정의

  • 현재 동작/제약: Round 2까지 User 도메인만 존재. 커머스 핵심 기능(상품, 주문 등)이 미구현 상태.
  • 문제(또는 리스크): 5개 도메인을 새로 구현하면서 DDD 원칙과 Layered Architecture를 일관되게 유지해야 함. 기존 User 코드도 규칙 위반 사항 존재.
  • 성공 기준(완료 정의): 모든 도메인의 CRUD 및 비즈니스 로직이 동작하고, 아키텍처 테스트를 포함한 전체 테스트가 통과한다.

선택지와 결정

1. Aggregate 내부 @OneToMany / @ManyToOne 사용

  • 고려한 대안:
    • A: 모든 연관관계에서 물리 매핑 금지 — Aggregate 내부도 ID만 보유
    • B: Aggregate 내부는 물리 연관, Aggregate 간은 논리 연관(ID만 보유)
  • 최종 결정: B
  • 트레이드오프: CLAUDE.md에 "물리적 매핑 금지"로 명시되어 있으나, DDD 관점에서 Aggregate 내부(Order↔OrderItem, Cart↔CartItem)는 동일 트랜잭션에서 라이프사이클을 함께하는 단위이므로 @OneToMany(cascade=ALL, orphanRemoval=true)
    오히려 Aggregate의 불변식 보호에 유리하다고 판단했다. Aggregate 간(Order→User/Product, Cart→Product 등)은 ID만 보유하는 논리 연관을 유지한다.

2. Application Layer에서 도메인 엔티티를 직접 리턴하는 이유

  • 고려한 대안:
    • A: Application 계층에 Info DTO를 두고, 엔티티→DTO 변환 후 리턴
    • B: Application 계층은 도메인 엔티티(Aggregate Root)를 그대로 리턴하고, Controller에서 Response DTO로 변환
  • 최종 결정: B (Command는 도메인 중심, Query는 읽기 최적화 모델 중심의 하이브리드 전략)
  • 트레이드오프: 현재 Commerce API는 쓰기 모델(도메인 엔티티/애그리거트)을 중심으로 애플리케이션 계층을 구성하며, 웹 어댑터(Controller)에서 응답 조합을 담당한다. 이는 의존 방향(외부 → 내부)을 유지하고 프레젠테이션 관심사가
    애플리케이션 계층으로 침투하는 것을 방지하기 위한 의도적 설계다.
  • 추후 개선 여지: 목록/검색/리포트처럼 다중 애그리거트 조합이 필요한 조회 유스케이스는 컨트롤러 복잡도가 증가할 수 있으므로, 해당 영역부터 조회 전용 모델(Query DTO/Projection)과 Query 서비스를 단계적으로 도입한다.

3. JPA 엔티티와 도메인 엔티티를 분리하지 않은 이유

  • 고려한 대안:
    • A: 분리 — Infrastructure에 *JpaEntity, Domain에 순수 도메인 객체를 두고 매핑 계층 추가
    • B: 통합 — JPA 엔티티가 곧 도메인 엔티티
  • 최종 결정: B
  • 근거:
    • JPA는 "자바 도메인 모델로 관계형 데이터베이스를 관리한다"는 기술적 목표로 설계되었으며, JPA 스펙은 엔티티를 *"경량 영속 도메인 오브젝트(lightweight persistent domain object)"*로 정의한다. 즉, JPA 엔티티는 태생적으로 도메인
      오브젝트이면서 영속 객체이도록 설계된 것이다.
    • JPA 어노테이션(@Entity, @Column 등)은 코드 동작을 변경하는 로직이 아니라 메타데이터 선언이다. JPA 의존성을 제거해도 도메인 로직 자체는 그대로 동작한다.
    • Spring Data Repository는 DDD의 Aggregate Root를 다루는 것을 전제로 설계되었으며, 저장소 기술 교체 시에도 일관된 프로그래밍 모델을 제공한다.
    • 현재 프로젝트에서 도메인 모델과 데이터 모델이 충분히 일치하며, JPA 매핑으로 도메인 의도를 자연스럽게 표현할 수 있다. 분리 시 발생하는 매핑 코드 증가, 유지보수 포인트 분산 등의 비용이 이점보다 크다고 판단했다.
  • 추후 개선 여지: 레거시 DB 마이그레이션이나 도메인 복잡도가 JPA 매핑 능력을 넘어서는 시점에 부분적 분리를 검토한다.

🏗️ Design Overview

변경 범위

  • 영향 받는 모듈/도메인: apps/commerce-api — User(리팩터링), Brand, Product, Like, Cart, Order
  • 신규 추가:
    • 도메인 계층: Brand, Product(Money/Stock VO), Like, Cart(CartItem), Order(OrderItem/OrderStatus/OrderPolicy), Quantity VO, PageResult
    • 애플리케이션 계층: 6개 도메인 ApplicationService + Command/Detail DTO
    • 인터페이스 계층: Customer/Admin Controller, ApiSpec, Request/Response DTO, Auth(인터셉터/리졸버)
    • 인프라 계층: 6개 도메인 JpaRepository + RepositoryImpl
    • 테스트: ArchUnit 아키텍처 테스트, 도메인 단위 테스트, DomainService 통합 테스트, E2E API 테스트
  • 제거/대체: Example 도메인(ExampleModel, ExampleService, ExampleController 등) 전체 제거

주요 컴포넌트 책임

도메인 계층

  • Brand: 브랜드 엔티티. rename() 으로 이름 변경
  • Product: 상품 엔티티. Money/Stock VO 활용. 재고 차감(deductStock), 좋아요 카운트 관리
  • Like: 유저-상품 좋아요 관계 엔티티. 물리 삭제 방식
  • Cart (Aggregate Root): 장바구니. CartItem 라이프사이클 통제 (addItem, removeItem, updateItemQuantity, clear)
  • Order (Aggregate Root): 주문. OrderItem 라이프사이클 통제. 주문 시점 상품 스냅샷 보존. cancel() 상태 전이
  • *DomainService: 상태 없는 순수 클래스. 조회/저장 + 도메인 규칙 검증 (중복 체크, 존재 확인 등)
  • OrderPolicy: 주문 내 중복 상품 검증 유틸리티

애플리케이션 계층

  • *ApplicationService: 유스케이스 오케스트레이터. @Transactional 경계 설정. 도메인 엔티티 리턴
  • CreateOrderCommand, RegisterProductCommand 등: 쓰기 유스케이스 입력 DTO

인터페이스 계층

  • *V1Controller / Admin*V1Controller: REST API 엔드포인트. Response DTO의 from() 팩토리로 엔티티→응답 변환
  • AuthUserArgumentResolver / AdminAuthInterceptor: 인증/인가 처리

인프라 계층

  • *RepositoryImpl: 도메인 Repository 인터페이스 구현. *JpaRepository 위임
  • BcryptPasswordEncryptor: PasswordEncryptor 인터페이스 구

@yoon-yoo-tak yoon-yoo-tak self-assigned this Feb 25, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 25, 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

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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.

@yoon-yoo-tak yoon-yoo-tak changed the title Round3 [3주차] 도메인 주도 설계 구현 - 윤유탁 Feb 25, 2026
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