Search
🏗️

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

Tags
Architcture
DDD
Study
Last edited time
2024/11/01 00:24
2 more properties
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는 불변으로 만들고
상태변경으로 관리하려는 포인트만 엔티티에서 관리
엔티티를 단순화 시키는 것이 중요