Skip to content

[3주차] 도메인 주도 설계 구현 - 강담희#120

Open
dame2 wants to merge 24 commits intoLoopers-dev-lab:dame2from
dame2:volume-3
Open

[3주차] 도메인 주도 설계 구현 - 강담희#120
dame2 wants to merge 24 commits intoLoopers-dev-lab:dame2from
dame2:volume-3

Conversation

@dame2
Copy link

@dame2 dame2 commented Feb 27, 2026

📌 Summary

  • 배경: 기존 도메인 레이어가 JPA 어노테이션에 직접 의존하여 순수 DDD 원칙을 위반하고 있었으며, 테스트 시 Spring Context가 필요한 구조였음
  • 목표: Domain Layer를 순수 Java로 구현하여 프레임워크 독립성 확보 및 Fake Repository를 활용한 빠른 단위 테스트 환경 구축
  • 결과: Brand, Product, Like, Order 도메인을 순수 DDD 패턴으로 구현하고, Value Object, Domain Service, Application Service, REST API까지 전 계층 구현 완료

🧭 Context & Decision

문제 정의

  • 현재 동작/제약: Domain Entity가 @Entity, @Table 등 JPA 어노테이션을 직접 사용하여 Infrastructure 계층에 결합됨
  • 문제(또는 리스크):
    • 도메인 로직 테스트 시 DB/Spring Context 필요
    • 도메인 객체가 영속성 관심사에 오염됨
    • 계층 간 의존성 역전 원칙(DIP) 위반
  • 성공 기준: Domain Layer에 jakarta.persistence.*, org.springframework.* import 금지, Fake Repository로 단위 테스트 가능

선택지와 결정

1) 도메인 모델을 어떻게 영속화할 것인가 (Domain 순수성)

  • 고려한 대안
    A: Domain Entity에 JPA 어노테이션 직접 사용

    • Domain 객체가 @entity, @table 등으로 영속성 모델을 겸함
    • 빠르게 구현 가능하지만 Domain이 Infrastructure에 결합

    B: Domain Entity와 JPA Entity 분리 + Mapper(현재 선택한 방향)

    • Domain은 순수 Java, Infrastructure에 JPA Entity + Mapper + Impl Repository
    • DIP를 지키면서 테스트 격리(Fake Repo)가 쉬움

    C: Domain Entity 분리 + “도메인 친화” ORM/모듈(예: Spring Data/JPA 스펙 노출 최소화)

    • 완전 분리 대신, Repository 인터페이스/쿼리 타입만 부분적으로 활용
      -“순수성”은 일부 희생하지만 구현/운영 난이도는 낮출 수 있음 (다만 성공기준의 import 금지와 충돌 가능)
  • 최종 결정

    • B안 채택: Domain ↔ JPA Entity 분리 + Mapper 패턴
    • 성공 기준(도메인 패키지에 jakarta., org.springframework. import 금지)과 Fake Repo 기반 단위 테스트 목표에 가장 부합
  • 트레이드 오프

    • 코드량 증가: Mapper/Impl/Entity가 추가되어 파일 수와 변경 범위가 커짐
    • 동기화 비용: Domain과 JPA Entity 필드/규칙 불일치 위험(리팩터링 시 누락)
    • 성능/쿼리 튜닝 난이도: 매핑 계층이 늘어 최적화 포인트가 분산될 수 있음

2) “여러 Aggregate 협력”은 어디에서 조정할 것인가 (Application vs Domain Service)

  • 고려한 대안
    A: 모든 규칙을 각 Aggregate(Entity) 내부에 넣기

    • 장점: 도메인 모델이 풍부해짐
    • 단점: Repository 조회가 필요한 규칙(좋아요 중복 검사 등)을 엔티티가 직접 처리하기 어려움

    B: 협력/트랜잭션은 Application Service, “정책(규칙) + Repository 필요”는 Domain Service

    • 주문: Product + Order를 Application Service에서 오케스트레이션(트랜잭션 경계)
    • 좋아요: 중복 방지/멱등성은 Domain Service로 캡슐화(Repository 조회 필요)

    C: UseCase(인터랙터) 패턴으로 더 세분화

    • PlaceOrderUseCase, LikeProductUseCase처럼 유스케이스 단위 클래스로 분리
    • 장점: 변경 영향 최소화, 테스트 더 명확
    • 단점: 클래스 수 증가(과제 범위에 비해 과도할 수 있음)
  • 최종결정

    • B안 채택
    • 다중 Aggregate 조합/트랜잭션은 Application Service에서, Repository 기반 정책은 Domain Service로 분리
  • 트레이드오프

    • 레이어가 늘어 복잡도 증가: “왜 서비스가 2종류인지” 팀 합의/규칙 필요
    • 분리 기준이 흐려질 위험: Domain Service가 점점 “작은 Application Service”처럼 비대해질 수 있음

3) 재고 동시성(오버셀링)을 어떤 전략으로 막을 것인가

  • 고려한 대안
    A: 비관적 락(Pessimistic Lock)

    • findByIdWithLock()로 재고 차감 구간을 단일 트랜잭션으로 보호

    B: 낙관적 락(Optimistic Lock) + 재시도

    • version 기반으로 충돌 시 재시도
    • 장점: 락 경합 줄어듦
    • 단점: 재시도 정책/에러 처리/테스트가 복잡

    C: 원자적 업데이트(Atomic Update)

    • update product set stock = stock - ? where id=? and stock >= ? 같은 형태
    • 장점: DB round-trip 감소, 경합에 강함
    • 단점: 도메인 로직이 “쿼리”에 흡수되어 모델 표현력이 떨어질 수 있음(VO/불변식 강제 위치가 흔들림)
  • 최종결정

    • A안 채택(비관적 락)
    • “재고 차감 + 주문 생성”을 강한 일관성으로 묶고 오버셀링을 확실히 방지하는 방향
  • 트레이드오프

    • 성능/확장성 비용: 동시 주문이 많아지면 락 경합/대기 증가
    • 장애 전파 범위: 락 타임아웃/DB 병목이 주문 처리 전체에 영향
    • Fake Repo 한계: Fake에서는 락이 생략되므로 “진짜 동시성” 검증은 별도 테스트 필요(TODO로 이미 인지)

🏗️ Design Overview

변경 범위

  • 영향 받는 모듈/도메인: commerce-api (Brand, Product, Like, Order)
  • 신규 추가 (82 files, +5883 lines):
    • domain/common/: Money, Quantity Value Objects
    • domain/brand/: Brand Entity, Repository, DomainService
    • domain/product/: Product Entity, Stock VO, Repository, DomainService
    • domain/like/: Like Entity, LikeId VO, Repository, DomainService
    • domain/order/: Order Aggregate Root, OrderItem, Repository
    • infrastructure/persistence/jpa/*/: JPA Entities, Mappers, Repository 구현체
    • application/*/: Application Services, Result DTOs
    • interfaces/api/*/: REST API Controllers, DTOs
    • test/fake/: Fake Repositories
    • test/domain/*/: 도메인 단위 테스트
  • 제거/대체: 없음 (신규 구현)

주요 컴포넌트 책임

  • Money, Stock: 불변 Value Object, 생성 시점 검증, 연산 시 새 인스턴스 반환
  • Brand, Product, Like, Order: 순수 Java 도메인 엔티티, 정적 팩토리 메서드 (create/reconstitute)
  • *DomainService: 단일 BC 내 비즈니스 규칙 캡슐화 (중복 좋아요 방지, 멱등 취소 등)
  • *ApplicationService: 트랜잭션 경계, 여러 BC 조합, 유스케이스 오케스트레이션
  • *Mapper: Domain ↔ JPA Entity 변환
  • Fake*Repository: 테스트용 Map 기반 in-memory 구현

🔁 Flow Diagram

주문 생성 Flow

sequenceDiagram
  participant Client
  participant OrderApplicationService
  participant ProductRepository
  participant Product
  participant OrderRepository
  participant Order

  Client->>OrderApplicationService: placeOrder(userId, items)

  loop 각 주문 항목
    OrderApplicationService->>ProductRepository: findByIdWithLock(productId)
    ProductRepository-->>OrderApplicationService: Product
    OrderApplicationService->>Product: decreaseStock(quantity)

    alt 재고 부족
      Product-->>OrderApplicationService: INSUFFICIENT_STOCK 예외
      OrderApplicationService-->>Client: 에러 응답
    else 재고 충분
      Product-->>OrderApplicationService: 성공
      OrderApplicationService->>ProductRepository: save(product)
    end
  end

  OrderApplicationService->>Order: create(userId, orderItems)
  Order-->>OrderApplicationService: Order (총액 자동 계산)
  OrderApplicationService->>OrderRepository: save(order)
  OrderRepository-->>OrderApplicationService: saved Order
  OrderApplicationService-->>Client: OrderResult
Loading

좋아요 등록 Flow

sequenceDiagram
  participant Client
  participant LikeApplicationService
  participant ProductRepository
  participant LikeDomainService
  participant LikeRepository

  Client->>LikeApplicationService: like(userId, productId)
  LikeApplicationService->>ProductRepository: findByIdActive(productId)

  alt 상품 미존재
    ProductRepository-->>LikeApplicationService: empty
    LikeApplicationService-->>Client: NOT_FOUND 예외
  else 상품 존재
    ProductRepository-->>LikeApplicationService: Product
    LikeApplicationService->>LikeDomainService: like(userId, productId)
    LikeDomainService->>LikeRepository: exists(userId, productId)

    alt 이미 좋아요함
      LikeRepository-->>LikeDomainService: true
      LikeDomainService-->>LikeApplicationService: CONFLICT 예외
      LikeApplicationService-->>Client: 에러 응답
    else 처음 좋아요
      LikeRepository-->>LikeDomainService: false
      LikeDomainService->>LikeRepository: save(Like.create())
      LikeDomainService-->>LikeApplicationService: Like
      LikeApplicationService-->>Client: LikeResult
    end
  end
Loading

상품 CRUD Flow

sequenceDiagram
  participant Client
  participant ProductAdminV1Controller
  participant ProductService
  participant ProductDomainService
  participant ProductValidator
  participant BrandRepository
  participant ProductRepository

  Client->>ProductAdminV1Controller: POST /api/admin/v1/products
  ProductAdminV1Controller->>ProductService: create(ProductInfo)
  ProductService->>ProductDomainService: create(info)
  ProductDomainService->>ProductValidator: validateBrandExists(brandId)
  ProductValidator->>BrandRepository: findByIdActive(brandId)

  alt 브랜드 미존재
    BrandRepository-->>ProductValidator: empty
    ProductValidator-->>ProductDomainService: BRAND_NOT_FOUND 예외
    ProductDomainService-->>ProductService: 예외 전파
    ProductService-->>ProductAdminV1Controller: 예외 전파
    ProductAdminV1Controller-->>Client: 에러 응답
  else 브랜드 존재
    BrandRepository-->>ProductValidator: Brand
    ProductDomainService->>ProductRepository: save(Product.create())
    ProductRepository-->>ProductDomainService: saved Product
    ProductDomainService-->>ProductService: Product
    ProductService-->>ProductAdminV1Controller: ProductResult
    ProductAdminV1Controller-->>Client: 201 Created
  end
Loading

hanyoung-kurly and others added 24 commits February 2, 2026 01:26
- CLAUDE.md 추가 (프로젝트 컨텍스트 및 개발 규칙)
- spring-security-crypto 의존성 추가
- ErrorType에 UNAUTHORIZED, USER_NOT_FOUND, PASSWORD_MISMATCH 추가
- MySqlTestContainersConfig에 MYSQL_ROOT_PASSWORD 환경변수 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- User 엔티티 (필드 검증, BCrypt 암호화, 이름 마스킹)
- UserRepository 인터페이스
- UserService (회원가입, 조회, 인증, 비밀번호 변경)
- UserTest 단위 테스트 47건

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- UserJpaRepository (Spring Data JPA)
- UserRepositoryImpl (Repository 구현체)
- UserServiceIntegrationTest 통합 테스트 9건

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- UserFacade, UserInfo (Application 계층)
- AuthenticatedUser, AuthenticatedUserArgumentResolver (헤더 인증)
- WebMvcConfig (ArgumentResolver 등록)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- UserV1Controller (POST /users, GET /users/me, PATCH /users/me/password)
- UserV1Dto (요청/응답 DTO)
- UserV1ApiSpec (OpenAPI 스펙)
- UserV1ApiE2ETest E2E 테스트 12건
- user-v1.http (IntelliJ HTTP Client)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- .claude/commands/create-pr.md (PR 템플릿 기반 자동 생성 스킬)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- 01-requirements.md: 도메인별 필드/비즈니스 규칙, 유저 시나리오
- 02-sequence-diagrams.md: 주문/좋아요/브랜드 삭제 시퀀스 다이어그램
- 03-class-diagram.md: 계층별 클래스 구조 다이어그램
- 04-erd.md: 테이블 스키마, 인덱스, FK 정책

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
feat: User 도메인 구현 (회원가입, 내 정보 조회, 비밀번호 변경)
- Brand/Product/ProductLike/Order/OrderItem 도메인 필드 정의
- 비즈니스 규칙 (BR-*) 및 검증 규칙 정의
- 유저 시나리오 9개 (US-001~009), 어드민 시나리오 7개 (AS-001~007)
- API 명세 및 에러 타입 정의

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- 주문 생성 시퀀스 (정상/재고 부족 플로우)
- 좋아요 등록 시퀀스 (토글 방식: 신규/취소)
- 브랜드 삭제 시퀀스 (Cascade 삭제)
- 상품 목록 조회 시퀀스 (좋아요 수 포함)
- 어드민 인증 플로우 (Interceptor + ArgumentResolver)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- 전체 계층 구조 개요 (Layered Architecture)
- Brand/Product/ProductLike/Order 도메인 클래스
- 인증 관련 클래스 (AdminAuthInterceptor, AdminUserArgumentResolver)
- 공통 클래스 (BaseEntity, ApiResponse, CoreException, ErrorType)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- 6개 테이블 스키마 (users, brands, products, product_likes, orders, order_items)
- 인덱스 설계 및 FK 삭제 정책
- 쿼리 최적화 가이드 (좋아요순 정렬, 비관적 락)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- requirements-analysis 스킬 정의
- 요구사항 분석 워크플로우 가이드라인

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Money: 금액 VO, 0 이상 검증, add/multiply 연산
- Stock: 재고 VO, 차감/증가 시 불변식 검증
- Quantity: 수량 VO, 1 이상 검증
- ProductSort: 상품 정렬 Enum (LATEST, PRICE_ASC, LIKES_DESC)
- 각 VO에 대한 단위 테스트 포함

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Domain Layer (순수 Java):
- Brand: 브랜드 엔티티, create/reconstitute 정적 팩토리
- BrandRepository: Repository 인터페이스
- BrandDomainService: 중복 이름 검증, CRUD 정책
- BrandValidator: 브랜드 존재 검증

Infrastructure Layer:
- BrandJpaEntity: JPA 엔티티 (@entity)
- BrandMapper: Domain ↔ JPA 변환
- BrandRepositoryImpl: Repository Adapter

Test:
- FakeBrandRepository: Map 기반 in-memory 구현
- BrandTest, BrandInfoTest: 도메인 단위 테스트

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Domain Layer (순수 Java):
- Product: 상품 엔티티, Money/Stock VO 사용
- ProductRepository: Repository 인터페이스 (비관적 락 포함)
- ProductDomainService: 재고 차감, CRUD 정책
- ProductValidator: 브랜드 존재 검증

Infrastructure Layer:
- ProductJpaEntity: JPA 엔티티
- ProductMapper: Domain ↔ JPA 변환
- ProductRepositoryImpl: Repository Adapter

Test:
- FakeProductRepository: Map 기반 in-memory 구현
- ProductTest: 재고 차감, soft delete 테스트

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Domain Layer (순수 Java):
- Like: 좋아요 엔티티 (userId + productId)
- LikeId: 복합키 VO
- LikeRepository: Repository 인터페이스
- LikeDomainService: 중복 좋아요 방지, 멱등 취소 정책

Infrastructure Layer:
- LikeJpaEntity: JPA 엔티티 (unique constraint)
- LikeMapper: Domain ↔ JPA 변환
- LikeRepositoryImpl: Repository Adapter

Test:
- FakeLikeRepository: Map 기반 in-memory 구현
- LikeTest, LikeDomainServiceTest: 도메인 단위 테스트

정책:
- 중복 좋아요 시 CONFLICT 예외
- 좋아요 취소는 멱등 (없어도 예외 없음)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Domain Layer (순수 Java):
- Order: 주문 Aggregate Root, 총액 자동 계산
- OrderItem: 주문 항목, 가격 스냅샷 보관
- OrderStatus: 주문 상태 Enum
- OrderRepository: Repository 인터페이스

Infrastructure Layer:
- OrderJpaEntity: JPA 엔티티 (CascadeType.ALL)
- OrderItemJpaEntity: JPA 엔티티
- OrderMapper: Domain ↔ JPA 변환
- OrderRepositoryImpl: Repository Adapter

Test:
- FakeOrderRepository: Map 기반 in-memory 구현
- OrderTest, OrderItemTest: 도메인 단위 테스트

불변식:
- 주문 항목은 1개 이상
- 총액은 OrderItem 합산으로 계산
- OrderItem은 불변 리스트로 보호

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Brand:
- BrandService: 브랜드 CRUD 유스케이스
- BrandResult: 응답 DTO

Product:
- ProductService: 상품 CRUD, 목록 조회
- ProductResult: 응답 DTO

Like:
- LikeApplicationService: 상품 검증 + 좋아요/취소
- LikeResult: 응답 DTO

Order:
- OrderApplicationService: 재고 차감 + 주문 생성
- OrderResult, OrderItemResult: 응답 DTO
- OrderItemRequest: 요청 DTO

Test:
- BrandServiceIntegrationTest: 통합 테스트
- LikeApplicationServiceTest: Fake 기반 단위 테스트
- OrderApplicationServiceTest: Fake 기반 단위 테스트

트랜잭션:
- 주문 시 비관적 락으로 재고 차감
- @transactional 경계 관리

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Brand API:
- BrandV1Controller: 브랜드 목록/상세 조회
- BrandAdminV1Controller: 브랜드 CRUD (관리자)
- BrandV1Dto: 요청/응답 DTO
- BrandV1ApiSpec, BrandAdminV1ApiSpec: OpenAPI 문서화

Product API:
- ProductV1Controller: 상품 목록/상세 조회
- ProductAdminV1Controller: 상품 CRUD (관리자)
- ProductV1Dto: 요청/응답 DTO
- ProductV1ApiSpec, ProductAdminV1ApiSpec: OpenAPI 문서화

HTTP 테스트 파일:
- brand-api.http, brand-admin-api.http
- product-api.http, product-admin-api.http

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- domain-implements-task.md: 순수 DDD 구현 계획서
  - Domain Layer 순수 Java 원칙
  - 애그리게잇 설계 (Order, Product, Brand, Like)
  - Value Object 설계 (Money, Stock, Quantity)
  - Repository Interface/Impl 분리
  - Fake Repository 테스트 전략

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Brand:
- BRAND_NOT_FOUND: 브랜드 미존재
- BRAND_ALREADY_EXISTS: 브랜드명 중복
- BRAND_DELETED: 삭제된 브랜드

Product:
- PRODUCT_NOT_FOUND: 상품 미존재
- PRODUCT_DELETED: 삭제된 상품
- INSUFFICIENT_STOCK: 재고 부족

Order:
- ORDER_NOT_FOUND: 주문 미존재
- ORDER_ACCESS_DENIED: 주문 접근 권한 없음

Admin:
- ADMIN_UNAUTHORIZED: 관리자 권한 필요

Co-Authored-By: Claude Opus 4.5 <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

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.

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.

2 participants