이 포스트는 📔 도메인 주도 개발 시작하기 책을 읽고 공부한 내용을 정리한 포스트입니다.
도메인은 소프트웨어로 해결하고자 하는 문제 영역을 이야기한다. 즉, 실제 세계에서의 문제를 소프트웨어로 해결하고자 한다면 그 문제는 하나의 도메인으로 바라볼 수 있다.
그렇다면 복잡한 문제가 존재한다고 했을때 이를 하나의 거대한 도메인으로 바라봐야 할까?
정답은 X이다. 하나의 도메인은 하위 도메인으로 분리할 수 있는 가능성을 갖고 있어 필요시에는 여러가지 도메인으로 분리하여 구성할 수 있다.
예를 들어 ATM에서 계좌 관리를 도메인으로 바라본다면 아래와 같은 하위 도메인으로 나눌 수 있을 것이다.
- 현금 인출
- 입금
- 이체
- 잔액 조회
- ...
이처럼 나누어진 하위 도메인은 모두 구현하는 것이 아닌 외부의 필요한 기능의 연동을 통해서 해결할 수 있다. 즉 상황에 따라서 구현이 필요한 도메인은 구현하고 외부 기능 연동등을 통해 구현 가능한 내용은 이를 차용해 해결이 가능하다.
그렇다면 어떻게 개발자는 도메인을 잘 해쳐나갈 수 있을까? 먼저 도메인 전문가가 도메인에 대한 개발 요청을 개발자에게 전달한다. 개발자는 전달받은 도메인에 대한 요구사항을 분석하고 이에 맞춰 개발, 테스트를 반복한다. 이를 완료하면 개발자는 도메인 전문가에게 결과를 배포한다.
이 과정에서 가장 중요한 포인트는 무엇일까? 바로 요구사항 분석하는 과정이다. 올바르게 요구사항을 이해하지 않고 개발을 하게 된다면 쓸모없거나 유용함이 떨어지는 시스템을 만들기 때문이다.
요구사항을 분석하는 가장 좋은 방법은 도메인 전문가와 개발자 간의 지식 공유가 직접적이고 원활하게 이루어저야 하며, 개발자도 도메인 지식을 어느정도는 갖춰야 한다는 점이다. 같은 지식을 공유하고 직접 소통할수록 요구사항에 대한 이해도는 자연스럽게 높아지기 때문이다.
그렇다면 개발자는 이러한 요구사항들을 분석해서 어떻게 표현할 수 있을까? 개발자들은 도메인 모델을 활용해서 이를 표현할 수 있다.
도메인 모델은 특정 도메인에 대한 개념적 표현을 이야기하며 객체 모델링 방식과 다이어그램 방식 등 다양한 표현 방식으로 도메인 모델을 표현할 수 있다.
여기서 도메인 모델을 표현할 때 꼭 UML 표기법을 사용해야 한다는 것은 아니다. 예를 들어 관계가 중요한 도메인이 존재한다면 그래프를 활용해서 도메인을 표현할 수 있고, 계산 규칙이 중요하다면 수학 공식을 활용해서 도메인 모델을 만들 수 있다.
즉 도메인 모델은 도메인을 이해하는데 도움이 된다면 자유롭게 표현할 수 있다.
여기서 주의해야할 점은 도메인 모델은 개념 모델이라는 점이다. 개념 모델은 이해를 돕는 개념을 설명하기 위한 모델로서 구현 기술에 맞는 구현 모델이 따로 필요하다. 구현 모델을 최대한 개념 모델을 따라서 만들수 있지만, 도메인 모델은 개념 모델로서의 역할을 위해 작성하지, 구현 모델로서의 역할로써 사용하는 것이 아니라는 점을 상기하고 있어야 한다.
- 개념 모델: 순수하게 문제를 분석한 결과물
- 구현 모델: 데이터베이스, 트랜잭션 처리, 성능, 구현 기술등을 고려하여 작성한 결과물
애플리케이션의 아키텍처를 그림으로 표현하면 아래와 같다.
각 영역의 역할은 다음과 같다.
여기서 중요한 포인트는 도메인 계층에서 도메인의 핵심 규칙을 모두 구현하고
응용 계층에서는 도메인의 조합을 활용하여 사용자 요청 기능을 실행한다는 점이다.
핵심 규칙을 구현한 코드는 도메인 모델에만 위치하므로 규칙이 바뀌거나 규칙을 확장해야 할 때 도메인 모델에 내역을 반영함으로써 다른 코드에 영향을 덜 끼칠수 있기 때문이다.
이제 도메인 모델을 작성해보고자 한다. 도메인을 모델링할때 가장 기본이 되는 작업은 모델을 구성하는 핵심 구성요소, 규칙, 기능을 찾는 것이다. 이 과정은 요구사항을 분석하는 과정에서부터 시작된다.
요구사항에 대한 분석이 완료되었다면 요구사항을 하나씩 메소드화 함으로써 점진적으로 도메인 모델을 구성해 나갈 수 있다.
예를 들어 ATM의 계좌 관리중 입금과 관련된 몇 가지 요구사항을 살펴보면
- 최소 1원 이상의 돈을 입금해야 한다.
- 한번의 입금 과정은 하나의 계좌로만 가능하다.
- 입금 과정에서 계좌를 지정해야 한다.
- 입금이 완료되면 취소할 수 없다.
- 입금이 완료되면 계좌에 입금액이 반영되야 한다.
이를 코드로 구성하면 아래와 같다.
public class Deposit {
private Account account;
private Money money;
public Deposit(Account account, Money money) {
this.account = account;
this.money = money;
}
public void deposit() {
verifyMoney(money);
account.add(money);
}
private veriftMoney(Money money) {
if (money.getValue() <= 0) {
throw new IllegalArgumentException("입금액은 0보다 작을 수 없습니다.");
}
}
}
위와 같이 요구사항을 활용하여 도메인 모델을 점진적으로 확장하는 방식으로 도메인 모델을 구현할 수 있다.
위에서 요구사항을 활용해서 구현한 도메인 모델은 크게 엔티티(Entity)와 밸류(Value)로 구분할 수 있다.
엔티티는 고유하고 불변한 식별자를 갖는다.
이를 활용하여 식별자가 같다면 엔티티가 같다고 판단할수 있기에 식별자를 활용하여 equals()
와 hashCode()
메서드를 구현한다.
고유하고 불변한 식별자를 생성하는 방식은 다음 중 한가지를 선택한다.
- 특정 규칙에 따라 생성
- UUID나 Nano ID와 같은 고유 식별자 생성기 사용
- 값을 직접 입력
- 일련번호 사용 (시퀸스나 DB의 자동 증가 칼럼 사용)
밸류 타입은 개념적으로 완전한 하나의 값을 표현할 때 사용한다. 예를 들어 위의 요구사항 구현에서 Money class 코드는 아래와 같다.
@Getter
public class Money {
private int value;
private String unit;
public Money(int money) {
this.value = money;
this.unit = "원";
}
public Money(int money, String unit) {
this.value = money;
this.unit = unit;
}
}
Money
Class는 '돈'이라는 도메인 개념을 포함한다. 밸류 타입을 사용함으로써 돈의 개념을 표현할 수 있는 것이다.
밸류 타입의 객체는 불변하게 구현하는 경우가 많기에 자연스럽게 setter 메서드를 구현하지 않는다.
코드를 구현하는 과정에서 도메인에서 사용하는 용어를 반영하여 구현해야 개발자가 코드를 해석하는 과정에서 부담을 덜 수 있다.
위 과정을 유비쿼터스 언어를 활용한다고 표현할 수 있는데, 이는 전문가, 관계자, 개발자가 도메인과 관련된 공통의 언어를 만들고 이를 대화, 문서, 도메인 모델, 코드, 테스트 등 모든 곳에서 같이 사용함으로써 불필요한 용어의 모호함을 줄이고 코드 해석 과정을 용이하게 한다.