Search
🏗️

[DDD] 트릴레마; 도메인 모델 완전성 vs 도메인 모델 순수성

Created
2024/05/21 07:10
Tags
Architcture
DDD
Post
Last edited time
2024/08/23 07:26
Status
Done

들어가기 앞서

앞의 포스트 [DDD] Domain Service vs Application Service 에서 도메인 서비스 와 애플리케이션 서비스의 차이점에 대해 살펴보았다. Vladimir Khorikov 의 블로그를 보면 DDD 관련 좋은 포스트가 많이 있는데, 이번에는 도메인 모델을 어떻게 구성하는 것이 좋은지, 특히 도메인 모델 완전성과 순수성에 대한 포스트에 대한 내용도 번역하여 살펴보도록 하자.

1. Domain Model Completeness & Domain Model Purity

1.1. Domain Model Completeness (도메인 모델 완전성)

A complete domain model (완전한 도메인 모델)
모든 비즈니스 로직이 도메인 클래스(모델)에 위치
cf) Domain logic fragmentation (도메인 로직 파편화)
도메인 로직이 도메인 레이어가 아닌 다른 레이어에 존재하는 것을 의미
예시 코드
모든 비즈니스 로직이 도메인 객체인 User에 존재
// Domain Layer export class User { private email: string; private company: Company; changeEmail(newEmail: string): Result { if (!this.company.isEmailCorporate(newEmail)) { return Result.Failure('Incorrect email domain'); } this.email = newEmail; return Result.Success(); } // 중략 } // Company.ts export class Company { public domainName: string; isEmailCorporate(email: string): boolean { const emailDomain = email.split('@')[1]; return emailDomain === this.domainName; } // 중략 } // Controller export class UserController { private userRepository: UserRepository; changeEmail(userId: number, newEmail: string): string { const user = this.userRepository.getById(userId); if (!user) { return 'User not found'; } const result = user.changeEmail(newEmail); if (!result.isSuccess) { return result.error!; } this.userRepository.save(user); return 'OK'; } // 중략 }
TypeScript
복사

1.2. Domain Model Purity (도메인 모델 순수성)

A pure domain model (순수한 도메인 모델)
프로세스 외부 의존성(out-of-process dependencies)을 가지지 않는 모델
cf) 프로세스 외부 의존성
애플리케이션의 실행 프로세스 외부에서 실행되는 의존성
예) DB 의존성
도메인 객체는 원시타입이나 다른 도메인 객체만 의존해야함
도메인 모델 순수성이 적용된 예시 코드
export class User { private email: string; private company: Company; changeEmail(newEmail: string): Result { if (!this.company.isEmailCorporate(newEmail)) { return Result.Failure('Incorrect email domain'); } this.email = newEmail; return Result.Success(); } // 중략 } // Company.ts export class Company { public domainName: string; isEmailCorporate(email: string): boolean { const emailDomain = email.split('@')[1]; return emailDomain === this.domainName; } // 중략 }
TypeScript
복사

1.3. 도메인 모델 완전성과 도메인 모델 순수성 간의 딜레마

새로운 요구사항
이메일 변경시, 이미 존재하는 이메일인지 검사하는 비즈니스 로직 추가
일반적인 구현 방식
도메인 로직이 controller에 추가되어 도메인 로직 파편화가 발생 → 도메인 모델 완전성을 달성하지 못함
export class UserController { private userRepository: UserRepository; changeEmail(userId: number, newEmail: string): string { const existingUser = userRepository.getByEmail(newEmail); if (existingUser !== null && existingUser.id !== userId) { return "Email is already taken"; } const user = this.userRepository.getById(userId); const result = user.changeEmail(newEmail); if (!result.isSuccess) { return result.error!; } this.userRepository.save(user); return 'OK'; } }
TypeScript
복사
도메인 로직 완정성을 회복한 예시 코드
도메인 로직 파편화는 제거 되었으나, 도메인 모델 순수성을 달성하지 못함.
도메인 모델이 외부 의존성 (Repository)를 가지게 됨
export class User { private email: string; private company: Company; private userRepository: UserRepository; changeEmail(newEmail: string): Result { if (!this.company.isEmailCorporate(newEmail)) { return Result.Failure('Incorrect email domain'); } const existingUser = userRepository.getByEmail(newEmail); if (existingUser !== null && existingUser.id !== userId) { return "Email is already taken"; } this.email = newEmail; return Result.Success(); } }
TypeScript
복사
다른 옵션을 적용하더라도 여전히 도메인 모델 순수성을 달성하지 못함
여전히 DB 의존성을 도메인 객체가 가짐
// 인터페이스 적용 changeEmail(newEmail: string, repository: IUserRepository) // 유저 검사 로직을 위임 ChangeEmail(string newEmail, Func<string, bool> isEmailUnique)
TypeScript
복사
결론
도메인 모델 완전성을 달성하려하다보면 도메인 모델 순수성을 달성하지 못하는 경우가 발생한다.
반대로, 모데인 모델 순수성을 달성하려하다보면 도메일 모델 완전성을 달성하지 못하는 경우가 발생한다.
도메인 모델 순수성과 도메인 모델 완전성을 모두 달성하는것은 불가능하다.

2. The trilemma

2.1. 애플리케이션 퍼포먼스

애플리케이션 퍼포먼스(성능)
도메인 모델 순수성과 도메인 모델 완전성과 트릴레마를 구성하는 요소
예시
모든 유저들을 불러와 메모리에 저장한 후 User 객체로 전달
도메인 모델 순수성과 도메인 모델 완전성을 모두 만족하나, 애플리케이션 퍼포먼스가 상당히 저해됨
class User { static IsEmailCorporate(email: string): boolean { return email.endsWith("@corporate.com"); } ChangeEmail(newEmail: string, allUsers: User[]): Result { if (!User.IsEmailCorporate(newEmail)) { return Result.Failure("Incorrect email domain"); } const emailIsTaken = allUsers.some(x => x.email === newEmail && x !== this); if (emailIsTaken) { return Result.Failure("Email is already taken"); } this.email = newEmail; return Result.Success(); } } class UserController { private _userRepository: UserRepository; ChangeEmail(userId: number, newEmail: string): string { const allUsers = this._userRepository.GetAll(); const user = allUsers.find(x => x.id === userId); if (!user) { return "User not found"; } const result = user.ChangeEmail(newEmail, allUsers); if (result.IsFailure) { return result.error; } this._userRepository.Save(user); return "OK"; } }
TypeScript
복사

2.2. 트릴레마의 구성요소

구성요소
도메인 모델 완전성
도메인 로직 파편화가 일어나지 않고, 도메인 로직이 모두 도메인 레이어에 위치해야 함
도메인 모델 순수성
도메인 레이어가 어떠한 외부 의존성(프로세스 외부 의존성)을 가지지 않아야 한다.
애플리케이션 퍼포먼스
외부 의존성에 대한 불필요한 호출이 존재하지 않아야 하며, 적정 수준의 애플리케이션 퍼포먼스를 유지해야한다.
반드시 필요한 경우에만 외부 의존성을 호출해야한다.
3가지 구성요소의 접근 방식
선행 조건
도메인 모델 완전성
도메인 모델 순수성
퍼포먼스
결과
비즈니스 작업 가장자리로 모든 외부 읽기 쓰기를 밀어냄 (2.1. 예시)
O
O
X
퍼포먼스 저하
외부 의존성을 도메인 모델에 주입
O
X
O
도메인 모델 순수성을 잃음
의사결정 프로세스를 도메인 레이어와 다른 레이어로 분리
X
O
O
도메인 모델 완전성을 잃음
그 어떠한 경우에도 트릴레마 중 최대 2가지 속성만 만족시 킬 수 있음

2.3. 옵션 1 검토

Pushing all external reads and writes to the edges of the business operation
비즈니스 작업 가장자리로 모든 외부 읽기 쓰기를 밀어냄
애플리케이션 로직 방식이 조회-판단-행동(read-decide-act)의 아키텍처를 따르는 경우에만 사용 가능
read: 저장소에서 데이터 조회
decide: 비즈니스 로직 실행
act: 저장소에 데이터 영속화
예) 새로운 이메일 중복 여부 확인
read: DB에서 유저 정보 조회
decide: 기업용 이메일이 맞는지 검사 & 이미 존재하는 이메일인지 검사
act: 이메일 업데이트
Flow
옵션 1은 현실성이 떨어짐
현실 세계에서는 의사결정 과정에서 추가적으로 데이터를 질의해야하는 경우가 존재할 수 있음
대안
옵션 2 (외부 의존성을 도메인 모델에 주입) → 도메인 모델 완전성 달성
옵션 3 (의사결정 과정; 비즈니스 로직을 도메인 레이어와 다른 레이어로 분리) → 도메인 모델 순수성 달성

2.4. 저자의 제안; 옵션 3

splitting the decision-making process between the domain layer and controllers
도메인 모델 순수성을 달성하는 옵션 3 제안
도메인 로직을 파편화 하는게 도메인 모델에 외부 의존성을 가지는 것보다 그나마 나음
도메인 모델 순수성의 중요성
애플리케이션에서 가장 중요하며 복잡한 부분임
외부 의존성이 주입되면, 도메인 레이어의 복잡도가 상당히 높아짐
따라서, 가능한한, 도메인 레이어는 도메인 로직 자체 외의 모든 책임에서 제외되어야함 (외부 의존성이 없어야 함)
의사 결정 과정(decision-making process; 도메인 로직)을 여러 계층(domain layer & controller)로 나누는 접근 방식은 FP, DDD, Unit Test 등에서 이미 공통적으로 나타나고 있음
DDD - 관리가능한 애플리케이션 복잡도를 유지하기 위해 이 방법 사용
FP - 함수를 순수하게 만들 기 위한 유일한 방법임. 함수형 프로그래밍의 핵심은 참조 투명성을 유지하고 숨겨진 입출력을 피하는 것임
Unit Test - 순수한 도메인 모델 === 테스트하기 위한 도메인 모델을 의미. 도메인 로직과 외부 의존성이 분리되어있지 않으면 테스트 작성이 어려움
의사 결정 과정을 여러 계층으로 분리한 예시
이메일 검사 로직을 UserControllerUser 로 분리한 것

3. Summary

도메인 모델 완전성
도메인 로직이 파편화되어있지 않고 도메인 모델에 위치하는 것
도메인모델 순수성
도메인 모델이 프로세스 외부 의존성(out-of-process dependencies)을 가지지 않는 것
트릴레마의 3가지 요소
도메인 모델 완전성
도메인 모델 순수성
애플리케이션 퍼포먼스
트릴레마 3가지 요소의 특징
대부분의 경우 3가지 모두 달성하기 어려움
최대 2가지만 달성 가능하며 나머지 하나는 trade-off로 포기할 수 밖에 없음
제안 사항
퍼포먼스에 많은 영향을 주지 않는다면 비즈니스 작업 가장자리에 모든 쓰기/읽기를 밀어넣는 것을 권장
도메인 모델 완전성과 도메인 모델 순수성을 달성하는 옵션
만약 위의 옵션을 적용하는 것이 어렵다면 도메인 모델 순수성을 지키는 옵션을 적용하는 것을 권장

4. Reference