List
Search
1. Preface
1.1. 도메인 주도 설계의 맥락
1.1.1. 모델 주도 설계
•
모델 주도 설계
◦
모델과 핵심 설계는 서로 영향을 주며 반복을 통해 구체화됨
1.1.2. 도메인 주도 설계의 맥락
•
도메인 모델 = 코드
1.2. 빌딩 블록
1.2.1. 해결해야 할 두 가지 연관된 문제
우리가 생각한 도메인으로 코드를 잘 표현하는 것이 중요
•
요구사항에 적합한 모습으로 도메인을 어떻게 모델링할 것인가?
•
도메인을 반영한 코드를 어떻게 개발할 것인가?
1.2.2. 빌딩 블록의 목적
•
구현에 대한 가이드를 제공해서 복잡도를 낮추는 것
◦
도메인을 표현하기 위한 빌딩 블록
▪
Entity, Value Object, Service, Module, Association
◦
생명주기를 관리하기 위한 빌딩 블록
▪
Aggregate, Repository, Factory
1.3. 이번 파트의 목표
•
도메인의 개념을 코드로 옮기기 위한 직관적인 가이드
◦
도메인 개념을 적절하게 코드로 잘 구현하겠다
◦
도메인 개념을 가지고 코드로 어떻게 옮길건지?
•
궁극적인 목표는 복잡성을 낮추는 것
2. 오늘의 예제 (배달앱)
2.1. 도메인 개념
•
앱 UI 예시 화면
•
도메인 개념 도식화
2.2. 도메인 객체
•
따르릉 직화삼겹 & 불고기 가게
2.3. DDD에서의 기능 구현
•
기능 요구사항과 불변식을 애그리거트로 구현
◦
코드의 구조를 불변식에 맞춰서 해야 함
◦
DDD는 기능요구사항과 불변식을 합쳐서 코드로 구현함
▪
구현 단위 = 애그리거트
•
개발자 입장에서 불변식을 추출하는 것은 요구사항밖에 없음
◦
비즈니스 요구사항 - “불변식은 이러이러한 조건이 있어”
•
애그리거트는 무엇을 기반으로 뽑는가?
◦
나한테 들어온 요구사항을 가지고 구현해야 함
◦
코드의 요구사항 = 코드의 경계
2.3.1. 기능 (1) 메뉴 공개하기
•
불변식
◦
상태 변경을 할 수 있는 것과 할 수 없는 것
2.3.2. 기능 (2) 옵션 그룹 추가하기
•
불변식
◦
어떤 경우에는 추가할 수 도 있고, 없을 수도 있음
2.3.3. 기능 (3) 옵션 그룹 삭제하기
•
불변식
◦
삭제할 수 있는 애들이 있고, 없는 애들이 있음
2.4. 불변식
•
판매 중인 Menu에는 OptionGroup이 한 개 이상 존재해야 한다.
•
판매 중인 Menu에는 가격이 0원 이상인 Option이 한 개 이상 존재해야 한다.
•
판매 중인 Menu에는 필수 OptionGroup이 한 개 이상 존재해야 한다.
•
판매 중인 Menu에는 필수 OptionGroup이 세 개 이하로 존재해야 한다.
•
Menu 안에서 OptionGroup의 이름은 중복되지 말아야 한다.
•
OptionGroup에는 Option이 한 개 이상 존재해야 한다.
3. 엔티티와 값 객체
3.1. 엔티티 (Entity)
3.1.1. 엔티티의 정의
•
수많은 객체는 본질적으로 해당 객체의 속성이 아닌 연속성과 식별성이 이어지는지를 기준으로 정의
◦
연속성과 식별성이 중요
3.1.2. 엔티티의 연속성
•
엔티티는 모델링 설계상의 특수한 고려사항에 포함되어 있음. 엔티티는 자신의 생명주기 동안 형태의 내용이 급격하게 바뀔 수도 있지만 연속성은 유지되어야 한다.
◦
“연속적으로 계속 지켜봐야하는 애야”
•
예시)
◦
데이터베이스의 명칭이 스타벅스 → 메가커피로 바뀌더라도 동일한 Shop으로 추적
데이터베이스 → | 시스템1 → | 메시지큐 → | 시스템2 |
Shop
- Name
- 스타벅스
- 메가커피 | Thread1
:Shop
name = “스타벅스”
Thread2
:Shop
name = “스타벅스” | {
“name”: “스타벅스”
} | Thread1
:Shop
name = “스타벅스” |
3.1.3. 엔티티의 식별성
•
어떤 객체를 일차적으로 해당 객체의 식별성으로 정의할 경우, 그 객체를 엔티티라고 함
◦
“다른 애랑 구별할수 있어야 해”
•
예시)
◦
다른 엔티티와 구별하기 위한 식별자(id = 1)를 가짐
데이터베이스 → | 시스템1 → | 메시지큐 → | 시스템2 |
Shop
- id: 1
- Name : 스타벅스 | Thread1
:Shop
id = 1
name = “스타벅스”
Thread2
:Shop
id = 1
name = “스타벅스” | {
“id”: 1
“name”: “스타벅스”
} | Thread1
:Shop
id = 1
name = “스타벅스” |
3.1.4. 엔티티의 단점
•
상태가 다른 경우에도 동일한 객체로 식별하는데 따르는 복잡성 존재
데이터베이스 → | 시스템1 → | 메시지큐 → | 시스템2 |
Shop
- id: 1
- Name : 스타벅스
메가커피 | Thread1
:Shop
id = 1
name = “스타벅스”
Thread2
:Shop
id = 1
name = “메가커피” | {
“id”: 1
“name”: “메가커피”
} | Thread1
:Shop
id = 1
name = “스타벅스” |
3.2. 값 객체 (Value Object)
3.2.1. 값 객체의 정의
•
속성 동일성
◦
속성에만 관심 있음. 식별성이 없음
◦
값만 같으면 동일하다고 판단
•
엔티티의 속성을 표현할 때 씀
•
엔티티를 단순화하기 위해 사용 (심플하게 만들기 위해)
•
cf) 값 객체와 DTO
◦
값 객체는 데이터가 아님 (DTO)
◦
데이터 관점이 아니라 행위 관점
◦
DTO은 불완전한 데이터를 일단 밀어 넣고 validate 한다면, 값 객체는 들어올때 validate 함
3.2.2. 값 객체와 복잡성 낮추기 (1)
•
어떤 요소의 속성에만 관심이 있다면 그것을 값 객체로 분류하라
•
값 객체에서는 그것이 전하는 속성의 의미를 표현하게 하고, 값 객체에 그 속성과 관련된 기능을 부여하라
•
값 객체는 불변적인 것으로 다루어라
•
값 객체에는 아무런 식별성도 부여하지 않고 Entity를 유지하는데 필요한 설계상의 복잡함만 피해라
•
불변 객체로 만들어라
•
값 객체는 불변적인 한 변경관리는 단순해진다.
•
완전히 교체하지 않는 한 아무것도 변경되지 않음
•
불변 객체는 마음껏 교체할 수 있음
3.3. 엔티티와 값 객체
3.3.1. 동일성(identical)과 동등성(equal)
엔티티. 각 사람들은 고유하며 식별성을 띔 (식별자 동등성)
값 객체. 각 숫자들은 식별성이 없음 (속성 동등성)
3.3.2. 엔티티 기반 클래스
•
도메인 엔티티
public abstract class DomainEntity<T extends DomainEntity<T, TID>, TID> {
public boolean equals(T other) {
if (other == null) return false;
if (getId() == null) return false;
if (other.getClass().equals(getClass())) {
return getId().equals(other.getId()); // 식별자 동일성
}
return super.equals(other);
}
@Override
public boolean equals(Object other) {
if (other == null) return false;
return equals((T) other);
}
@Override
public int hashCode() {
return getId() == null ? 0 : getId().hashCode();
}
abstract public TID getId();
}
Java
복사
•
Shop 엔티티
public class Shop extends DomainEntity<Shop, ShopId> {
private ShopId id;
private String name;
private Money minOrderPrice;
@Override
public ShopId getId() {
return id;
}
// other methods...
}
Java
복사
3.3.3. 값 객체 기반클래스
•
값 객체
public abstract class ValueObject<T extends ValueObject<T>> {
@Override
public boolean equals(Object other) {
if (other == null) return false;
if (!(other.getClass().equals(getClass()))) return false;
return equals((T) other);
}
public boolean equals(T other) {
if (other == null) return false;
// 값 동등성
return Arrays.equals(getEqualityFields(), other.getEqualityFields());
}
@Override
public int hashCode() {
// hashCode implementation...
}
protected Object[] getEqualityFields() {
return Arrays.stream(getClass().getDeclaredFields())
.map(field -> {
try {
field.setAccessible(true);
return field.get(this);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
})
.toArray();
}
}
Java
복사
•
Money 값 객체
public class Money extends ValueObject<Money> {
public static final Money ZERO = Money.wons(0);
private final BigDecimal amount;
public static Money wons(long amount) {
return new Money(BigDecimal.valueOf(amount));
}
public static Money wons(double amount) {
return new Money(BigDecimal.valueOf(amount));
}
public Money plus(Money other) {
return new Money(this.amount.add(other.amount));
}
public Money minus(Money other) {
return new Money(this.amount.subtract(other.amount));
}
@Override
protected Object[] getEqualityFields() {
return new Object[] { amount.doubleValue() };
}
// other methods...
}
Java
복사
3.3.4. Shop 엔티티의 속성을 표현하는 값 객체
•
Money minOrderPrice
public class Shop extends DomainEntity<Shop, ShopId> {
private ShopId id;
private String name;
private Money minOrderPrice;
// other methods...
}
Java
복사
•
값 객체가 데이터베이스와 맵핑되어있음
◦
Shop Table
▪
ID (PK)
▪
NAME
▪
MIN_ORDER_PRICE
•
식별성이 없고 값의 동등성으로 구분
4. 값 객체와 복잡성 낮추기
여러 VALUE OBJECT의 인스턴스 가운데 어느 것을 사용하는 지는 중요하지 않다. VALUE OBJECT가 불변적인 한 변경관리는 단순해진다. 즉, 완전히 교체하지 않는 한 아무 것도 변경되지 않는다. 불변 객체는 마음껏 교체할 수 있다. … 복사와 공유와 같은 문제에 관해 자유롭게 의사결정을 내릴 수 있다.
4.1. Money가 가변객체라면
•
가변 객체 Money
◦
속성에 계산 결과를 다시 대입
◦
객체의 상태 변경
public class Menu {
private BigDecimal amount;
public void plus(Money other) {
this.amount = this.amount.plus(other.amount);
}
}
Java
복사
•
가변객체를 이용한 계산
◦
객체의 속성이 자꾸 변하므로 계산 결과를 파악하기 어려움
Money amount1 = Money.wons(10000L);
Money amount2 = Money.wons(20000L);
amount1.plus(amount2);
amount1.minus(amount2);
amount2.minus(amount1);
amount1.plus(amount2);
amount1 = ?
amount2 = ?
Java
복사
•
A가 Money의 amount 변경 → 변경에 의한 파급효과 발생
4.2. Money가 불변객체라면
•
불변 객체 Money
◦
새료운 인스턴스 생성하여 반환
◦
객체의 상태가 변하지 않음
public class Money extends ValueObject<Money> {
private final BigDecimal amount;
public Money plus(Money other) {
return new Money(this.amount.add(other.amount));
}
}
Java
복사
•
불변객체를 이용한 계산
◦
객체의 속성이 변하지 않고, 새로운 인스턴스가 생성되므로 그대로 값이 유지됨
Money amount1 = Money.wons(10000L);
Money amount2 = Money.wons(20000L);
Money amount3 = amount1.plus(amount2);
Money amount4 = amount1.minus(amount2);
Money amount5 = amount2.minus(amount1);
Money amount6 = amount1.plus(amount2);
amount1 = ?
amount2 = ?
Java
복사
•
새로운 객체가 생성되므로 파급효과가 발생하지 않음
4.3. 요약
•
불변 객체로 만들어라
•
값 객체는 불변적인 한 변경관리는 단순해진다.
•
완전히 교체하지 않는 한 아무것도 변경되지 않음
•
불변 객체는 마음껏 교체할 수 있음
5. 엔티티의 복잡성 낮추기
[ENTITY에는] 개념에 필수적인 행위만 추가하고 그 행위에 필요한 속성만 추가한다.
그 밖의 객체는 행위의 속성을 검토해서 가장 중심이 되는 ENTITY와 연관관계에 있는 다른 객체로 옮긴다. 이들 중 일부는 ENTITY가 될 것이다. 또 어떤 것은 VALUE OBJECT가 될 것이다.
식별성 문제를 제외하면 ENTITY는 주로 자신이 소유한 객체의 연산을 조율해서 책임을 완수한다.
5.1. 가게 영업 시간 예시 (1)
•
isOpen - 영업 중인지 확인
•
putOffOneHourOn - 특정 요일의 영업 시작, 종료 시간을 1시간 뒤로 연장
public class Shop extends DomainEntity<Shop, ShopId> {
// 요일 별 영업 시작 시간
private Map<DayOfWeek, LocalTime> startTimes;
// 요일 별 영업 종료 시간
private Map<DayOfWeek, LocalTime> endTimes;
public boolean isOpen() {
return isOpen(LocalDateTime.now());
}
public boolean isOpen(LocalDateTime time) {
LocalTime startTime = startTimes.get(time.getDayOfWeek());
LocalTime endTime = endTimes.get(time.getDayOfWeek());
if (startTime == null || endTime == null) {
return false;
}
return time.toLocalTime().isAfter(startTime) && time.toLocalTime().isBefore(endTime);
}
public void putOffOneHourOn(DayOfWeek dayOfWeek) {
LocalTime startTime = startTimes.get(dayOfWeek);
LocalTime endTime = endTimes.get(dayOfWeek);
if (startTime == null || endTime == null) {
return;
}
startTimes.put(dayOfWeek, startTime.plus(1, ChronoUnit.HOURS));
endTimes.put(dayOfWeek, endTime.plus(1, ChronoUnit.HOURS));
}
}
Java
복사
5.2. 가게 영업 시간 예시 (2)
•
영업 시간을 값 객체로 분리
public class Shop extends DomainEntity<Shop, ShopId> {
private Map<DayOfWeek, TimePeriod> operationHours;
...
}
Java
복사
public class TimePeriod extends ValueObject<TimePeriod> {
private LocalDateTime startTime;
private LocalDateTime endTime;
public boolean contains(LocalTime datetime) {
return (datetime.isAfter(startTime) || datetime.equals(startTime)) &&
(datetime.isBefore(endTime) || datetime.equals(endTime));
}
public TimePeriod putOffHours(long hours) {
return new TimePeriod(startTime.plusHours(hours), endTime.plusHours(hours));
}
}
Java
복사
5.3. 모든 상태 변경을 엔티티로
•
가게 영업 시간 예시를 보면, 영업 시간 이라는 용어를 말하고 있는데 코드로 표현되고 있지 않았음
◦
말하고 있는걸 값 객체로 묶어서 코드로 표현
•
복잡한 상태 변경 로직을 파악하고 싶다면 단순하게 설계된 엔티티만 이해하면됨
◦
EntryPoint 역할
◦
내부 로직은 각각의 값 객체들이 책임을 가지고 있음
•
엔티티를 VO의 집합으로 많이 표현하면 엔티티가 짧아짐
•
복잡성이 낮아짐
•
복잡해지고 속성이 많아지고 찢자니 애매하다면 값객체를 잘 활용하면 좋음
•
불변 값 객체로 잘 활용하면 코드를 이해하기 쉽고 수정하기 쉬울 수 있음
5.4. 엔티티인가 값 객체인가
•
도메인의 특성에 따라 다름
•
가능하다면 값 객체로 구현
•
초반에 큰 그림에서 얘기하는 개념은 대부분 엔티티 (사용자, 주문, 가게 등)
•
값 객체
◦
코드하면서 리팩토링하면서 나옴
◦
처음부터 요구사항에서 뽑긴 어려움
•
엔티티
◦
VO는 불변으로 만들고
◦
상태변경으로 관리하려는 포인트만 엔티티에서 관리
◦
엔티티를 단순화 시키는 것이 중요