이번 포스팅에서는 DDD의 핵심 구성 요소들이 실제 코드 레벨(Spring + JPA)에서 어떻게 구현되고, 어떤 고민을 거쳐 적용되어야 하는지 정리해보았다.
DDD의 구성요소
Entity
- 고유 식별자(ID)가 존재 (값이 변해도 같은 존재이다)
- 동일성은 ID로 판단
- 생명주기를 가짐
- 상태가 변할 수 있음
// id가 같으면 같은 객체로 본다.
class Member {
private MemberId id;
private String name;
}
Value Object (VO)
- 식별자가 없음
- 값 자체가 동일성 기준
- 불변(immutable)
- 부작용이 없음
// amount + currency가 같으면 같은 객체
class Money {
private final int amount;
private final String currency;
}
Aggregate / Aggregate Root
Aggregate
하나의 일관성 경계(Consistency Boundary)
데이터 변경의 단위로 묶인 객체들의 집합.
- 여러 Entity와 VO를 하나로 묶은 것
- 내부 객체는 외부에서 직접 접근 불가
Aggregate Root
애그리거트의 대표 엔티티. 외부에서 접근 가능한 유일한 진입점
(예시)
Order (Root)
├─ OrderLine
└─ ShippingInfo
// 외부에서 Aggregate Root인 order를 통해 외부 진입 시..
order.addOrderLine(...)
// 불가 (내부 메서드 접근 금지)
orderLine.setQuantity(...)
왜 필요한가?
- 일관성 보호
- 트랜잭션 경계 정의
Aggregate Root는 도메인 모델의 일관성을 보호하기 위해 필요하다.
외부에서 내부 객체를 직접 수정하면 규칙이 깨질 수 있으므로,
데이터 무결성 보장을 위해 외부에서 Root를 통해서만 내부 객체를 변경할 수 있도록 하여
비즈니스 규칙과 불변 조건을 강제해야 한다.
트랜잭션의 범위
DDD의 원칙상 '하나의 트랜잭션은 하나의 애그리거트만' 수정하는 것이 권장된다. 그래야 시스템의 확장성과 성능을 확보할 수 있기 때문이다. 만약 여러 애그리거트를 고쳐야 한다면 도메인 이벤트를 활용한 최종적 일관성을 고려
Repository
Aggregate 저장소. Repository는 Aggregate 단위의 영속성을 추상화하는 역할을 한다.
- Domain에 인터페이스 위치
- Repository는 Aggregate 단위
- Infrastructure에서 구현
interface OrderRepository {
void save(Order order);
Optional<Order> findById(OrderId id);
}
Domain Service vs Application Service
Domain Service는 도메인 규칙을 수행하고,
Application Service는 Use Case를 실행하며
트랜잭션을 관리한다.
Domain Service
- 특정 Entity에 속하기 어려운 비즈니스 로직 수행
- 비즈니스 규칙
- 순수 도메인 개념
class DiscountPolicy {
Money calculateDiscount(Order order);
}
Application Service
- Use Case 실행
- 트랜잭션 관리
- 여러 Aggregate 조합
@Transactional
public void placeOrder(...) {
Order order = orderFactory.create(...);
orderRepository.save(order);
}
차이
| 구분 | Domain Service | Application Service |
| 책임 | 도메인 규칙 | 흐름 제어 |
| 위치 | Domain | Application |
| 트랜잭션 | 없음 | 있음 |
Invariant (불변 조건)
도메인 모델이 항상 만족해야 하는 비즈니스 규칙(불변 조건)
Invariant는 도메인에서 항상 유지되어야 하는 규칙이며,
Aggregate Root 내부에서 이를 강제하여 일관성을 보장한다.
예시는 다음과 같다.
- 주문 총 금액은 0 이상
- 결제 완료된 주문은 취소 불가
- 포인트는 음수가 될 수 없음
이걸 Aggregate 내부에서 보장해야 한다.
트랜잭션 경계와 Aggregate 설계
DDD에서 중요한 원칙:
하나의 트랜잭션은 하나의 Aggregate만 수정하는 것이 이상적이다.
이유
- 분산 트랜잭션 방지
- 동시성 문제 최소화
- 성능 개선
여러 Aggregate 수정이 필요하면, Domain Event로 풀어야 한다.
JPA Entity = Domain Entity 인가?
그렇지 않다
꼭 그렇지는 않다. 그러나 이론적으로는 Domain Entity와 Persistence Entity를 분리하는 것이 이상적이지만,
실무에서는 복잡도와 생산성을 고려해 JPA Entity를 Domain Entity로 사용하는 경우가 많다. 대신, 비즈니스 규칙은 반드시 Entity 내부에 두도록 설계한다.
왜 문제인가?
JPA Entity를 Domain Entity와 동일시 한다면 아래와 같은 문제가 발생할 수 있기 때문이고, 이는 DDD의 순수성을 깨트리기 때문이다.
- JPA Lazy Loading
- Setter 남발
- 프록시 객체
- 영속성 로직 침투
반응형
'TIL' 카테고리의 다른 글
| [TIL] JOIN 전략 (0) | 2026.03.10 |
|---|---|
| [TIL] 레이어드 아키텍처 & 책임 분리 (0) | 2026.03.04 |
| [TIL] 전략적 DDD와 전술적 DDD (0) | 2026.02.23 |
| [TIL] DDD 개념 & 설계 사고방식 (0) | 2026.02.20 |
| [TIL] 블로킹/논블로킹과 동기/비동기 (0) | 2026.02.10 |