본문 바로가기

JAVA/DDD

도메인 모델 도출

반응형

도메인 모델 도출

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


도메인을 모델링 할때 기본이 되는 작업은 모델을 구성하는 핵심구성요소, 규칙, 기능을 찾는것이다.
-최소 한종류 이상의 상품을 주문해야한다.
-한 상품을 한개 이상 주문할 수 있다.
-총 주문 금액은 각 상품의 구매 가격 합을 모두 더한 금액이다.
-각 상품의 구매 가격 합은 상품가격에 구매 개수를 곱한 값이다.
-주문할 때 배송지 정보를 반드시 지정해야한다.
-배송지 정보는 받는사람이름, 전화번호, 주소로 구성된다.
-출고를 하면 배송지 정보를 변경할 수 없다.
-출고 전에 주문을 취소할 수 있다.
-고객에 결제를 완료하기 전에는 상품을 준비하지 않는다.

주문은 '출고상태로 변경하기','배송지 정보 변경하기', '주문 취소하기','결제완료로 변경하기'의 네 기능을 제공한다는것
Order에 관련 기능을 메서드로 추가할 수 있다.

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

-한 상품은 한개 이상 주문할 수 있다.
-각 상품의 구매 가격 합은 상품 가격에 구매 개수를 곱한 값이다.


주문항목을 표현하는 OrderLine은 적어도 주문할 상품, 상품의 가격, 구매 개수를 포함하고 있어야 한다.
추가로 각 구매 항목의 구매 가격도 제공해야한다.

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;

    this.quantity = qunantiy;

    this.amounts = calculateAmounts();

  }

  private int calculateAmount(){

    return price * quantity;

  }


  public int getAmounts() {}

}

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


Order와 OrderLine과의 관계
-최소 한 종류 이상의 상품을 주문해야 한다.
-총 주문 금액은 각 상품의 구매 가격 합을 모두 더한 금액이다.

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

public class Order {

    private List<OrderLine> orderLines;

    private int 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() {

      this.totalAmounts = orderLines.stream().mapToInt(x->x.getAmounts()).sum();

    }

}

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


배송지 정보는 이름, 전화번호, 주소 데이터를 가진다

public class ShippingInfo {

private String receiverName;

private String receiverPhoneNumber;

private String shippingAddress1;

private String shippingAddress2;

private String shippingZipcode;


  //생성자 getter

}

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

public class Order {

    private List<OrderLine> orderLines;

    private int totalAmounts;

    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;

}


    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() {

      this.totalAmounts = orderLines.stream().mapToInt(x->x.getAmounts()).sum();

    }

}

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

도메인을 구현하다 보면 특정 조건이나 상태에 따라 제약이나 규칙이 달리 적용되는 경우가 많다.
- 출고를 하면 배송지 정보를 변경할 수 없다.
- 출고 전에 주문을 취소 할 수 있다.

이 요구사항은 출고상태가 되기전과 후의 제약사항을 기술하고 있다. 출고상태에 따라 배송지 정보 변경 기능과 주문 취소 기능이 제약을 받는다.
이 요구사항을 충족하려면 주문은 적어도 출고 상태를 표현할 수 있어야 한다.
-고객이 결제를 완료하기 전에는 상품을 준비하지 않는다.

이 요구사항은 결제 완료 전을 의미하는 상태와 결제 완료내지 상품 준비중이라는 상태가 필요함을 알려주고 있다.
다른 요구사항을 좀 더 분석해서 추가로 존재할 수 있는 상태를 분석 한 뒤, 열거 타입을 이용해서 상태 정보를 표현할 수 있다.


public enum OrderState {

PAYMENT_WATING, PREPARING, SHIPPED, DELIVERING, DELIVERY_COMPLATED, CANCELD;

}

배송지 변경이나 주문 취소 기능은 출고 전에만 가능하다는 제약 규칙이 있으므로 이 규칙을 적용하기 위해 changeShippingInfo()와 cancel()은 verifyNotYetShipped() 메서드를 먼저 실행한다.

public class Order {

    private OrderState state;

    private List<OrderLine> orderLines;

    private int totalAmounts;

    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;

}


    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() {

      this.totalAmounts = orderLines.stream().mapToInt(x->x.getAmounts()).sum();

    }


    public void changeShippingInfo(ShippingInfo newShippingInfo) {

     verifyNotYetShipped();

     setShippingInfo(newShippingInfo);

    }


    private void verifyNotYetShipped() {

        if(state != OrderState.PAYMENT_WATING && state != OrderState.PREPARING) {

            throw new IllegalStateException("aleady shipped");

        }

    }

}


주문과 관련된 요구사항에서 도메인 모델을 점진적으로 만들어 나갔다.
일부는 구현 수준까지 만들었고 일부는 이름정도만 결정했다.
이렇게 만든 모델은 요구사항 정련을 위해 도메인 전문가나 다른 개발자와 논의하는 과정에서 공유하기도 한다.
모델을 공유할 때는 화이트보드나 위키와 같은 도구를 사용해서 누구나 쉽게 접근할 수 있도록 하면 좋다.


DDD START! 도메인주도설꼐구현과 핵심개념 익히기 저자-최범균

반응형

'JAVA > DDD' 카테고리의 다른 글

트랜잭션 범위  (0) 2017.12.23
Aggregate 애그리거트  (0) 2017.12.19
아키텍처  (0) 2017.12.18
엔티티와 밸류  (0) 2017.12.16
DDD - 도메인 모델 패턴  (0) 2017.12.15