📘 1.1 도메인이란?
- 예를 들어, 온라인 서점 = “소프트웨어로 해결하고자 하는 문제 영역, 즉 도메인에 해당한다”
- 한 도메인은 다시 하위 도메인으로 나눌 수 있다.
- ex) 주문, 혜택, 회원, 결제, 배송 등
- 한 하위 도메인은 다른 하위 도메인과 연동하여 완전한 기능을 제공한다.
- ex) 고객이 물건을 구매하면 주문, 결제, 배송, 혜택 하위 도메인의 기능이 엮이게 된다.
- 소프트웨어가 도메인의 모든 기능을 제공하진 않는다.
- 특정 도메인을 위한 소프트웨어라고 해서 도메인이 제공해야 할 모든 기능을 직접 구현하는 것은 아니다. → 쇼핑몰 자체 시스템이 아닌 외부 배송 업체의 시스템을 사용하고 배송 추적 정보를 제공하는 필요한 기능만 일부 연동하는 방식
- 도메인마다 고정된 하위 도메인이 존재하는 것은 아니다.
- 하위 도메인을 어떻게 구성할지는 회사의 상황에 따라 다르다.
- 예를 들어, 작은 쇼핑몰의 경우 배송 추척 시스템은 사용하지 않을 것이다.
- 하위 도메인을 어떻게 구성할지는 회사의 상황에 따라 다르다.
- 한 도메인은 다시 하위 도메인으로 나눌 수 있다.
📘 1.2 도메인 전문가와 개발자 간 지식 공유
- 요구사항을 올바르게 이해하는 것은 중요하다
- 회사에는 홍보, 정산 배송과 같은 각 영역의 전문가들이 있는데 개발자에게 해당 도메인에 대한 지식과 경험을 바탕으로 원하는 기능 개발을 요구한다.
- 이때, 개발자는 이러한 요구사항을 분석하고 설계하여 코드를 작성하며 테스트하고 배포한다.
- 요구사항을 올바르게 이해하지 않으면 엉뚱한 기능을 만들게 되며 이를 수정하는 데에는 많은 비용이 발생하므로 처음부터 요구사항을 올바르게 이해하는 것은 중요하다.
- 요구사항을 올바르게 이해하는 방법
-
- 개발자와 해당 도메인의 전문가와 직접 대화
-
- 개발자도 도메인 지식을 갖춰야 한다.
-
📘 1.3 도메인 모델
- 도메인 모델에는 다양한 정의가 존재함
- 기본적으로 도메인 모델은 특정 도메인을 개념적으로 표현한 것이다.
- 객체 모델
- 객체의 기능과 데이터를 함께 보여줌
- 도메인이 제공하는 기능, 도메인의 주요 데이터 구성을 파악 가능
- 장점 = 도메인의 모든 내용을 담고 있지는 않지만 이 모델이 여러 관계자들이 동일한 모습으로 도메임을 이해하고 도메인 지식을 공유하는 데 도움이 된다.
- 상태 다이어그램을 통해 주문 상태 모델링 가능
- 계산 규칙이 중요하다면 수학 공식을 활용한 도메인 모델을 만들 수 있다.
- 객체 모델
- 도메인 모델은 기본적으로 도메인 자체를 이해하기 위한 개념 모델
- 코드를 작성하기 위해서는 개념 모델뿐만 아니라 구현 기술에 맞는 구현 모델이 따로 필요하다.
- 이때, 구현 모델이 개념 모델을 최대한 따르도록 할 수는 있다.
[개념 모델과 구현 모델]
- 개념 모델 = 순수하게 문제를 분석한 결과물⇒ 실제 코드를 작성할 때 개념 모델을 있는 그대로 사용할 수 없다.
- 그래서 개념 모델을 구현 가능한 형태의 모델로 전환하는 과정을 거치게 된다.
- ⇒ 데이터베이스, 트랜잭션 처리, 성능, 구현 기술과 같은 것 고려 ❌
- 처음부터 완벽한 개념 모델을 만들기보다는 전반적인 개요를 알 수 있는 수준으로 개념 모델을 작성해야 한다.
- 처음부터 완벽하게 도메인을 표현하는 모델을 만드는 것은 거의 불가능
- 프로젝트 초기에는 개요 수준의 개념 모델로 도메인에 대한 전체 윤곽을 이해하는 데 집중하고, 구현하는 과정에서 개념 모델을 구현 모델로 점진적으로 발전시켜 나가야 한다.
[참고 - 하위 도메인과 모델]
- 모델의 각 구성 요소는 특정 도메인으로 한정할 때 비로소 의미가 완전해진다.
- 배송에서의 상품과 카탈로그에서의 상품은 의미가 다르다.
- → 도메인에 따라 용어 의미가 결정된다.
- 같은 상품이라고 해서 하나의 다이어그램에 함께 표시하면 안 된다.
- 배송에서의 상품과 카탈로그에서의 상품은 의미가 다르다.
📘 1.4 도메인 모델 패턴
- 일반적인 애플리케이션 아키텍쳐 (마틴 파울러의 “엔터프라이즈 애플리케이션 아키텍처 패턴”의 도메인 모델 패턴)
- 도메인 모델 = 아키텍처 상의 도메인 계층을 **객체 지향 기법**으로 구현하는 패턴
- 참고 : 도메인 모델은 도메인 자체를 표현하는 개념적 모델을 의미하지만 객체 모델을 언급할 때에도 사용한다. (즉, 도메인 계층의 객체 모델을 표현할 때도 표현한다)
- 도메인 모델 = 아키텍처 상의 도메인 계층을 **객체 지향 기법**으로 구현하는 패턴
💡 user → 표현 → 응용 → 도메인 → 인프라스트럭처 → DB
- 사용자 인터페이스(또는 표현)
- 사용자(사람 or 외부 시스템)의 요청을 처리하고 사용자에게 정보를 보여준다.
- 응용
- 사용자가 요청한 기능을 실행한다. 업무 로직을 직접 구현하지 않으며 도메인 계층을 조합해서 기능을 실행한다.
- 도메인
- 시스템이 제공할 도메인 규칙을 구현한다.
- 인프라스트럭쳐
- 데이터베이스나 메시징 시스템과 같은 외부 시스템과의 연동을 처리한다.
도메인 계층은 도메인의 핵심 규칙을 구현
💡 배송지 변경 가능 여부를 판단하는 기능과 같이 중요 업무 규칙을 주문 도메인 모델인 Order나 OrderState에서 구현한다는 점이다.
=> 핵심 규칙을 구현한 코드는 도메인 모델에만 위치하기 때문에 규칙이 바뀌거나 규칙을 확장해야할 때 다른 코드에 영향을 덜 주고 변경 내역을 모델에 반영할 수 있게 된다
- ex) 주문 도메인의 경우
- 규칙 1 : 출고 전에 배송지를 변경할 수 있다.
- 규칙 2 : 주문 취소는 배송 전에만 할 수 있다.
- 배송지 변경이 가능한지 판단할 규칙이 주문 상태만 사용한다면 해당 방법(Order state 만)으로는 배송지 변경 가능 여부를 판단
[OrderState + 다른 정보를 반영하여 배송지 변경 여부를 판단하는 경우]package com.efub.dddstudy.Chap1_도메인모델시작하기; /*핵심 = 배송지 변경 가능 여부를 판단하는 기능과 같이 중요 업무 규칙을 주문 도메인 모델인 Order나 OrderState에서 구현한다는 점이다. => 핵심 규칙을 구현한 코드는 도메인 모델에만 위치하기 때문에 규칙이 바뀌거나 규칙을 확장해야할 때 다른 코드에 영향을 덜 주고 변경 내역을 모델에 반영할 수 있게 된다. */ public class Order_주문상태만으로_배송지변경가능여부판단 { /*배송지 변경이 가능한지 판단할 규칙이 주문 상태만 사용한다면 해당 방법(Order state 만)으로는 배송지 변경 가능 여부를 판단*/ 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; } } }
- 배송지 변경이 가능한지 판단할 규칙이 주문 상태와 다른 정보를 함께 사용한다면 해당 방법(Order state 만)으로는 배송지 변경 가능 여부를 판단할 수 없으므로 Order에서 로직을 구현해야 한다.
package com.efub.dddstudy.Chap1_도메인모델시작하기; 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; } }
📘 1.5 도메인 모델 도출
- 도메인 초안을 만들어야 비로소 코드를 작성할 수 있다.
- 도메인을 모델링할 때 기본이 되는 작업
- 모델을 구성하는 핵심 구성 요소, 규칙, 기능을 찾는 것이다.
[주문 도메인과 관련된 몇 가지 요구사항을 보자]
최소 한 종류 이상의 상품을 주문해야 한다.
한 상품을 한 개 이상 주문할 수 있다.
총 주문 금액은 각 상품의 구매 가격 합을 모두 더한 금액이다.
각 상품의 구매 가격 합은 상품 가격에 구매 개수를 곱한 값이다.
주문할 때 배송지 정보를 반드시 지정해야 한다.
배송지 정보는 받는 사람 이름, 전화번호, 주소로 구성된다.
출고를 하면 배송지를 입력할 수 없다.
출고 전에 주문을 취소할 수 있다.
고객이 결제를 완료하기 전에는 상품을 준비하지 않는다.
✅ Order 클래스
⇒ 이 요구 사항에서 알 수 있는 것은 주문 도메인은 다음과 같은 기능을 제공한다는 것이다.
- 출고 상태로 변경하기
- 배송지 정보 변경하기
- 주문 취소하기
- 결제 완료하기
- Order에 관련 기능을 메서드로 추가할 수 있다.
package com.efub.dddstudy.Chap1_도메인모델시작하기;
public class Order {
public void changeShipped(){}
public void changeShippingInfo(){}
public void cancel(){}
public void completePayment(){}
}
✅ OrderLine 클래스
⇒ 요구 사항에 따라 주문 항목이 어떤 데이터로 구성되는지 알려준다.
- 한 상품을 한 개 이상 주문할 수 있다.
- 각 상품의 구매 가격 합은 상품 가격에 구매 개수를 곱한 값이다.
- OrderLine에 주문할 상품, 상품의 가격, 구매 개수를 포함해야 한다. + 각 구매 항목의 구매 가격도 제공
package com.efub.dddstudy.Chap1_도메인모델시작하기;
public class OrderLine {
private Product product;// 한 상품을
private int price;//얼마에
private int quantity;// 몇 개 살지 담고 있다.
private int amounts;
public OrderLine(Product product, int price, int quantity, int amounts) {
this.product = product;
this.price = price;
this.quantity = quantity;
this.amounts = amounts;
}
private int calculateAmounts(){// 구매 가격을 계산하는 로직
return price * quantity;
}
public int getAmounts(){
return amounts;
}
public class Product{
}
}
✅ Order와 OrderLine과의 관계
- Order와 OrderLine과의 관계를 알려주는 요구사항
- 최소 한 종류 이상의 상품을 주문해야 한다.
- → Order은 최소 한 개 이상의 OrderLine을 포함해야 한다.
- 총 주문 금액은 각 상품의 구매 가격 합을 모두 더한 금액이다.
- OrderLine에서 총 주문 금액을 구할 수 있다.
package com.efub.dddstudy.Chap1_도메인모델시작하기; import java.util.List; public class Order { private List<OrderLine> orderLines; // Order은 한 개 이상의 orderLine을 가질 수 있으므로 Order을 생성할 때 OrderLine를 리스트로 전달한다. private ShippingInfo shippingInfo; private Money totalAmounts; public Order(List<OrderLine> orderLines, ShippingInfo shippingInfo) { setOrderLines(orderLines); setShippingInfo(shippingInfo);// 주문할 때 배송지 정보를 반드시 지정해야 한다는 내용 -> Order 을 생성할 때 해당 정보도 함께 전달해야 함 } private void setOrderLines(List<OrderLine> orderLines){ //요구 사항에 정의한 제약 조건을 검사한다. verifyAtLeastOneOrMoreOrderLines(orderLines);//요구사항에서 최소 한 종류 이상의 상품을 주문해야 하므로 이 조건을 검사한다. this.orderLines = orderLines; calculateTotalAmounts(); } public void setShippingInfo(ShippingInfo shippingInfo){ if(shippingInfo == null){ throw new IllegalArgumentException("no ShippingInfo"); //shippingInfo가 null이면 익셉션이 발생하는데 이렇게 하여 배송지 정보 필수라는 도메인 규칙을 구현한다. } this.shippingInfo = shippingInfo; } private void verifyAtLeastOneOrMoreOrderLines(List<OrderLine> orderLines){ if(orderLines == null || orderLines.isEmpty()){ throw new IllegalArgumentException("no OrderLine"); } } private void calculateTotalAmounts()// 총 주문 금액 계산 { int sum = orderLines.stream().mapToInt(x -> x.getAmounts()).sum(); this.totalAmounts = new Money(sum); } public class Money{ private int sum; public Money(int sum) { this.sum = sum; } } public void changeShipped(){} public void changeShippingInfo(){} public void cancel(){} public void completePayment(){} public enum OrderState { PAYMENT_WAITING, PREPARING, SHIPPED, DELIVERING, DELIVERY_COMPLETED, CANCELED; } }
package com.efub.dddstudy.Chap1_도메인모델시작하기; public class ShippingInfo { private String receiverName; private String receiverPhoneNumber; private String shippingAddress1; private String shippingAddress2; private String shippingZipcode; }
- 최소 한 종류 이상의 상품을 주문해야 한다.
✅ 도메인 상태에 따라
- 요구사항
- 출고를 하면 배송지 정보를 변경할 수 있다.
- 출고 전에 주문을 취소할 수 있다.
- 고객이 결제를 완료하기 전에는 상품을 준비하지 않는다.
- 특정 조건아니 상태에 따라 규칙이 달리 적용되는 경우가 있으므로 상태 정보를 표현한다.
- → 주문 상태를 표현한다.
public enum OrderState { //주문 상태를 표현한다.-> 특정 조건아니 상태에 따라 규칙이 달리 적용되는 경우가 있으므로 상태 정보를 표현한다.
PAYMENT_WAITING,
PREPARING,
SHIPPED,
DELIVERING,
DELIVERY_COMPLETED,
CANCELED;
}
public void changeShippingInfo(ShippingInfo newShoppingInfo){
verifyNotYetShipping();// 배송지 변겅은 출고 전에만 가능하다는 조건을 만족시키기 위해
setShippingInfo(newShoppingInfo);
}
public void cancel(){
verifyNotYetShipping();//주문 취소는 출고 전에만 가능하기 때문에 조건을 체크한다.
this.state = OrderState.CANCELED;
}
private void verifyNotYetShipping()//조건 체크, 도메인이 "출고 전에 가능하다"는 조건을 명확하게 가지고 있으므로 함수명을 다음과 같이 함
{
if(state != OrderState.PAYMENT_WAITING && state != OrderState.PREPARING)
{
throw new IllegalStateException("already shipped");
}
}
이 글은 도메인 주도 개발 시작하기 : DDD 핵심 개념 정리부터 구현까지" 책을 읽고 정리한 글입니다.
'책 > 도메인 주도 개발 시작하기 : DDD 핵심 개념 정리부터 구현까지' 카테고리의 다른 글
Chap 7. 도메인 서비스 (0) | 2023.04.09 |
---|