본문은 [도메인 주도 개발 시작하기: DDD 핵심 개념 정리부터 구현까지]를 읽고 간단하게 정리한 글입니다. 필요에 따라 생략/수정된 부분이 있을 수 있으며, 내용이 추후 변경될 수 있습니다.
1.1 도메인이란?
- 도메인(domain)이란 소프트웨어로 해결하고자 하는 문제 영역에 해당한다
- 한 도메인은 다시 하위 도메인으로 나눌 수 있으며, 한 하위 도메인은 다른 하위 도메인과 연동하여 완전한 기능을 제공한다
- 예를 들어 고객이 물건을 구매하면 주문, 결제, 배송, 혜택 등 하위 도메인의 기능이 엮이게 된다
- 특정 도메인을 위한 소프트웨어라고 해서 도메인이 제공해야 할 모든 기능을 직접 구현하는 것은 아니다
- [그림 1.2]에서 배송 도메인은 외부 배송 업체의 시스템을 사용하고, 결제 도메인은 외부 PG사의 시스템을 사용한다.
- 도메인마다 고정된 하위 도메인이 존재하는 것은 아니며, 하위 도메인을 어떻게 구성할지 여부는 상황에 따라 달라진다
1.2 도메인 전문가와 개발자 간 지식 공유
- 개발자는 요구사항을 분석하고 설계하여 코드를 작성하며 테스트하고 배포한다
- 이때, 요구사항을 올바르게 이해하는 것이 중요하다
- 요구사항을 잘못 이해하면 수정해야 할 코드가 많아지고 제품을 만드는데 실패하거나 일정이 밀리기도 한다
- 요구상항을 올바르게 이해하기 위한 가장 쉬운 방법은 전문가와 직접 대화하는 것이다
- 중간에 전달자가 많으면 정보의 누락이나 왜곡이 발생할 여지가 크고, 이에 따라 잘못된 소프트웨어가 만들어질 수 있다
- 도메인 전문가 만큼은 아니더라도 이해관계자와 개발자도 도메인 지식을 갖춰야 한다
- 제품 개발과 관련된 사람들이 같은 지식을 공유하고 직접 소통할 수록 도메인 전문가가 원하는 제품을 만들 가능성이 커진다
1.3 도메인 모델
1) 도메인 모델 예시
- 도메인 모델에는 다양한 정의가 존재하는데, 기본적으로 도메인 모델은 특정 도메인을 개념적으로 표현한 것이다
- 도메인 모델을 사용하면 여러 관계자들이 동일한 모습으로 도메인을 이해하고 도메인 지식을 공유하는 데 도움이 된다
- [그림 1.3]의 모델은 도메인의 모든 내용을 담고 있지 않지만, 해당 모델을 통해 도메인이 지니고 있는 특성과 기능을 파악할 수 있다
- 도메인을 이해하려면 도메인이 제공하는 기능과 도메인의 주요 데이터 구성을 파악해야 한다
- 따라서 기능과 데이터를 함께 보여주는 객체 모델은 도메인을 모델링하기에 적합하다
- 도메인 모델을 객체로만 모델링 할 수 있는 것은 아니다
- [그림 1.4]와 같이 상태 다이어그램을 이용해서 주문의 상태 전이를 모델링할 수 있다
- 도메인을 이해하는 데 도움이 된다면 표현 방식이 무엇인지는 중요하지 않다
- 꼭 클래스 다이어그램이나 상태 다이어그램과 같은 UML 표기법만 사용해야 하는 것은 아니며, 그래프나 수학 공식 등 다양한 방식으로 도메인 모델을 만들 수 있다
- 도메인 모델은 기본적으로 도메인 자체를 이해하기 위한 개념 모델이다
- 따라서 코드를 작성하기 위해선 구현 기술에 맞는 구현 모델이 별도로 필요하다
- 개념 모델과 구현 모델은 서로 다른 것이지만 구현 모델이 개념 모델을 최대한 따르도록 할 수 있다
- 객체 기반 모델 -> 객체 지향 언어를 이용한 구현 모델
- 수학적인 모델 -> 함수를 이용한 구현 모델
2) 하위 도메인과 모델
- 각 하위 도메인이 다루는 영역은 서로 다르므로 같은 용어라도 하위 도메인마다 의미가 달라질 수 있다
- 카탈로그 도메인의 상품은 상품 가격, 상세 내용을 담고 있는 정보를 의미하는 반면 배송 도메인의 상품은 고객에게 실제 배송되는 물리적인 상품을 의미한다
- 도메인에 따라 용어 의미가 결정되므로 여러 하위 도메인을 하나의 다이어그램에 모델링하면 안 된다
- 모델의 각 구성요소는 특정 도메인으로 한정할 때 비로소 의미가 완전해지므로 각 하위 도메인마다 별도로 모델을 만들어야 한다
1.4 도메인 모델 패턴
1) 아키텍처 구성
- 일반적으로 애플리케이션의 아키텍처는 [그림 1.5]와 같이 네 개의 영역으로 구성되며, 각 영역의 역할은 [표 1.1]과 같다
- 앞서 살펴본 도메인 모델이 도메인 자체를 이해하는 데 필요한 개념 모델을 의미한다면, 지금 살펴볼 도메인 모델은 마틴 파일러가 쓴 [엔터프라이즈 애플리케이션 아키텍처 패턴] 책의 도메인 모델 패턴을 의미한다
- 도메인 모델 패턴은 아키텍처 상의 도메인 계층을 객체 지향 기법으로 구현하는 패턴이다
2) 도메인 모델 패턴 예제
- 도메인 계층의 도메인의 핵심 규칙을 구현한다
- 주문 도메인의 경우 '출고 전에 배송지를 변경할 수 있다'라는 규칙과 '주문 취소는 배송 전에만 할 수 있다'라는 규칙을 구현한 코드가 도메인 계층에 위치하게 된다
- 이런 도메인 규칙을 객체 지향 기법으로 구현하는 패턴이 도메인 모델 패턴이다
배송지 정보 변경 가능 여부를 OrderState에서 판단
public class Order {
private OrderState state;
private ShippingInfo shippinginfo;
public void changeShippingInfo(ShippingInfo newShippingInfo) {
if (!state.isShippingChangeable()) {
throw new IllegalStateException("can't change shipping in " + state);
}
this.shippinginfo = newShippingInfo;
}
// ...
}
public enum OrderState {
PAYMENT_WAITING {
public boolean isShippingChangeable() {
return true;
}
},
PREPARING {
public boolean isShippingChangeable() {
return true;
}
},
SHIPPED, DELIVERING, DELIVERY_COMPLETED;
public boolean isShippingChangeable() {
return false;
}
}
- 위 코드는 주문 도메인의 일부 기능을 도메인 모델 패턴으로 구현했다
- OrderState에는 주문 대기 중이거나 상품 준비 중에는 배송지를 변경할 수 있다는 도메인 규칙을 구현하고 있다
- 큰 틀에서 보면 OrderState는 Order에 속한 데이터이므로 배송지 정보 변경 가능 여부를 판단하는 코드를 Order로 이동할 수도 있다
배송지 정보 변경 가능 여부를 Order에서 판단
public class Order {
private OrderState state;
private ShippingInfo shippinginfo;
public void changeShippingInfo(ShippingInfo newShippingInfo) {
if (!isShippingChangeable()) {
throw new IllegalStateException("can't change shipping in " + state);
}
this.shippinginfo = newShippingInfo;
}
private boolean isShippingChangeable() {
return state == OrderState.PAYMENT_WAITING || state == OrderState.PREPARING;
}
// ...
}
public enum OrderState {
PAYMENT_WAITING, PREPARING, SHIPPED, DELIVERING, DELIVERY_COMPLETED;
}
- 배송지 정보 변경 가능 여부를 Order에서 판단하도록 수정했다
- 배송지 변경이 가능한지를 판단할 규칙이 주문 상태와 다른 정보를 함께 사용한다면 OrderState만으로는 배송지 변경 가능 여부를 판단할 수 없으므로 Order에서 로직을 구현해야 한다
- 중요한 점은 주문과 관련된 중요 업무 규칙을 주문 도메인 모델인 Order나 OrderState에서 구현한다는 점이다
- 핵심 규칙을 구현한 코드는 도메인 모델에만 위치하므로 규칙이 바뀌거나 규칙을 확장해야 할 때 다른 코드에 영향을 덜 주고 변경 내역을 모델에 반영할 수 있게 된다
'도메인 모델'이라는 용어는 도메인 자체를 의미하는 개념적인 모델을 의미하지만, 도메인 계층을 구현할 때 사용하는 객체 모델을 언급할 때에도 '도메인 모델'이라는 용어를 사용한다. 이 책에서도 도메인 계층의 객체 모델을 표현할 때 도메인 모델이라고 표현하고 있다.
3) 개념 모델과 구현 모델
- 개념 모델은 순수하게 문제를 분석한 결과물이다
- DB, 트랜잭션 처리, 성능, 구현과 같은 기술을 고려 X
- 따라서 실제 코드를 작성할 땐 개념 모델을 있는 그대로 사용할 수 없음
- 개념 모델을 만들 때 처음부터 완벽하게 도메인을 표현하는 모델을 만드는 것은 불가능에 가깝다
- 프로젝트 초기에 완벽한 도메인을 만들더라도 결국 도메인에 대한 새로운 지식이 쌓이면서 모델을 보완하거나 변경하는 일이 발생함
- 따라서 처음부터 완벽한 개념 모델을 만들기보다는 전반적인 개요를 알 수 있는 수준으로 개념 모델을 작성해야 한다
1.5 도메인 모델 표출
- 구현을 시작하기 위해서는 도메인에 대한 초기 모델이 필요하다
- 도메인을 모델링할 때 기본이 되는 작업은 모델을 구성하는 핵심 구성요소, 규칙, 기능을 찾는 것이다
- 이 과정은 요구사항에서 출발한다
1) 도메인 모델 표출 예제
앞선 예제에 이어서 주문 도메인과 관련된 몇 가지 요구사항을 살펴보자
- 최소 한 종류 이상의 상품을 주문해야 한다
- 한 상품을 한 개 이상 주문할 수 있다
- 총 주문 금액은 각 상품의 구매 가격 합을 모두 더한 금액이다
- 각 상품의 구매 가격 합은 상품 가격에 구매 개수를 곱한 값이다
- 주문할 때 배송지 정보를 반드시 지정해야 한다
- 배송지 정보는 받는 사람 이름, 전화번호, 주소로 구성된다
- 출고를 하면 배송지를 변경할 수 있다
- 출고 전에 주문을 취소할 수 있다
- 고객이 결제를 완료하기 전에는 상품을 준비하지 않는다
위 요구사항에서 주문은 '출고 상태로 변경하기', '배송지 정보 변경하기', '주문 취소하기', '결제 완료하기' 기능을 제공한다는 사실을 도출할 수 있고, Order에 관련 기능을 메서드로 추가할 수 있다.
public class Order {
public void changeShipped() { ... }
public void changeShippingInfo(ShippingInfo newShippingInfo) { ... }
public void cancel() { ... }
public void completePayment() { ... }
}
다음 요구사항으로부터 주문 항목(OrderLine)이 주문할 상품, 상품의 가격, 구매 개수를 포함하는 동시에 각 구매 항목의 구매 가격도 제공해야 한다는 사실을 알 수 있다.
- 한 상품을 한 개 이상 주문할 수 있다
- 각 상품의 구매 가격 합은 상품 가격에 구매 개수를 곱한 값이다
public class OrderLine {
private Product product; // 주문할 상품
private int price; // 상품의 가격
private int quantity; // 구매 개수
private int amounts; // 구매 가격 합
public OrderLine(Product product, int price, int quantity) {
this.product = product;
this.price = price;
this.quantity = quantity;
this.amounts = calculateMounts();
}
private int calculateMounts() {
return price * quantity;
}
public int getAmounts() { ... }
// ...
}
2) 정리
- 내용이 많아 다른 예제는 생략했다
- 중요한 것은 요구사항을 분석함으로써 도메인 모델이 지니고 있어야 할 상태와 기능, 그리고 제약조건 등을 찾을 수 있다는 것이다
- 이와 같이 도메인 지식이 쌓여가면서 도메인 모델을 점진적으로 만들어 갈 수 있다
- 이렇게 만든 모델은 요구사항 정련을 위해 도메인 전문가나 다른 개발자 논의하는 과정에서 공유하기도 한다
1.6 엔티티와 밸류
- [그림 1.6]은 앞서 요구사항 분석 과정에서 만든 모델이며, 이 안에는 엔티티와 밸류가 모두 존재한다
- 도출한 모델은 크게 엔티티와 밸류로 구분할 수 있다
- 엔티티와 밸류를 제대로 구분해야 도메인을 올바르게 설계하고 구현할 수 있으므로 이 둘의 차이를 명확하게 이해해야만 도메인을 잘 구현할 수 있다
1) 엔티티
- 엔티티의 가장 큰 특징은 식별자를 가진다
- 식별자는 엔티티 객체마다 고유해서 각 엔티티는 서로 다른 식별자를 가진다
- 엔티티의 식별자는 바뀌지 않는다
- 엔티티의 식별자는 바뀌지 않고 고유하므로 두 엔티티의 식별자가 같으면 두 엔티티는 같다고 판단할 수 있다
- 이는 코드 상에서 equals() 메서드와 hashCode() 메서드로 구현될 수 있다
2) 엔티티의 식별자 생성
엔티티의 식별자를 생성하는 시점은 도메인의 특징과 사용하는 기술에 따라 달라지는데, 보통 다음 중 한 가지 방식으로 생성한다
- 특정 규칙에 따라 생성
- UUID나 Nano ID와 같은 고유 식별자 생성기 사용
- 값을 직접 입력
- 일련번호 사용(시퀀스나 DB의 자동 증가 칼럼 사용)
자동 증가 칼럼을 제외한 다른 방식은 다음과 같이 식별자를 먼저 만들고 엔티티 객체를 생성할 때 식별자를 전달한다.
// 엔티티를 생성하기 전에 식별자 생성
String orderNumber = orderRepository.generateOrderNumber();
Order order = new Order(orderNumber, ...);
orderRepository.save(order);
자동 증가 칼럼은 DB 테이블에 데이터를 삽입해야 비로소 값을 알 수 있으므로 테이블에 데이터를 추가하기 전까지는 식별자를 알 수 없다. 따라서 엔티티 객체를 생성할 때 식별자를 전달 할 수 없다.
Article article = new Article(author, title, ...);
articleRepository.save(article); // DB에 저장한 뒤 구한 식별자를 엔티티에 반영
Long savedArticleId = article.getId(); // DB에 저장한 후 식별자 참조 가능
3) 밸류 타입
- [그림 1.8]에서 '받는 사람'과 '주소'라는 개념을 표현하기 위해 각 필드들이 사용된다
- receiverName과 receiverPhoneNumber필드는 '받는 사람'이라는 하나의 개념을 표현한다
- shppingAddress1, shppingAddress2, shippingZipcode 필드는 '주소'라는 하나의 개념을 표현한다
- 밸류 타입은 이렇듯 개념적으로 완전한 하나를 표현할 때 사용한다
- 밸류 타입을 이용하여 받는 사람(Receiver)과 주소(Address)라는 개념을 표현할 수 있다
그림 1.8을 밸류 타입을 이용하여 수정한 코드
public class Receiver {
private String name;
private String phoneNumber;
public Receiver(String name, String phoneNumber) {
this.name = name;
this.phoneNumber = phoneNumber;
}
public String getName() {
return name;
}
public String getPhoneNumber() {
return phoneNumber;
}
}
public class Address {
private String address1;
private String address2;
private String zipcode;
public Address(String address1, String address2, String zipcode) {
this.address1 = address1;
this.address2 = address2;
this.zipcode = zipcode;
}
}
- 위의 코드는 도메인 개념과 관련된 데이터를 모아서 하나의 클래스 형태로 만든 것을 보여준다
- 이렇게 밸류 타입을 사용하면 개념적으로 완전한 하나를 잘 표현할 수 있다
- 밸류 타입을 사용함으로써 ShippingInfo의 가독성도 향상되었다
하나의 예제를 더 살펴보자. OrderLine은 int타입의 price와 amounts 필드를 사용하는데, 이를 밸류 타입인 Money타입으로 대체하여 의미를 보다 명확하게 표현하고 밸류 타입이 가지고 있는 기능을 이용할 수 있다.
public class OrderLine {
private Product product; // 주문할 상품
private int price; // 상품의 가격
private int quantity; // 구매 개수
private int amounts; // 구매 가격 합
...
}
public class OrderLine {
private Product product; // 주문할 상품
private Money price; // 상품의 가격
private int quantity; // 구매 개수
private Money amounts; // 구매 가격 합
}
public class Money {
private int value;
// 생성자, getter 생략...
// 돈 계산을 위한 기능이 추가됨
public Money add(Money money) {
return new Money(this.value + money.value); // 불변 보장
}
public Money multiply(int multiplier) {
return new Money(value * multiplier);
}
}
정리하자면 밸류타입은 아래와 같은 장점을 가진다고 할 수 있다
- 가독성이 향상된다
- 의미를 보다 명확하게 표현할 수 있다
- 밸류 타입을 위한 기능을 추가할 수 있다
4) 도메인 모델에 setter 넣지 않기
- 도메인 모델에 get/set 메서드를 무조건 추가하는 것은 좋지 않은 버릇이다
- setter는 도메인의 핵심 개념이나 의도를 코드에서 사라지게 한다
- 예를 들어 changeShippinngInfo()가 배송지 정보를 새로 변경하는 의미를 가졌다면 setShippingInfo() 메서드는 단순히 배송지 값을 설정한다는 것은 의미한다
- 이렇게 setter를 쓰면 단순히 상태 값만 변경할 것인지 혹은 다른 처리를 위한 코드를 함께 구현할 것인지 애매해진다
- set 메서드의 또 다른 문제는 도메인 객체가 생성될 때 온전하지 않은 상태가 될 수 있다는 것이다
- 불완전한 상태의 객체를 생성한 뒤 setter로 도메인 객체의 프로퍼티를 채우는 방식으로 코드를 구현할 수 있다
- 이런 경우 핵심 프로퍼티가 누락된 도메인 객체가 생성되어 문제를 일으킬 수 있다
- 도메인 객체가 불완전한 상태로 사용되는 것을 막기 위해선 객체의 생성 시점에 필요한 데이터를 전달하고, 이를 검증해야 한다
- 즉, 생성자를 통해 필요한 데이터를 모두 받고, 필요한 경우 생성자 내부에 검증 코드를 추가한다
- 불변으로 밸류 타입으로 구현하면 자연스럽게 setter를 구현하지 않을 수 있다
- setter를 구현해야 할 특별한 이유가 없다면 불변 타입의 장점을 살릴 수 있도록 밸류 타입은 불변으로 구현한다
5) DTO의 get/set 메서드
- DTO는 Data Transfer Object의 약자로 프레젠테이션 계층과 도메인 계층이 데이터를 서로 주고받을 때 사용하는 일종의 구조체이다
- DTO는 도메인 로직을 담고 있지 않으므로 get/set 메서드를 제공해도 도메인 객체의 데이터 일관성엔 영향을 줄 가능성이 높지 않다
- 하지만 DTO도 밸류 타입과 마찬가지로 setter를 열어주는 대신 생성 시점에 값이 주입되게 만듦으로써 불변 객체를 만드는 것이 좋다
1.7 도메인 용어와 유비쿼터스 언어
- 코드를 사용할 때 도메인에서 사용하는 용어는 매우 중요하다
- 도메인에서 사용하는 용어를 코드에 반영하지 않으면 개발자는 코드의 의미를 해석해야 하는 부담을 지우게 된다
- 예컨대 '결제 대기 중', '상품 준비 중', '출고 완료됨', '배송 중', '배송 완료됨', '주문 취소됨'이라는 주문 상태가 있는데 이를 코드 상에서 STEP1, STEP2, STEP3, STEP4, STEP5, STEP6이라는 Enum 상수로 표현되어있을 수 있다
- 이렇게 구현이 이루어져 있다면 개발자는 코드↔도메인 용어 해석 과정에서 어려움을 겪게 될 것이다
- 도메인 용어를 사용해서 PAYMENT_WATING, PREPARING, SHIPPED, DELIVERING, DELIVERY_COMPLETED라는 Enum 상수로 표현한다면 더욱 읽기 쉬운 코드가 될 것이다
- 에릭 에반스는 도메인 주도 설계에서 언어의 중요함을 강조하기 위해 유비쿼터스 언어(ubiquitous language)라는 용어를 사용했다
- 전문가, 관계자, 개발자가 도메인과 관련된 공통의 언어를 만들고 이를 대화, 문서, 도메인 모델, 코드, 테스트 등 모든 곳에서 같은 용어를 사용한다
- 이렇게 하면 용어의 모호함을 줄이고 불필요한 해석 과정을 줄일 수 있다
- 시간이 지나 도메인에 대한 이해가 높아지면 새롭게 이해된 내용을 잘 표현할 수 있는 용어를 찾아내고 이를 다시 공통의 언어로 만들어 함께 사용한다
- 새로 발견한 용어는 코드나 문서에도 반영해서 산출물에 최신 모델을 적용한다
- 영어로 코드를 작성해야 하는 환경 때문에 도메인 용어를 영어로 해석하는 노력이 필요하다
- 알맞은 영단어를 찾는 것은 쉽지 않은 일이지만 시간을 들여 찾는 노력을 해야 한다
- 도메인에 어울리지 않는 단어를 사용하면 코드는 도메인과 점점 멀어지게 된다
'책 > 도메인 주도 개발 시작하기: DDD 핵심 개념 정리부터 구현까지' 카테고리의 다른 글
[도메인 주도 개발 시작하기] 6장: 응용 서비스와 표현 영역 (0) | 2022.10.14 |
---|---|
[도메인 주도 개발 시작하기] 5장: 스프링 데이터 JPA를 이용한 조회 기능 (0) | 2022.10.07 |
[도메인 주도 개발 시작하기] 4장: 리포지터리와 모델 구현 (0) | 2022.10.03 |
[도메인 주도 개발 시작하기] 3장: 애그리거트 (1) | 2022.09.20 |
[도메인 주도 개발 시작하기] 2장: 아키텍처 개요 (0) | 2022.09.16 |