Search
moon
sun
🏗️

도메인 주도 설계의 사실과 오해 (2) Preface, Entity & VO

Tags
Architcture
DDD
Study
Last edited time
2024/11/01 00:24
2 more properties
Search
🏗️
도메인 주도 설계의 사실과 오해 2부 (3) 모델의 경계를 긋고, 핵심에 집중하라
🏗️
도메인 주도 설계의 사실과 오해 2부 (3) 모델의 경계를 긋고, 핵심에 집중하라

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는 불변으로 만들고
◦
상태변경으로 관리하려는 포인트만 엔티티에서 관리
◦
엔티티를 단순화 시키는 것이 중요