도메인 주도 설계
도메인 주도 설계란 이용자의 세계와 소프트웨어 구현을 연결 짓는 것이다. 유용한 소프트웨어를 만들려면 이용자의 세계를 이해하고, 그들의 문제를 해결할 수 있는 수단을 제공해야 한다. 기술적인 접근으로만 문제를 접근하면 목적 없는 소프트웨어가 만들어질 위험이 있다.
도메인 모델링
모델은 현실에 일어나는 사건 또는 개념을 추상화한 개념을 일컫는다. 그리고 이것을 추상화하는 작업을 모델링이라고 한다. 추상화를 할 때 무엇을 남기고 무엇을 버릴지는 도메인에 따라 결정된다.
책에서 설명하는 패턴
책에서 설명하는 패턴의 순서는 다음과 같으며 순차적으로 다뤄볼 것이다.
지식을 위한 패턴
- 값 객체 - 도메인만의 고유한 개념을 값으로 나타내는 패턴
- 엔티티 - 도메인 개념을 나태내기 위한 객체. 동일성으로 식별됨(식별자를 가짐)
- 도메인 서비스 - 값 객체나 엔티티만으로 잘 표현할 수 없는 지식을 다루기 위한 패턴
애플리케이션을 구성하는 패턴
- 레포지토리 - 데이터 퍼시스턴시(저장, 복원)를 담당하는 객체
- 애플리케이션 서비스 - 앞선 4가지 요소가 서로 협력하며 어플리케이션으로 기능하는 장소
- 팩토리 - 객체를 만드는 데 필요한 지식에 특화된 객체. 객체 생성에 대한 지식을 한곳에 모음
지식 표현을 위한 고급 패턴
- 애그리게이트 - 무결성을 유지하는 경계
- 명세 - 객체를 평가하기 위한 지식. 명세 패턴을 적용하면 객체를 평가하는 기준을 모듈로 분리 가능
값 객체
원시 데이터 타입만으로 도메인의 개념을 나타내지 못할 때, 시스템 특유의 값을 정의하기 위해 값 객체를 활용한다. 이해를 위해 성명을 입력하는 경우를 생각해보자.
const fullName = "kim cheolsu”
const tokens = fullName.split(" ")
const lastName = tokens[0]
성명을 나타내는 fullName 변수에서 첫 번째 문자열은 성이다. 그런데 외국인은 John Smith 로 이름을 표현하기 때문에 두 번째 문자열이 성이 되어야 한다. 위치만으로 어떤 것이 성인지 확신할 수 없기 때문에, 이름에 대한 클래스를 만들어서 성과 이름을 명확하게 구분하면 보다 명료해진다.
class FullName {
constructor(
private firstName: string,
private lastName: string,
) {}
getFirstName() {
return this.firstName;
}
getLastName() {
return this.lastName;
}
}
const fulName = new FullName("kim", "cheolsu");
const lastName = fulName.getLastName();
const firstName = fulName.getFirstName();
console.log(lastName); // cheolsu
console.log(firstName); // kim
이를 통해 시스템에서 필요로 하는 값이 원시데이터 타입만으로 충분히 표현될 수 없다는 것을 살펴볼 수 있을 것이다. 도메인 주도 설계에서 말하는 값 객체는 앞선 예시처럼 시스템 특유의 값을 나타내는 객체다.
값의 성질과 값 객체 구현
값의 역할을 하는 것이 값 객체라면, 값은 어떤 성질을 가지고 있는지 대표적인 3가지를 살펴보자.
- 변하지 않는다.
- 주고받을 수 있다.
- 등가성을 비교할 수 있다.
값의 불변성
우리가 어떤 값을 사용할 때는 그 값이 변하지 않는다는 확신이 있기 때문이다. 예를 들어, 1이라는 숫자 타입의 값을 사용한다면 이것은 1이지 0이나 2가 되면 안된다. 즉, 변수에 새로운 값을 대입하여 수정할 때 변하는 것은 변수의 내용이지 값 자체가 수정되는 것이 아니다.
이렇게 값의 불변성은 중요하기 때문에 값 내부적으로 값을 변화시키는 메서드가 있어서는 안된다. 그리고 값의 역할을 하는 값 객체 역시 마찬가지다. 앞서 예시로 들었던 FullName 클래스에서 값을 수정하는 changeLastName() 같은 메서드가 정의되어선 안된다.
교환 가능
값은 교환 가능해야 하나, 그 형식은 대입문으로만 가능하다. 그 외의 수단으로는 수정을 할 수 없어야 한다.
등가성 비교 가능
원시 타입의 값과 마찬가지로 값 객체도 비교할 수 있어야 한다. 이때, 값 객체의 요소를 꺼내서 비교하는 것이 아니라 값 객체 자체적인 비교가 가능해야 하고, 그러기 위해서 값 객체 내부적으로 속성들을 비교할 수 있는 메서드를 제공해야 한다. 이렇게 비교에 대한 메서드를 값 객체 내부에 캡슐화함으로써 속성이 추가되거나 제거 될 때 값 객체만 수정하면 되므로, 다른 부분에 영향을 최소화할 수 있다.
값 객체 선정 기준
값 객체로 정의해야 하는가를 고려할 때 생각해볼 점은 다음 2가지이다.
- 규칙이 존재하는가
- 낱개로 다루어야 하는가.
그리고 값 객체로 정의하기로 결정했다면, 그 다음은 내부 요소를 별개의 타입으로 나눌지를 결정해야 한다. 별도의 타입으로 다룰 필요가 없다면 하나의 타입으로 구성할 수도 있다. FullName 클래스에서 성, 이름에 대한 공통 타입을 구성하고 등가성을 비교하는 메서드를 추가하면 다음과 같다.
class FullName {
constructor(
private firstName: string,
private lastName: string,
) {}
getFirstName() {
return this.firstName;
}
getLastName() {
return this.lastName;
}
}
const fulName = new FullName("kim", "cheolsu");
const lastName = fulName.getLastName();
const firstName = fulName.getFirstName();
console.log(lastName); // cheolsu
console.log(firstName); // kim
행동의 정의된 값 객체
값 객체의 중요한 점 중 하나는 독자적인 행위를 정의할 수 있다는 점이다. 즉, 메서드를 가질 수 있다. 돈을 나타내는 객체를 예로 들어보자. 돈은 액수와 화폐단위, 두 가지 속성으로 값 객체를 정의할 수 있다. 또한, 돈은 덧셈이 가능하므로 덧셈에 대한 메서드도 구현할 수 있다. 이때 값 객체는 불변이므로 메서드에서 리턴값은 새로운 인스턴스여야 한다.
이처럼 값 객체는 데이터를 담는 것만이 목적인 구조체가 아니라, 데이터와 더불어 그 데이터에 대한 행동을 한곳에 모아둠으로써 자신만의 규칙을 갖는 도메인 객체다. 객체의 행동을 정의하면 이를 통해 객체가 어떤 일을 할 수 있는지를 알 수 있고, 또한 명시되지 않은 것은 가능하지 않다는 것도 알 수 있게 된다.
값 객체를 도입했을 때의 장점
- 표현력 증가
자기 정의를 통해 자신이 무엇인지에 대한 정보를 제공하는 자기 문서화를 돕는다. - 무결성 유지
유효하지 않은 값을 허용하는 경우 값을 사용할 때 항상 값이 유효한지 확인을 거쳐야 한다. 값 객체를 이용해서 유효하지 않은 값을 처음부터 방지할 수 있다. - 잘못된 대입 방지
값 객체를 정의해 타입 시스템에 의존하면 다른 타입의 대입을 방지할 수 있다. - 로직이 흩어지는 것을 방지
코드 중복을 방지할 수 있다.
참고자료
도메인 주도 설계 철저 입문, 위키북스
'Dev > DDD' 카테고리의 다른 글
| 유저 CRUD 코드 예시로 도메인 개념 살펴보기: 도메인 서비스, 리포지토리, 애플리케이션 서비스 (0) | 2025.01.22 |
|---|---|
| 엔티티 - 생애주기를 갖는 객체 (0) | 2025.01.20 |
| <도서관 시스템> 예시로 도메인 다이어그램 이해하기 (2) | 2024.12.10 |
| [객체지향의 오해와 사실] 객체 지향의 개념 이해하기 (0) | 2024.10.19 |
| plant UML 을 사용해서 스퀀스 다이어그램 작성하기 (0) | 2023.12.16 |