IT서적/도메인 주도 개발 시작하기

[도메인 주도 개발 시작하기:DDD] 1. 도메인 모델 시작하기

dev22 2023. 7. 19. 17:57
728x90

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

 

코딩에 앞서 요구사항을 올바르게 이해하는 것이 중요하다.

요구사항을 제대로 이해하지 않으면 쓸모없거나 유용함이 떨어지는 시스템을 만들기 때문이다. 요구사항을 잘못 이해하면 변경하거나 다시 만들어야 할 코드가 많아지고 경우에 따라 소프트웨어, 즉 제품을 만드는 데 실패하거나 일정이 크게 밀리기도 한다. 아쉽게도 이런 문제는 자주 발생한다. 

 

도메인 전문가 만큼은 아니겠지만 이해관계자와 개발자도 도메인 지식을 갖춰야 한다. 제품 개발과 관련된 도메인 전문가,

관계자, 개발자가 같은 지식을 공유하고 직접 소통할수록 도메인 전문가가 원하는 제품을 만들 가능성이 높아진다. 

 

1.3 도메인 모델

특정 도메인을 개념적으로 표현한 것이다. 

 

p28 도메인 모델 객체 표현

여러 관계자들이 동일한 모습으로 도메인을 이해하고 도메인 지식을 공유하는 데 도움이 된다. 각 하위 도메인마다 별도로 모델을 만들어야 한다. 

 

1.4 도메인 모델 패턴

 

=아키텍처 구성

사용자 인터페이스 또는 표현

응용

도메인

인프라스트럭처

 

'도메인 모델'이란 용어는 도메인 자체를 표현하는 개념적인 모델을 의밈하지만, 도메인 계층을 구현할 때 사용하는 객체 모델을 언급할 때에도 '도메인 모델'이란 용어를 사용한다. 

 

프로젝트 초기에 완벽한 도메인 모델을 만들더라도 결국 도메인에 대한 새로운 지식이 쌓이면서 모델을 보완하거나 변경하는 일이 발생한다. 따라서 처음부터 완벽한 개념 모델을 만들기보다는 전반적인 개요를 알 수 있는 수준으로 개념 모델을 작성해야 한다. 프로젝트 초기에는 개요 수준의 개념 모델로 도메인에 대한 전체 윤곽을 이해하는 데 집중하고, 구현하는 과정에서 개념 모델을 구현 모델로 점진적으로 발전시켜 나가야 한다. 

 

1.5 도메인 모델 도출

주문 도메인과 관련된 몇 가지 요구사항을 보자.

  • 최소 한 종류 이상의 상품을 주문해야 한다. -->Order와 OrderLine과의 관계
  • 한 상품을 한 개 이상 주문할 수 있다. --> 어떤 데이터로 구성
  • 총 주문 금액은 각 상품의 구매 가격 합을 모두 더한 금액이다. -->Order와 OrderLine과의 관계
  • 각 상품의 구매 가격 합은 상품 가격에 구매 개수를 곱한 값이다. --> 어떤 데이터로 구성 
  • 주문할 때 배송지 정보를 반드시 지정해야 한다. --> shippingInfo를 Order에 전달
  • 배송지 정보는 받는 사람, 이름, 전화번호, 주소로 구성된다.  --> shippingInfo
  • 출고를 하면 배송지를 변경할 수 없다. --> 제약과 규칙
  • 출고 전에 주문을 취소할 수 있다. --> 제약과 규칙
  • 고객이 결제를 완료하기 전에는 상품을 준비하지 않는다. --> 상태와 관련
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;
        tis.quantity = quantity;
        this.amounts = calculateAmounts();
    }   
    
    private int calculateAmounts(){
    	return price * quantity;
    }
    
    public int getAmounts(){...}
    ...
}

한 종류 이상의 상품을 주문할 수 있으므로 Order는 최소 한 개 이상의 OrderLine을 포함해야 한다. 또한 총 주문 금액은 OrderLine에서 구할 수 있다. 

 

public class Order{
	private List<OrderLine> orderLines;
    private Money totalAmounts;
    
    public Order(List<OrderLine> orderLines){
    	setOrderLines(orderLines)
    }
    
    private void setOrderLines(List<OrderLine> orderLines){
    	verifyAtLeastOneOrMoreOrderLines(orderLines);
        this.orderLines = orderLines;
        calculateTotalAmounts();
    }
    
    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);
    }
    
    //다른 메서드


}

배송지 정보는 이름, 전화번호, 주소 데이터를 가지므로 ShippingInfo 클래스를 다음과 같이 정의할 수 있다. 

public class ShippingInfo{
	private String receiverName;
    private String receiverPhoneNumber;
    private String shippingAddress1;
    private String shippingAddress2;
    private String shippingZipcode;
    
    ...생성자, getter
}
public class Order{
	private List<OrderLine> orderLines;
    private ShippingInfo shippingInfo;
    ...
    
    public Order(List<OrderLine> orderLines, ShippingInfo shippingInfo){
    	setOrderLines(orderLines);
        setShippingInfo(shippingInfo);
    }
    
    private void setShippingInfo(ShippingInfo shippinfInfo){
    	if(shippingInfo == null)
        	throw new IllegalArgumentException("no ShippingInfo");
         this.shippingInfo = shippingInfo;
    }
    ...
}
public enum OrderState{
	PAYMENT_WAITING, PERPARING, SHIPPED, DELIVERING, DELIVERY_COMPPLETED, CANCELED;
}

배송지 변경이나 주문 취소 기능은 출고 전에만 가능하다는 제약 규칙이 있다. 

public class Order{
	private OrderState state;
    
    public void changeShippingInfo(ShippingInfo newShippingInfo){
    	verifyNotYetShipped();
        setShippingInfo(newShippingInfo);
    }
    
    public voic cancel(){
    	verifyNotYetShipped();
        this.state = OrderState.CANCELED;
    }
    
    private void verifyNotYetShipped(){
    	if(state != OrderState.PAYMENT_WAITING && state != OrderState.PREPARING)
        	throw new IllegalStateException("already shipped");
    }
}