이번에 처음으로 DDD(Domain-Driven Design) 오프라인 강의를 듣게 되었다. 평소 객체지향 설계에 관심이 많았지만, DDD는 늘 어렵고 추상적으로 느껴졌던 주제였다. '애그리게이트', '바운디드 컨텍스트' 같은 용어들은 들어본 적은 있지만 정확히 무엇을 의미하는지, 왜 필요한지 제대로 이해하지 못했었다.
이번 강의를 통해 DDD가 단순히 기술적인 패턴이 아니라, 복잡한 문제를 해결하기 위한 사고방식이자 철학이라는 것을 깨달았다. 강의 내용을 정리하면서 DDD의 핵심 개념들이 어떻게 연결되는지 이해할 수 있었고, 이를 기록으로 남기고자 한다.
DDD의 본질: 소프트웨어는 도메인 문제를 해결하기 위해 존재한다
강의는 에릭 에반스의 책 "도메인 주도 설계"의 핵심 메시지로 시작했다.
"소프트웨어의 본질은 도메인에 관련된 문제를 해결하는 능력에 있다."
DDD는 복잡한 도메인을 다뤄야 하는 소프트웨어 프로젝트에 박차를 가하는 사고방식이자 우선순위의 모음이다. 단순히 코드를 어떻게 작성할지에 대한 기술적 가이드가 아니라, 어떤 관점으로 문제를 바라보고 해결해 나갈 것인지에 대한 철학적 접근이었다.
Part 1: 동작하는 도메인 모델 만들기
도메인 모델이란?
**도메인(Domain)**은 사용자가 프로그램을 사용하는 주제 영역이다. 예를 들어 배달 앱이라면 '메뉴', '주문', '배달' 같은 것들이 도메인이 된다.
**모델(Model)**은 대상의 단순화다. 당면한 문제를 해결하는 것과 관련된 측면을 추상화하고, 중요하지 않은 세부사항은 생략한다.
따라서 도메인 모델은:
- 사용자가 프로그램을 사용하는 주제 영역 안에서
- 당면한 문제를 해결하는 것과 관련된 측면을 추상화하고
- 중요하지 않은 세부사항은 생략한 것
동작하는 도메인 모델
DDD의 핵심은 도메인 모델을 만드는 것에서 그치지 않고, 이를 동작하게 만드는 것이다.
도메인 모델 → 설계 → 코드
이 세 가지는 서로 분리된 것이 아니라 하나다. 코드가 변경되면 설계와 도메인 모델도 함께 변경되고, 도메인 모델이 변경되면 코드도 변경된다. 이것이 바로 **모델 주도 설계(Model-Driven Design)**다.
개발자의 관점에서 도메인 모델 = 코드
이것이 DDD의 문맥이다.
도메인 모델은 그림으로만 존재하는 것이 아니라, 실제로 동작하는 코드로 구현되어야 한다.
유비쿼터스 언어(Ubiquitous Language)
도메인 전문가와 개발자 사이의 커뮤니케이션 수단이 바로 유비쿼터스 언어다.
- 도메인 모델의 용어를 그대로 코드에 사용
- 모든 팀 구성원이 동일한 언어 사용
- 코드, 대화, 문서 모두에서 일관된 언어 사용
예를 들어 도메인 모델에서 "Customer가 Product를 order한다"라고 정의했다면, 코드에서도 customer.order(product)와 같이 동일한 용어를 사용한다.
지식 탐구(Knowledge Crunching)
중요한 것은 처음부터 완벽한 도메인 모델을 만들 수 없다는 점이다.
설계의 전 단계에 걸쳐 개발자와 도메인 전문가 사이의 활발한 논의를 거쳐 유용한 도메인 모델을 도출해야 한다. 그리고 이는 반복적인 과정이다.
Part 2: 모델 주도 설계의 빌딩 블록
빌딩 블록의 목적
빌딩 블록은 도메인의 개념을 코드로 옮기기 위한 직관적인 가이드를 제공한다. 궁극적인 목표는 복잡성을 낮추는 것이다.
빌딩 블록은 크게 두 가지로 나뉜다:
1. 도메인을 표현하기 위한 빌딩 블록
- Entity (엔티티)
- Value Object (값 객체)
- Service (서비스)
- Module (모듈)
- Association (연관관계)
2. 생명주기를 관리하기 위한 빌딩 블록
- Aggregate (애그리게이트)
- Repository (리포지토리)
- Factory (팩토리)
Entity vs Value Object
처음 DDD를 접하면서 가장 혼란스러웠던 부분이 엔티티와 값 객체의 차이였다.
Entity (엔티티)
- **연속성(Continuity)**과 **식별성(Identity)**으로 정의
- 상태가 변하더라도 동일한 객체로 추적 가능
- 식별자(ID)를 통해 구분
- 예: Shop, Menu, OptionGroup
Value Object (값 객체)
- 특정 속성으로만 정의
- 값이 같으면 동일한 객체
- 불변(Immutable)해야 함
- 예: Money, TimePeriod, Option
강의에서 강조한 핵심은:
"DDD는 값 객체를 최대한 많이 만든다"
값 객체가 많을수록 좋은 이유:
- 불변 객체이므로 상태 변경에 대한 복잡도 감소
- 가변 객체보다 안전하게 공유 가능
- 엔티티의 복잡성을 줄일 수 있음
// 값 객체 예시
public class Money extends ValueObject<Money> {
private final BigDecimal amount;
public Money plus(Money other) {
return new Money(this.amount.add(other.amount)); // 새 인스턴스 반환
}
}
"이거 엔티티인데 값 객체로 구현해도 되지 않을까?" 라는 질문을 계속 던져야 한다.
Aggregate (애그리게이트)
애그리게이트는 DDD를 이해하는 데 가장 중요한 개념 중 하나다.
애그리게이트란?
- 불변식(Invariant)을 지키기 위한 객체 그룹
- 트랜잭션의 단위
- 하나의 애그리게이트 루트를 통해서만 내부 접근 가능
**불변식(Invariant)**은 언제나 일관되게 유지되어야 하는 비즈니스 규칙이다.
예를 들어:
- 판매 중인 Menu에는 OptionGroup이 한 개 이상 존재해야 한다
- 판매 중인 Menu에는 필수 OptionGroup이 1개 이상 3개 이하로 존재해야 한다
- Menu 안에서 OptionGroup의 이름은 중복되지 말아야 한다
이런 불변식들을 지키기 위해 Menu, OptionGroup, Option을 하나의 애그리게이트로 묶는다.
애그리게이트 설계 원칙:
- 애그리게이트는 작아야 한다
- 트랜잭션 당 하나의 애그리게이트
- 너무 크면 동시성 문제와 성능 저하
- 불변식 기준으로 경계 결정
- 객체 구조가 아닌 불변식과 도메인 룰로 애그리게이트 결정
- 회사마다, 도메인마다 불변식이 다르므로 애그리게이트도 다름
- 애그리게이트 루트를 통해서만 접근
- 외부에서는 루트만 접근 가능
- 루트가 전체 불변식 보장 책임
- ID를 통한 참조
- 애그리게이트 외부는 ID로 참조
- 애그리게이트 내부는 객체 참조
- 결과적 일관성(Eventual Consistency)
- 모든 것을 하나의 트랜잭션에 넣으려 하지 말 것
- 도메인 이벤트를 통해 결과적 일관성 유지
Repository (리포지토리)
리포지토리는 애그리게이트의 생명주기를 관리한다.
public interface MenuRepository extends Repository<Menu, MenuId> {
List<Menu> findOpenMenusIn(ShopId shopId);
}
핵심 원칙:
- 애그리게이트 루트 단위로만 Repository 생성
- 메모리상의 컬렉션처럼 느껴지도록 설계(하지만 실무에서는 하위의 영속성 메커니즘 기반으로 설계하는 경향이 강함 예: Spring Data의 Repository)
- 영속성 기술에 대한 캡슐화
Factory (팩토리)
복잡한 객체 생성을 캡슐화한다.
예를 들어 Cart에서 Order를 생성할 때:
public class Cart extends AggregateRoot<Cart, CartId> {
public Order placeOrder() {
return new Order(userId, shopId, toOrderLineItems());
}
}
Cart가 Order의 Factory 역할을 할 수 있다. 하지만 의존성이 문제가 된다면 독립적인 Factory를 만들 수도 있다.
Domain Service (도메인 서비스)
어떤 객체에도 속하지 않는 도메인 로직은 Domain Service로 추출한다.
public class PromotionDomainService {
public void offer(Cart cart, Promotion promotion, PromotionLimit limit) {
// 할인 적용 로직
if (limit.isExceeded()) {
throw new IllegalStateException("할인 한도 초과");
}
cart.apply(promotion);
}
}
주의할 점:
- 무분별한 서비스 사용은 도메인 로직 누수로 이어짐
- 가능하면 엔티티나 값 객체에 로직 배치
- 정말 어디에도 속하지 않을 때만 서비스 사용
Part 3: 애자일과 DDD
강의에서 인상 깊었던 부분은 DDD와 애자일의 관계였다.
애자일의 본질
많은 사람들이 애자일을 "빠르게 개발하는 것"으로 오해한다. 하지만 애자일의 핵심은:
"요구사항 변경에 기민하게 반응하는 것"
- 전체 시스템 중 일부만 먼저 개발
- 반복적이고 점증적인 프로세스
- 피드백을 통한 지속적인 개선
진화적 설계와 리팩터링
처음부터 완벽할 수 없다.
이것이 애자일과 DDD의 핵심 마인드셋이다. 지속적인 개발 반복주기와 리팩터링을 통해 도메인 모델을 발전시켜야 한다.
값 객체는 리팩터링을 할수록 나온다.
처음에는 엔티티로 설계했더라도, 리팩터링 과정에서 "이것은 값 객체로 충분하겠다"는 인사이트를 얻게 된다. 그래서 리팩터링은 DDD에서 필수다.
DDD의 선행 조건
에릭 에반스의 책에서 강조한 두 가지:
- 개발은 반복 주기를 통해 진행되어야 한다
- 개발자와 도메인 전문가는 밀접한 관계를 가져야 한다
이것이 바로 애자일과 DDD가 만나는 지점이다.
Part 4: 전략적 설계
Bounded Context (바운디드 컨텍스트)
하나의 거대한 도메인 모델은 비현실적이다. 컨텍스트에 따라 같은 개념도 다르게 표현된다.
예를 들어 "Ticket"이라는 개념은:
- 판매 컨텍스트: 판매 상품
- 배송 컨텍스트: 배송 물품
- 출력 컨텍스트: 출력물
바운디드 컨텍스트는:
- 특정 도메인 모델이 적용되는 범위
- 같은 컨텍스트 안에서는 도메인 모델의 통합성 유지
- 서로 다른 컨텍스트 사이에서는 통합성에 신경 쓰지 않음
- 프레젠테이션부터 영속성까지 수직 슬라이스 포함
바운디드 컨텍스트는 조직 관리와 설계가 만나는 곳이다.
각각의 바운디드 컨텍스트는 팀 단위로 관리된다. 이것이 마이크로서비스 아키텍처와 연결되는 지점이다.
하지만 바운디드 컨텍스트가 꼭 마이크로서비스 아키텍처와 연관된 것은 아니다. 모놀리식에서도 이 원칙은 그대로 유효하다.
Strategic Distillation (전략적 디스틸레이션)
모든 도메인이 똑같이 중요한 것은 아니다.
Core Domain (핵심 도메인)
- 사업의 성공을 결정하는 핵심
- 최고의 인력 배치
- 가장 많은 시간과 노력 투자
Generic Subdomain (제네릭 서브도메인)
- 경쟁 우위를 제공하지 않음
- 기성 소프트웨어 구입 또는 주니어 개발자 배치
Supporting Subdomain (지원 서브도메인)
- 핵심 도메인을 지원
- 외주 또는 개발자 성장 기회로 활용
DDD에서 제일 중요한 것은 사업적 기준이 되는 도메인 모델까지 끌고 올라가서 설계하는 것이다.
마치며: DDD에서 얻은 인사이트
1. DDD는 기술이 아니라 철학이다
DDD는 단순히 "이렇게 코드를 작성하세요"가 아니다. **"이렇게 생각하세요"**에 가깝다. 복잡한 문제를 어떻게 바라보고, 어떻게 단순화하고, 어떻게 표현할 것인지에 대한 사고방식이다.
2. 완벽함보다 반복이 중요하다
처음부터 완벽한 도메인 모델을 만들 수 없다. 그리고 그럴 필요도 없다. 중요한 것은:
- 도메인 전문가와의 지속적인 대화
- 반복적인 리팩터링
- 코드와 모델의 동기화
3. 값 객체를 사랑하라
강의를 들으면서 가장 실용적인 조언은 **"값 객체를 최대한 많이 만들어라"**였다.
- 불변 객체는 복잡도를 낮춘다
- 리팩터링을 통해 값 객체가 드러난다
- "이거 엔티티인데 값 객체로 만들 수 있지 않을까?" 질문을 계속하라
4. 애그리게이트는 작게, 불변식을 기준으로
- 객체 구조가 아닌 불변식으로 애그리게이트 결정
- 트랜잭션 당 하나의 애그리게이트
- 결과적 일관성을 두려워하지 말 것
5. 기술보다 도메인에 집중하라
DDD의 목표는 도메인 로직을 기술로부터 격리하는 것이다. JPA, Spring 같은 기술에 도메인 로직이 오염되지 않도록 해야 한다.
다음 스텝
이번 강의를 통해 DDD의 전체적인 그림을 이해할 수 있었다. 하지만 진짜 학습은 이제부터다:
- 실습 프로젝트에 적용해보기
- 간단한 도메인부터 시작
- 엔티티와 값 객체 구분 연습
- 애그리게이트 경계 설정 연습
- 에릭 에반스의 책 읽기
- 이제는 맥락을 이해했으니 책을 다시 읽어볼 차례
- 특히 Part 1과 Part 4에 집중
- 리팩터링 연습
- 기존 코드를 DDD 관점에서 바라보기
- 도메인 로직이 누수된 부분 찾기
- 값 객체로 추출할 수 있는 부분 찾기
DDD는 하루아침에 습득할 수 있는 기술이 아니다. "하면서 알아가는 것"이라는 애자일의 정신처럼, 실제로 적용하면서 조금씩 이해의 깊이를 더해갈 것이다.
이번 강의는 그 여정의 좋은 출발점이 되었다.
'WIL' 카테고리의 다른 글
| MSA와 EDA 이해하기: 분산 아키텍처 기본 개념 정리 (0) | 2025.10.26 |
|---|---|
| 10주간의 백엔드 부트캠프 회고: 설계부터 운영까지, 실전적 고민의 기록 (0) | 2025.09.17 |
| 캐시 전략(Cache Strategies) 정리 (3) | 2025.08.17 |
| 동시성 문제와 RDB에서의 해결 — 비관적 락 적용기 (4) | 2025.08.10 |
| WIL – TDD & 테스트 가능한 구조 (0) | 2025.07.16 |