DDD 핵심 개념부터 JPA Fetch 전략까지 한 번에 정리하기
오랜만에 '도메인 주도 개발 시작하기: DDD의 핵심개념 정리부터 구현까지' 읽고 독후감(?!) 작성함 , 내가 나중에 보고 기억할 수 있게
DDD의 핵심은 “도메인을 구조적으로 이해 가능하게 만들고, 일관성을 어디서 강하게 보장할지 명확히 정하는 것”이며, JPA의 지연 로딩과 Fetch 전략은 그 설계를 기술적으로 구현하는 수단이다.
이 글은 다음 흐름으로 정리한다.
- 도메인 정의
- 엔티티와 밸류 객체
- 엔티티 동일성의 의미
- 애그리거트의 등장 배경과 정의
- 일관성 경계의 의미
- JPA를 활용한 애그리거트 구현
- 지연 로딩 전략
- Fetch Join / EntityGraph
- JPQL / QueryDSL 예제
- 이벤트 기반 동작
- CQRS 구성
- 실무 점검 질문
1️⃣ 도메인 정의
DDD에서 가장 중요한 것은 기술이 아니라 도메인이다.
도메인은 “해결하려는 문제 영역”이며,
코드는 그 도메인을 반영해야 한다.
- CRUD 중심 사고 → 행위 중심 사고
- 테이블 중심 설계 → 개념 중심 설계
- 기능 구현 → 규칙 구현
2️⃣ 엔티티(Entity)와 밸류(Value Object)
엔티티
- 식별자(ID)를 가진다
- 상태가 변한다
- 동일성이 중요하다
밸류 객체
- 식별자가 없다
- 값 자체가 의미다
- 불변으로 설계하는 것이 이상적
| 구분 | 엔티티 | 밸류 |
|---|---|---|
| 식별자 | O | X |
| 동일성 기준 | ID | 값 |
| 변경 | 가능 | 불변 권장 |
3️⃣ 엔티티 동일성이 중요한 이유
엔티티는 “값”이 아니라 “식별자”로 동일성을 판단한다.
- 값이 달라도 ID가 같으면 같은 엔티티
- 값이 같아도 ID가 다르면 다른 엔티티
왜 중요한가?
- Dirty Checking 기반 변경 추적
- 낙관적 락 충돌 관리
- 생성 vs 수정 구분
- 컬렉션 중복 관리
엔티티는 “현재 값”이 아니라
시간을 따라 추적되는 존재다.
4️⃣ 애그리거트가 등장한 배경
복잡한 도메인 모델은
100개 테이블이 얽힌 ERD와 같다.
- 전체 구조 파악 어려움
- 수정 영향도 예측 불가
- 변경 회피 → 꼼수 증가
이를 해결하기 위해 등장한 개념이 애그리거트(Aggregate) 다.
5️⃣ 애그리거트의 정확한 정의
애그리거트는 두 가지 목적을 가진다.
1. 구조적 단위
도메인을 상위 수준에서 조망 가능하게 만드는 단위
2. 일관성 경계
하나의 트랜잭션에서 강하게 보장해야 하는 규칙 범위
즉,
애그리거트는 “하나의 트랜잭션에서 불변식을 반드시 보장해야 하는 최소 단위”다.
6️⃣ 일관성 경계의 의미
일관성 = 항상 참이어야 하는 규칙
예:
- 주문 합계는 주문 라인의 합과 같아야 한다
- 결제 완료된 주문은 취소 불가
기준 질문:
두 객체는 반드시 같은 트랜잭션에서 함께 변경되어야 하는가?
YES → 같은 애그리거트
NO → 다른 애그리거트 (ID 참조 + 이벤트)
7️⃣ JPA로 애그리거트 구현하기
1. 조회
- Repository는 애그리거트 단위 조회
- Lazy 기본
2. 상태 변경
- Dirty Checking 이해 필수
- 트랜잭션 범위 명확히
3. 페이징
- Pageable 사용
- 컬렉션 fetch join 주의
4. Subselect / JPQL
- 복잡 조회는 JPQL
- 객체 중심 쿼리
8️⃣ 지연 로딩(LAZY)의 필요성
기본 전략:
모든 연관은 LAZY로 시작한다.
왜?
- 조인 폭발 방지
- 불필요한 데이터 로드 방지
- 애그리거트 경계 유지
- 성능 안정성 확보
9️⃣ N+1 문제
for (Order o : orders) {
o.getMember().getName();
}
1번 + N번 쿼리 발생
해결 방법:
- Fetch Join
- EntityGraph
- DTO 프로젝션
- 2단계 조회
🔟 Fetch Join
JPQL 예제
select o
from Order o
join fetch o.member
left join fetch o.orderLines
where o.id = :id
단건 상세 조회에 적합.
주의:
- 컬렉션 fetch + 페이징 거의 금지
1️⃣1️⃣ QueryDSL Fetch Join
queryFactory
.selectFrom(order)
.join(order.member, member).fetchJoin()
.where(order.id.eq(id))
.fetchOne();
.fetchJoin()이 핵심.
1️⃣2️⃣ 목록 + 페이징 전략 (실무 패턴)
2단계 조회
1️⃣ ID만 페이징 조회
2️⃣ fetch join으로 재조회
이 방식이 가장 안전하다.
1️⃣3️⃣ EntityGraph
@EntityGraph(attributePaths = {"member"})
List<Order> findByStatus(OrderStatus status);
- 로딩 정책 분리
- JPQL 오염 없음
- 재사용성 높음
1️⃣4️⃣ Fetch Join vs EntityGraph
| 항목 | Fetch Join | EntityGraph |
|---|---|---|
| 제어력 | 강함 | 중간 |
| 재사용성 | 낮음 | 높음 |
| 추천 | 상세 조회 | 재사용 조회 |
1️⃣5️⃣ 이벤트 기반 동작
애그리거트 간 직접 참조 대신
도메인 이벤트 사용
장점:
- 결합도 감소
- 확장성 증가
- 비동기 처리 가능
비동기 설계 고려사항:
- 트랜잭션 보장 여부
- 재시도 전략
- 메시지 브로커 선택
- Outbox 패턴 적용 여부
1️⃣6️⃣ CQRS 구성
Command와 Query 분리
| Command | Query |
|---|---|
| 상태 변경 | 조회 |
| 도메인 중심 | 읽기 모델 중심 |
조회는 DTO / Projection / View 활용
도메인 모델 오염 방지
1️⃣7️⃣ OSIV와 LazyInitializationException
트랜잭션 밖에서 LAZY 접근 시 발생.
권장:
- 서비스 계층에서 DTO 변환
- 엔티티 직접 반환 금지
- OSIV는 신중히
전체 요약
- 도메인이 먼저다
- 엔티티는 ID로 동일성 유지
- 애그리거트는 일관성 경계
- 연관은 기본 LAZY
- 상세는 fetch join
- 목록은 DTO 또는 2단계 조회
- 이벤트로 애그리거트 간 연결
- CQRS로 조회/명령 분리
DDD 설계와 JPA Fetch 전략은
분리된 주제가 아니라 하나의 설계 문제다.
스스로 점검 질문
- 내 애그리거트 경계는 명확한가?
- 즉시 강제해야 할 불변식은 무엇인가?
- 모든 연관이 LAZY인가?
- N+1이 실제 발생하는지 로그로 확인했는가?
- 컬렉션 fetch + 페이징을 쓰고 있지 않은가?
- 엔티티를 그대로 API로 반환하고 있지 않은가?
- CQRS가 필요한 조회가 섞여 있지 않은가?
Top comments (0)