도메인 모델 시작[DDD START!]

도메인

  • 예를 들어 온라인 서점을 구현한다고 하면, 개발자 입장에서는 온라인 서점은 구현해야 할 소프트웨어의 대상이 된다.
  • 온라인 서점 소프트웨어는 온라인으로 책을 판매하는 데 필요한 상품조회, 구매, 결제, 배송 추적 등의 기능을 제공해야한다.
  • 이때 ‘온라인 서점’은 소프트웨어로 해결하고자 하는 문제, 즉 도메인에 해당한다.
    • 한 도메인은 다시 하위 도메인으로 나눌 수 있다.
    • 특정 도메인을 위한 소프트웨어라고 해서 도메인이 제공해야 할 모든 기능을 구현하는 것은 아니다.
    • 도메인마다 고정된 하위 도메인이 존재하는 것은 아니다.
    • 하위 도메인을 어떻게 구성할지 여부는 상황에 따라 달라진다.

도메인 모델

  • 기본적으로 도메인 모델은 특정 도메인을 개념적으로 표현한 것이다.
  • 도메인 모델을 사용하면 여러 관계자들이 동일한 모습으로 도메인을 이해하고 도메인 지식을 공유하는 데 도움이 된다.
  • 도메인을 이해하려면 도메인이 제공하는 기능과 도메인의 주요 데이터 구성을 파악해야 한다.
  • 도메인을 이해하는 데 도움이 된다면 표현 방식이 무엇인지는 중요하지 않다.
  • 도메인 모델은 기본적으로 도메인 자체를 이해하기 위한 개념 모델이다.

도메인 모델 패턴

  • 일반적인 애플리케이션의 아케텐처는 네 개의 계층으로 구성된다.
계층(Layer) 설명
사용자인터페이스(UI) 또는 표현(Presentiation) 사용자의 요청을 처리하고 사용자에게 정보를 보여준다. 여기서 사용자는 소프트웨어를 사용하는 사람뿐만 아니라 외부 시스템도 사용자가 될 수 있다.
응용(Application) 사용자가 요청한 기능을 실행한다. 업무 로직을 직접 구현하지 않으며 도메인 계층을 조합해서 기능을 실행한다.
도메인 시스템이 제공할 도메인의 규칙을 구현한다.
인프라스트럭처(Infrastructure) 데이터베이스나 메시징 시스템과 같은 외부 시스템과의 연동을 처리한다.
  • 도메인 계층은 도메인의 핵심 규칙을 구현한다. 예를 들어, 주문 도메인인 경우 ‘출고 전에 배송지를 변경할 수 있다.’는 규칙과 ‘주문 취소는 배송 전에만 할 수 있다.’는 규직츨 구현한 코드가 도메인 계층에 위차하게 된다.

  • 이런 도메인 규칙을 객체 지향 기법으로 구현하는 패턴이 도메인 모델 패턴이다.
  • 아래 코드는 주문 도메인의 일부 기능을 도메인 모델 패턴으로 구현한 것이다.
public class Order {
  private OrderState state;
  private ShippingInfo shippingInfo;
  
  public void changeShippingInfo(ShippingInfo newShippingInfo) {
  //변경 가능 여부를 확인
    if(!state.isShippingChangeable()) {
      throw new Exceiption("can't chage shipping in "+sate);
    }
    
    this.shippingInfo = newShippingInfo;
  }
  
  public void changeShippped() {
    //로직검사
    this.state = OrderState.SHIPPED;
  }
  //...
}

//배송지를 변경할 수 있는지 여부를 검사할 수 있는 메서드 제공
//주문 대기 중, 상품 준비 중에는 배송지를 변경할 수 있다는 규칙을 구현
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에 속한 데이터이므로 배송지 정보 변경 가능 여부를 판단하는 코드를 Order로 이동할 수도 있다.
  • 아래 코드는 Order클래스에서 판단하도록 수정한 코드이다.
public class Order {
  private OrderState state;
  private ShippingInfo shippingInfo;
  
  public void changeShippingInfo(ShippingInfo newShippingInfo) {
    if(!isShippingChangeable()) {
      throw new Exception("can't change shipping in "+state);
    }
    this.shippingInfo = newShippingInfo;
  }
  
  private boolean isShippingChangeable() {
    return state == OrederState.PAYMENT_WAITING ||
      state == OrderState.WAITING;
  }
  //...
}

public enum OrderState {
  PAYMENT_WAITING, PREPARING, SHIPPED, DELIVERING, DELIVERY_COMPLETED;
}
  • 배송지 변경이 가능한지 여부를 판단할 규칙이 주문 상태와 다른 정보를 함께 사용한다면 배송지 변경 가능 여부 판단을 OrderState만으로 할 수 없으므로 로직 구현을 Order에서 해야 한다.

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

도메인 모델 도출

  • 도메인을 모델링할 때 기본이 되는 작업은 모델을 구성하는 핵심 구성요소, 규칙, 기능을 찾는 것이다.
* 주문 도메인과 관련된 요구사항
1. 최소 한 종류 이상의 상품을 주문해야 한다.
2. 한 상품을 한 개 이상 주문할 수 있다.
3. 총 주문 금액은 각 상품의 구매 가격 합을 모두 더한 금액이다.
4. 각 상품의 구매 가격 합은 상품 가격에 구매 개수를 곱한 값이다.
5. 주문할 때 배송지 정보를 반드시 지정해야 한다
6. 배송지 정보는 받는 사람 이름, 전화번호, 주소로 구성된다.
7. 출고를 하면 배송지 정보를 변경할 수 없다.
8. 출고 전에 주분을 취소할 수 있다.
9. 고객이 결제를 완료하기 전에는 상품을 준비하지 않는다.
  • 요구사항에서 알 수 있는 것은 주문은 ‘출고 상태로 변경하기’, ‘배송지 정보 변경하기’, ‘주문 취소하기’, ‘결재 완료로 변경하기’의 네 기능을 제공한다는 것이다.
  • Oder에 관련 기능을 메소드로 추가할 수 있다.
public class Order {
  public void changeShipped() { }
  public void changeShippingInfo(ShippingInfo newShipping) { }
  public void cancel() { }
  public void completePayment() { }
}
  • 아래 항목은 주문 항목이 어떤 데이터로 구성되는지 알려준다.
- 한 상품을 한 개 이상 주문할 수 있다.
- 각 상품의 구매 가격 합은 상품 가격에 구매 개수를 곱한 값이다.
  • 주문 항목을 표현하는 OrderLine은 적어도 주문할 상품, 상품의 가격, 구매 개수를 포함하고 있어야 한다.
  • 아래는 이를 구현한 OrderLine의 코드이다.
public class OrderLine {
  private Product product; //상품
  private int price;  //가격
  private int quantity;  //수량
  private int amounts; //구매가격
  
  public OrderLine(Prodect product, int price, int quantity) {
    this.product = product;
    this.private = price;
    this.quantity = quantity;
    this.amounts = calculateAounts();
  }
  
  //구매 가격 구하기
  private int calculateAounts() {
    return price * quantity;
  }
  
  public int getAmounts() {  }
}
  • 아래 항목은 Order와 OderLine과의 관계를 알려준다.
- 최소 한 종류 이상의 상품을 주문해야 한다.
- 총 주문 금액은 각 상품의 구매 가격 합을 모두 더한 금액이다.
  • 한 종류 이상의 상품을 주문할 수 있으므로 Order는 최소 한 개 이상의 OrderLine을 포함해야 한다.
  • OrderLine으로부터 총 주문 금액을 구할 수 있다.
public class Oreder {
  private List<OrderLine> ordrLines;
  private int totalAmounts;
  
  public Order(List<OrederLine> orderLines) {
    setOrderLines(orderLines);
  }
  
  private void setOrderLines(List<OrderLine> orderLines) {
    verifyAtLeastOneOrMoreOrderLines(orderLines);
    this.orderLines = orderLines;
    calculateTotalAmounts();
  }
  
  private void verifyAtLeastOneOrMoreOrderLines(List<OrderLine> orderLine) {
    if(orderLines == null || orderLines.isEmpty()) {
      throw new Exception("no OrderLine");
    }
  }
  
  private void calculateTotalAmounts() {
    this.totalAmounts = new Money(orderLines.stream()
      .mapToInt(x -> x.getAmounts().getValue()).sum();
  }
  //....
}