ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 도메인 주도 개발3 - 도메인 모델 도출
    Web 개발/도메인 주도 개발 2023. 10. 10. 15:05

    유스케이스(Use Case)

    도메인 모델 도출

    도메인에 대한 이해 없이 코딩을 시작할 수 없다. 기획서, 유스케이스(Use Case),사용자 스토리와 같은 요구 사항과 관련자와의 대화를 통해 도메인을 이해하고 이를 바탕으로 도메인 초안을 만들어야 비로소 코드를 작성할 수 있다. 화이트보드, 종이와 연필, 모델링 툴 중 무엇을 선택하든 구현을 시작하려면 도메인에 대한 초기 모델이 필요하다.

     

    요구사항 정리

    도메인을 모델링할 때 기본이 되는 작업은 모델을 구성하는 1)핵심 구성요소, 2)규칙, 3)기능을 찾는 것이다. 이 과정은 요구사항에서 출발한다. 예시로 주문 도메인과 관련된 몇 가지 요구사항을 알아보자.

    • 최소 한 종류 이상의 상품을 주문해야 한다.
    • 한 상품을 한 개 이상 주문할 수 있다.
    • 각 상품의 구매 가격 합은 상품 가격에 구매 개수를 곱한 값이다.
    • 총 주문 금액은 각 상품의 구매 가격 합을 모두 더한 금액이다.
    • 주문할 때 배송지 정보를 반드시 지정해야 한다.
    • 배송지 정보는 받는 사람 이름, 전화번호, 주소로 구성된다.
    • 출고를 하면 배송지를 별견할 수 없다.
    • 출고 전에 주문을 취소할 수 있다.
    • 고객이 결제를 완료하기 전에는 상품을 준비하지 않는다.

     

    Order  객체

    위 요구사항에서 알 수 있는 것은 주문은 '출고 상태로 변경하기', '배송지 정보 변경하기', '주문 취소하기', '결제 완료하기' 기능을 제공한다는 것이다. 요구사항을 통해서 필요 매서드를 도출해 낼 수 있는 것이다. 아직 상세 구현까지 할 수 있는 수준은 아니지만 Order에 관련기능을 메서드로 추가할 수 있다.

     

    public class Order{
       public void changeShipped(){...} //출고 상태로 변경하기
       public void changeShippingInfo(ShippingInfo new Shipping){...} //배송지 정보 변경하기
       public void cancel(){...} //주문 취소하기
       public void completePayment(){...} //결제 완료하기
    }

     

    OrderLine 객체

    다음 요구사항은 주문 항목이 어떤 데이터로 구성되는지 알려준다.

    • 한 상품을 한 개 이상 주문할 수 있다
    • 각 상품의 구매 가격 합은 상품 가격에 구매 개수를 곱한 값이다.
    public class OrderLine{
       private Product product;
       private int price;
       private int quantity;
       private amounts;
       
       public OrderLine(Product product, int price, int quantity){
    		this.product = product;
            this.price = price;
            this.quantity = quantity;
            this.amounts = caculateAmounts();
       }
       
       private int calculateAmounts(){
       	return price * quantity;
       }
       
       public int getAmounts(){...}
       ...
    }

    두 요구사항에 따르면 주문 항목을 표현하는 OrderLine은 적어도 주문할 상품, 상품의 가격, 구매 개수를 포함해야한다. 추가로 각 구매 항목의 구매 가격도 제공해야 한다. 이를 구현한 OrderLine은 위 코드와 같다.

     

    OrderLine은 한 상품(product 필드)을 얼마에(price 필드) 몇 개 살지 (quantity 필드)를 담고 있고 calculateAmounts()메서드로 구매 가격을 구하는 로직을 구현하고 있다.

     

    Order객체와 OrderLine 객체의 관계

    다음 요구사항은 Order와 OrderLine과의 관계를 알려준다.

    • 최소 한 종류 이상의 상품을 주문해야 한다.
    • 총 주문 금액은 각 상품의 구매 가격 합을 모두 더한 금액이다.
    public class Order{
       private List<OrderLine> orderLines;
       private Money totalAmounts;
       
       public Order(List<OrderLine> orderLines){
       	setOrderLine(orderLines); //생성자에서 orderLine stter 메서드 소화
       }
       
       private void setOrderLines(List<orderLine>orderLines){
       	verifyAtLeastOneOrMoreOrderLines(orderLines);  // 점검 메서드
        this.orderLines = orderLines;	// setter 핵심(대입)
        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);
       }
       
       ...// 다른 메서드
    }

    한 종류 이상의 상품을 주문할 수 있으므로 Order는 최소 한 개 이상의 OrderLine을 포함해야한다. 또한 총 주문 금액은 OrderLine의 (멤버)메서드를 사용해서 구할 수 있다. 두 요구사항은 위와 같이 코드로 작성할 수 있다.

     

    Order는 한 개 이상의 OrderLine을 가질 수 있으므로 Order를 생성할 때 OrderLine 목록을 List로 전달한다. 생성자에 호출하는 setOrderLines()메서드는 요규사항에 정의한 제약 조건을 검사한다. 요구사항에 따르면 최소 한 종류 이상의 상품을 주문해야 하므로 verifyAtLeastOneOrMoreOrderLines() 메서드를 이용해서 OrdeLine이 한개 이상 존재하는지 검사한다. 또한 calculateTotalAmounts() 메서드를 이용해서 총 주문 금액을 계산한다.(각 상품의 구매가격의 합)

     

    ShippingInfo 객체

    public class ShippingInfo{
        private String receiverName;
        private String receiverPhoneNmber;
        private String shippingAdress1;
        private String shippingAddress2;
        private String shippingZipcode;
        
        ... 생성자, getter
        
    }

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

     

    Order객체와 ShippingInfo객체의 관계

    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 shippingInfo) {
       	if (shippingInfo == null)
        	throw new IllegalArgumentException("no ShippingInfo");
        this.shippingInfo = shippingInfo;
       }
       ...
    }

    앞서 요구사항 중 '주문할 때 배송지 정보를 반드시 지정해야 한다'라는 내용이 있다. 이는 Order를 생성할 때 OrderLine의 목록뿐만 아니라 ShippingInfo도 함께 전달해야 함을 의미한다. 이를 생성자에 반영한다.

     

    생성자에서 호출하는 setShippingInfo()메서드는 ShippingInfo가 null이면 익셉션이 발생하는데, 이렇게 함으로써 '배송지 정보 필수'라는 도메인 규칙을 구현한다.

     

     

    조건or상태에 따른 제약과 규칙

    도메인을 구현하다 보면 특정 조건이나 상태에 따라 제약이나 규칙이 달리 적용되는 경우가 많다. 주문 요구사항에서는 위의 내용이 제약과 규칙에 해당한다.

    조건

    • 출고를 하면 배송지 정보를 변경할 수 없다.
    • 출고 전에 주문을 취소할 수 있다.

    요구사항은 출고 상태가 되기 전과 후의 제약사항을 기술하고 있다. 출고 상태에 따라 배송지 정보 변경 기능과 주문 취소 기능은 다른 제약을 갖는다. 이 요구사항을 충족할려면 주문은 최소한 출고 상태를 표현할 수 있어야 한다.

    조건

    • 고객이 결제를 완료하기 전에는 상품을 준비하지 않는다.

    위 요구사항도 상태와 관련이있다. 결제 완료 전을 의미하는 상태와 결제 완료 내지 상품 준비 중 이라는 상태가 필요함을 알려준다.

     

    public enum OrderState{
       PAYMENT_WAITING, PREPARING, SHIPPED, DELIVERING, DELIVERY_COMPLETED,CANCLELED;
    }

    다른 요구사항을 확인해서 추가로 존재할 수 있는 상태를 분석한 뒤, 위와 같이 열거 타입(enum)을 이용해서 상태 정보를 표현할 수 있다.

     

     

    Order객체와 OrderState의 관계

    public class Order{
       private OrderState state;
       
       public void changeShippingInfo(ShippingInfo new ShippingInfo){
         verifyNotYetShipped(); 
         // 외부에서 changeShippingInfp를 호출했을때 우선 state를 체크
         setShippingInfo(new ShippingInfo);
       }
       
       public void cancel(){
       	verifyNotYetShipped();
        this.state = OrderState.CANCELED;
       }
       
       private void verifyNotYetShipped(){
       	if(state != OrderState.PAYMENT_WAITING && state != OrderState.PREPARING)
        	throw new IllegalStateException("aleady shipped");
       }
       
       ...
    }

     

    💡 tip) 메서드 명이 바뀐 이유는?

    앞서 게시글 <도메인 주도 개발2 - 도메인 모델 패턴>에서 도메인 모델 패턴을 설명할 때의 제약조건을 검사하던 메서드 이름 isShippingChangeable()와 다르게 verifyNotYetShipped()로 변경된 이유는 그 상이 도메인에 대한 이해가 더 깊어졌기 때문이다. 이전 게시글의 예시 코드에서는 배송지 정보 변경이라는 제약 조건만 파악했기 때문에 메서드 이름에 '배송지 정보 변경 여부 확인'만을 의미하는 이름이 사용되었다. 하지만 요구사항을 분석하면서 배송지 정보 변경과 주문 취소 기능이 모두 '출고 전에 가능' 하나든 제약이 있음을 알게 되었으므로 '출고 전'이라는 의미를 반영하기 위해 메서드 이름이 변경된 것이다. 

     

     

    [출처 - 도메인 주도 개발 시작하기: DDD 핵심 개념 정리부터 구현까지, 최범균 저]

    https://www.hanbit.co.kr/store/books/look.php?p_code=B4309942517

     

    도메인 주도 개발 시작하기

    실제 업무에 도메인 주도 설계(DDD)를 적용할 수 있도록 기본적인 DDD의 핵심 개념을 익히고 구현을 통해 학습할 수 있도록 구성한 DDD 입문서

    www.hanbit.co.kr

     

    댓글

Designed by Tistory.