본문 바로가기

책/도메인 주도 개발 시작하기 : DDD 핵심 개념 정리부터 구현까지

Chap1 - 1. 도메인 모델 시작하기

📘 1.1 도메인이란?

  • 예를 들어, 온라인 서점 = “소프트웨어로 해결하고자 하는 문제 영역, 즉 도메인에 해당한다”
    • 한 도메인은 다시 하위 도메인으로 나눌 수 있다.
      • ex) 주문, 혜택, 회원, 결제, 배송 등
    • 한 하위 도메인은 다른 하위 도메인과 연동하여 완전한 기능을 제공한다.
      • ex) 고객이 물건을 구매하면 주문, 결제, 배송, 혜택 하위 도메인의 기능이 엮이게 된다.
    • 소프트웨어가 도메인의 모든 기능을 제공하진 않는다.
      • 특정 도메인을 위한 소프트웨어라고 해서 도메인이 제공해야 할 모든 기능을 직접 구현하는 것은 아니다. → 쇼핑몰 자체 시스템이 아닌 외부 배송 업체의 시스템을 사용하고 배송 추적 정보를 제공하는 필요한 기능만 일부 연동하는 방식
    • 도메인마다 고정된 하위 도메인이 존재하는 것은 아니다.
      • 하위 도메인을 어떻게 구성할지는 회사의 상황에 따라 다르다.
        • 예를 들어, 작은 쇼핑몰의 경우 배송 추척 시스템은 사용하지 않을 것이다.

📘 1.2 도메인 전문가와 개발자 간 지식 공유

  • 요구사항을 올바르게 이해하는 것은 중요하다
    • 회사에는 홍보, 정산 배송과 같은 각 영역의 전문가들이 있는데 개발자에게 해당 도메인에 대한 지식과 경험을 바탕으로 원하는 기능 개발을 요구한다.
    • 이때, 개발자는 이러한 요구사항을 분석하고 설계하여 코드를 작성하며 테스트하고 배포한다.
    • 요구사항을 올바르게 이해하지 않으면 엉뚱한 기능을 만들게 되며 이를 수정하는 데에는 많은 비용이 발생하므로 처음부터 요구사항을 올바르게 이해하는 것은 중요하다.
  • 요구사항을 올바르게 이해하는 방법
      1. 개발자와 해당 도메인의 전문가와 직접 대화
      1. 개발자도 도메인 지식을 갖춰야 한다.

📘 1.3 도메인 모델

  • 도메인 모델에는 다양한 정의가 존재함
  • 기본적으로 도메인 모델은 특정 도메인을 개념적으로 표현한 것이다.
    • 객체 모델
      • 객체의 기능과 데이터를 함께 보여줌
      • 도메인이 제공하는 기능, 도메인의 주요 데이터 구성을 파악 가능
      • 장점 = 도메인의 모든 내용을 담고 있지는 않지만 이 모델이 여러 관계자들이 동일한 모습으로 도메임을 이해하고 도메인 지식을 공유하는 데 도움이 된다.
    • 상태 다이어그램을 통해 주문 상태 모델링 가능
    • 계산 규칙이 중요하다면 수학 공식을 활용한 도메인 모델을 만들 수 있다.
  • 도메인 모델은 기본적으로 도메인 자체를 이해하기 위한 개념 모델
  • 코드를 작성하기 위해서는 개념 모델뿐만 아니라 구현 기술에 맞는 구현 모델이 따로 필요하다.
  • 이때, 구현 모델이 개념 모델을 최대한 따르도록 할 수는 있다.

[개념 모델과 구현 모델]

  • 개념 모델 = 순수하게 문제를 분석한 결과물⇒ 실제 코드를 작성할 때 개념 모델을 있는 그대로 사용할 수 없다.
  • 그래서 개념 모델을 구현 가능한 형태의 모델로 전환하는 과정을 거치게 된다.
  • ⇒ 데이터베이스, 트랜잭션 처리, 성능, 구현 기술과 같은 것 고려 ❌
  • 처음부터 완벽한 개념 모델을 만들기보다는 전반적인 개요를 알 수 있는 수준으로 개념 모델을 작성해야 한다.
    • 처음부터 완벽하게 도메인을 표현하는 모델을 만드는 것은 거의 불가능
    • 프로젝트 초기에는 개요 수준의 개념 모델로 도메인에 대한 전체 윤곽을 이해하는 데 집중하고, 구현하는 과정에서 개념 모델을 구현 모델로 점진적으로 발전시켜 나가야 한다.

[참고 - 하위 도메인과 모델]

  • 모델의 각 구성 요소는 특정 도메인으로 한정할 때 비로소 의미가 완전해진다.
    • 배송에서의 상품과 카탈로그에서의 상품은 의미가 다르다.
      • → 도메인에 따라 용어 의미가 결정된다.
      • 같은 상품이라고 해서 하나의 다이어그램에 함께 표시하면 안 된다.

📘 1.4 도메인 모델 패턴

  • 일반적인 애플리케이션 아키텍쳐 (마틴 파울러의 “엔터프라이즈 애플리케이션 아키텍처 패턴”의 도메인 모델 패턴)
    • 도메인 모델 = 아키텍처 상의 도메인 계층을 **객체 지향 기법**으로 구현하는 패턴
      • 참고 : 도메인 모델은 도메인 자체를 표현하는 개념적 모델을 의미하지만 객체 모델을 언급할 때에도 사용한다. (즉, 도메인 계층의 객체 모델을 표현할 때도 표현한다)

💡 user → 표현 → 응용 → 도메인 → 인프라스트럭처 → DB

  • 사용자 인터페이스(또는 표현)
    • 사용자(사람 or 외부 시스템)의 요청을 처리하고 사용자에게 정보를 보여준다.
  • 응용
    • 사용자가 요청한 기능을 실행한다. 업무 로직을 직접 구현하지 않으며 도메인 계층을 조합해서 기능을 실행한다.
  • 도메인
    • 시스템이 제공할 도메인 규칙을 구현한다.
  • 인프라스트럭쳐
    • 데이터베이스나 메시징 시스템과 같은 외부 시스템과의 연동을 처리한다.
도메인 계층은 도메인의 핵심 규칙을 구현

💡 배송지 변경 가능 여부를 판단하는 기능과 같이 중요 업무 규칙을 주문 도메인 모델인 Order나 OrderState에서 구현한다는 점이다.

=> 핵심 규칙을 구현한 코드는 도메인 모델에만 위치하기 때문에 규칙이 바뀌거나 규칙을 확장해야할 때 다른 코드에 영향을 덜 주고 변경 내역을 모델에 반영할 수 있게 된다

  • ex) 주문 도메인의 경우
    • 규칙 1 : 출고 전에 배송지를 변경할 수 있다.
    • 규칙 2 : 주문 취소는 배송 전에만 할 수 있다.
    [OrderState에서 배송지 변경 가능 여부 판단하는 경우]
    • 배송지 변경이 가능한지 판단할 규칙이 주문 상태만 사용한다면 해당 방법(Order state 만)으로는 배송지 변경 가능 여부를 판단
    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;
    		}
    
    		}
    	}
    
    [OrderState + 다른 정보를 반영하여 배송지 변경 여부를 판단하는 경우]
    • 배송지 변경이 가능한지 판단할 규칙이 주문 상태와 다른 정보를 함께 사용한다면 해당 방법(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 클래스

⇒ 이 요구 사항에서 알 수 있는 것은 주문 도메인은 다음과 같은 기능을 제공한다는 것이다.

  1. 출고 상태로 변경하기
  2. 배송지 정보 변경하기
  3. 주문 취소하기
  4. 결제 완료하기
  • Order에 관련 기능을 메서드로 추가할 수 있다.
package com.efub.dddstudy.Chap1_도메인모델시작하기;

public class Order {
	public void changeShipped(){}
	public void changeShippingInfo(){}
	public void cancel(){}
	public void completePayment(){}
}

OrderLine 클래스

⇒ 요구 사항에 따라 주문 항목이 어떤 데이터로 구성되는지 알려준다.

  1. 한 상품을 한 개 이상 주문할 수 있다.
  2. 각 상품의 구매 가격 합은 상품 가격에 구매 개수를 곱한 값이다.
  • 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 핵심 개념 정리부터 구현까지" 책을 읽고 정리한 글입니다. 

(http://www.yes24.com/Product/Goods/108431347)