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 - 순수한 도메인 모델 === 테스트하기 위한 도메인 모델을 의미. 도메인 로직과 외부 의존성이 분리되어있지 않으면 테스트 작성이 어려움
•
의사 결정 과정을 여러 계층으로 분리한 예시
◦
이메일 검사 로직을 UserController 와 User 로 분리한 것
3. Summary
•
도메인 모델 완전성
◦
도메인 로직이 파편화되어있지 않고 도메인 모델에 위치하는 것
•
도메인모델 순수성
◦
도메인 모델이 프로세스 외부 의존성(out-of-process dependencies)을 가지지 않는 것
•
트릴레마의 3가지 요소
◦
도메인 모델 완전성
◦
도메인 모델 순수성
◦
애플리케이션 퍼포먼스
•
트릴레마 3가지 요소의 특징
◦
대부분의 경우 3가지 모두 달성하기 어려움
◦
최대 2가지만 달성 가능하며 나머지 하나는 trade-off로 포기할 수 밖에 없음
•
제안 사항
◦
퍼포먼스에 많은 영향을 주지 않는다면 비즈니스 작업 가장자리에 모든 쓰기/읽기를 밀어넣는 것을 권장
▪
도메인 모델 완전성과 도메인 모델 순수성을 달성하는 옵션
◦
만약 위의 옵션을 적용하는 것이 어렵다면 도메인 모델 순수성을 지키는 옵션을 적용하는 것을 권장