DDD 를 공부한 내용을 기록하기 위한 포스팅 입니다.
출처 : 도메인 주도 개발 시작하기 - DDD 핵심 개념 정리부터 구현까지 (저자. 최범균)
1. DDD 란?
DDD 는 Domain Driven Design 의 약자로, 소프트웨어를 개발함에 있어 도메인을 중심으로 모델링하고 개발하는 개발 기법을 말합니다.
1-1. 도메인 (Domain)
도메인은 "소프트웨어를 통해 문제를 해결하고자 하는 업무영역" 을 의미합니다.
우리가 일상생활에서 사용하는 서비스들(ex. 네이버, 쿠팡, 카카오 등등) 도 하나의 거대한 도메인이라고 할 수 있습니다.
하지만, 도메인을 정의하기 용이하게 하기 위해 너무 거대한 영역의 경우 작은 여러 단위의 하위 도메인을 묶어 큰 도메인을 구성하게 됩니다. 아래에는 여러 하위 도메인이 쇼핑몰이라는 하나의 상위 도메인을 구성하는 예시입니다.
1-2. 도메인 모델 & 도메인 모델 패턴
도메인 모델 이란, "특정 도메인을 개념적으로 표현한 것" 이라고 정의합니다. 해당 용어에 대해서는 다양한 정의가 존재할 수 있다고 하는데, 공통된 목표는 "개발자, 도메인 전문가, 그 외 관련자들이 도메인을 이해하기 위한 수단" 이라는 것입니다.
여기서 중요한 것은 도메인들 간 용어의 혼선이 있을 수 있기 때문에 너무 큰 도메인을 모델링하기 보단, 작은 단위의 도메인을 따로 모델링 하는 것이 도메인을 이해하기 더 수월하다는 점입니다.
예를 들면, 상품 도메인에서의 상품은 소프트웨어 상에서 상품 데이터를 의미하지만, 배송 도메인에서의 상품은 실제 고객에게 배송되는 물리적인 물건을 의미하기 때문에 이런 경우 관련자에 따라 용어를 혼동할 수 있습니다. 이럴 경우 용어 간의 혼동을 최소화하기 위해 상품도메인과 배송도메인의 도메인 모델을 따로 표현하는 것이 좋습니다.
위 목적을 가지고 표현된 도메인 모델에는 여러 종류가 있을 수 있는데, 공통적으로 "도메인이 가지고 있는 규칙" 을 포함하고 있습니다.
예를 들면, 주문 도메인에 대해 아래와 같은 규칙이 있다고 가정해봅시다.
- 상품이 출고되기 전에는 주문을 취소할 수 있다.
해당 기능을 코드로 구현할 때, 전 직장에서는 아래처럼 코드를 구현해왔습니다.
public class OrderService{
public void cancel(Long orderId) {
Order order = findById(orderId);
if(order.getOrderStatus != READY) { // READY : 상품 준비중
throw new IllegalStateException("상품 준비 중인 주문만 취소할 수 있습니다.");
}
order.setOrderStatus(CANCEL);
}
}
위 코드에는 여러 문제가 있지만, 이번 포스팅의 주제인 DDD 에 맞게 DDD 관점에서 이야기를 해보면 도메인이 가지는 규칙을 도메인 내에 구현하도록 하고 있습니다.
즉, 주문 취소에 대한 구현을 주문 도메인 내에서 하도록 하는 것이 DDD의 기본적인 개발 방법이라고 할 수 있습니다.
public class OrderService{
public void cancel(Long orderId) {
Order order = findById(orderId);
order.cancel();
}
}
public class Order {
// ... 중략
public void cancel() {
if(!isReady()) {
throw new IllegalStateException("상품 준비 중인 주문만 취소할 수 있습니다.");
}
this.orderStatus = CANCEL;
}
}
이 처럼 도메인 내에서 객체지향기법을 통해 소프트웨어를 개발하는 패턴을 도메인 모델 패턴 이라고 합니다.
2. 엔티티 vs 밸류
도메인을 표현하는 모델에는 크게 엔티티(Entity) 와 밸류(Value) 로 나눌 수 있습니다.
두 개념의 차이를 명확히 알고 있어야 도메인 개발에 적절히 사용할 수 있습니다.
1. 엔티티(Entity)
엔티티의 가장 큰 특징은 객체 내에 식별자를 가진다는 것입니다. 즉, 객체를 유일하게 식별하는 값이 객체 내에 존재한다는 것입니다.
식별자는 기본적으로 중복이 없어야 하기 때문에 아래와 같은 방법으로 생성할 수 있습니다.
- UUID
- 특정 규칙에 의한 생성
- 직접 입력(중복체크 필수)
- DB 내 Sequence 혹은 auto increment
물론 위 방법 외 에도 다른 방식이 있다면, 사용해도 무방하지만 가장 중요한 중복이 발생하면 안된다. 라는 성질은 있어야 합니다.
그리고 식별자는 한 번 부여되면 바뀌지 않아야 합니다. 즉, 두 객체가 같은 식별자를 가진다면, 두 객체는 논리적으로 같은 객체라고 간주하게 됩니다.
2. 밸류(Value)
밸류는 개념적으로 온전한 하나의 의미를 표현하는데 사용됩니다. 아래 예시로 설명해보겠습니다.
주문 정보에 배송정보가 포함되어 있다고 가정해보겠습니다.
public class Order {
// ... 중략
private String receiverName;
private String receiverPhoneNumber;
private String address;
private String detailAddress;
private String zipCode;
}
위 정보는 크게 1) 배송지 정보 와 2) 수령인 정보 로 구분될 수 있습니다.
배송지 정보를 의미하는 DeliveryInfo, 수령인 정보를 의미하는 Receiver 로 객체를 새로 생성하면 Order 객체는 아래 처럼 변경됩니다.
public class Order {
private Receiver receiver;
private DeliveryInfo deliveryInfo;
}
public class Receiver {
private String name;
private String phoneNumber;
}
public class DeliveryInfo {
private String address;
private String detailAddress;
private String zipCode;
}
이렇게 밸류 라는 개념을 도입해 객체를 분리하면 아래와 같은 이득을 얻을 수 있습니다.
- 코드에 의미를 부여할 수 있다.
- 각 밸류에 필요한 검증 혹은 기능을 수행할 수 있다.
코드에 의미를 부여함으로써 가독성을 높이고, 다른 사람이 코드를 이해하는데 도움을 줄 수 있습니다.
필요한 검증이나 기능을 객체 단위에서 실행할 수 있다는 장점은 밸류를 적용하기 전 상황을 생각해보면 엄청난 의미를 가져옵니다.
만약, Receiver 객체 내에 phoneNumber 라는 값에 대해 전화번호 유효성 검사를 하기 위해 정규식 검증 로직이 추가된다면,
밸류로 Receiver 를 분리하기 전에는 아래와 같이 Order 객체에 해당 로직이 들어가게 됩니다.
public class Order {
private String receiverPhoneNumber;
// ... 중략
public Order(...중략, String receiverPhoneNumber) {
// ... 중략
if(receiverPhoneNumber 의 정규식 검사 성공 여부){
throw new IllegalArgumentException("전화번호 형식이 맞지 않습니다.");
}
this.receiverPhoneNumber = receiverPhoneNumber;
}
}
위 코드의 경우, Order 라는 주문 을 의미하는 객체에서 수령인의 전화번호 형식을 검사하는 형태로 구성되어 있습니다.
사실 Order 객체는 주문 과 관련된 검증/기능만 있으면 된다고 생각할 수 있기 때문에 그 외 정보에 대한 검증/기능이 같은 객체 내에 위치하면 코드가 상당히 길어 질 수 있어 실질적으로 Order 의 중요 기능을 파악하는데 어려움이 있을 수 있다는 단점이 있습니다.
하지만, 이를 Receiver 라는 밸류로 분리한다면 아래와 같이 코드가 수정됩니다.
public class Order {
private Receiver recevier;
public Order(String receiverName, String phoneNumber) {
this.receiver = new Receiver(receiverName, phoneNumber);
}
}
public class Receiver {
private String name;
private String phoneNumber;
public Receiver(String name, String phoneNumber) {
if(phoneNumber 정규식 검사 성공 여부) {
throw new IllegalArgumentException("전화번호 형식이 아닙니다.");
}
this.name = name;
this.phoneNumber = phoneNumber;
}
}
이렇게 코드가 수정되면 Order 는 Receiver 에게 수령인 정보에 대한 유효성검증/기능을 위임할 수 있고, Order 는 주문에 대한 기능에 집중할 수 있어 코드가 비교적 간략해지는 효과를 가져올 수 있습니다.
밸류는 이 처럼 코드의 가독성을 높이고 코드에 의미를 부여하기 위해 사용되는 방식입니다.
'DDD&MSA' 카테고리의 다른 글
[DDD] 도메인 서비스(Domain Service) (0) | 2023.06.14 |
---|---|
[DDD] 응용 서비스 구현과 표현 계층 (0) | 2023.06.07 |
[DDD] 주문 애그리거트 구현하기(with JPA) (0) | 2023.06.03 |
[DDD] 애그리거트(Aggregate) 이해하기 (0) | 2023.06.02 |
[DDD] Domain 영역의 구성 요소 맛보기(with 애플리케이션 아키텍처) (0) | 2023.05.29 |
댓글