DDD 도메인 주도 설계 1장

DDD - 도메인 주도 설계

DDD

앞으로 적어갈 글은 최범균님의 저서 DDD START!를 읽고 요약 - 정리한 글입니다.

제 1장 : 도메인 모델 시작

제 1장 도메인 모델 시작

도메인이란 무엇인가

  • 온라인 서점을 개발한다고 가정했을 때 온라인 서점은 소프트웨어로 해결하고자 하는 문제 영역, 즉 Domain에 해당한다.

1

  • 고객이 물건을 주문하면 주문, 결제, 배송, 혜택 하위 도메인의 기능이 엮이게 된다.

  • 소프트웨어가 항상 도메인의 모든 기능을 제공하지는 않는다.

    • 가령 온라인 서점에서 결제와 배송의 경우 대행 업체의 서비스를 이용해서 처리할 수도 있다.

도메인 모델

2

  • 위에서 언급한 주문 모델을 객체 모델로 구성하면 위와 같다.

  • Order에서 주문 번호와 총 금액이 있고, 배송 정보를 변경하고 주문을 취소할 수 있음을 알 수 있다.

  • 이러한 객체 모델은 도메인을 모델링하기에 적합하다.

3

  • 도메인 모델을 객체로만 모델링할 수 있는 것은 아니다.

    • 클래스 다이어그램이나 상태 다이어그램과 같은 UML 뿐만 아니라 그래프를 활용해서 모델링 할 수도 있다.
  • 도메인을 이해하는데 도움이 된다면 수식도 상관없으며 표현 방식이 중요한 것은 아니다.

  • 개념 모델과 구현 모델이 반드시 같지는 않지만 구현 모델이 개념 모델을 최대한 따르게 할 수는 있다.

    • 객체 기반 모델을 이용해서 도메인을 표현했다면, 객체 지향 언어를 이용해 개념 모델에 가깝게 구현 가능

도메인 모델 패턴

4

  • 일반적인 애플리케이션의 아키텍처는 위와 같이 네 개의 계층으로 구성된다.

  • UI : 사용자의 요청을 처리하고 사용자에게 정보를 보여준다. 소프트웨어 사용자 뿐만 아니라 외부 시스템도 사용자가 될 수 있다.

  • 응용 : 사용자가 요청한 기능을 실행한다. 도메인 계층을 조합해 기능을 실행한다.

  • 도메인 : 시스템이 제공할 도메인의 규칙을 구현한다.

  • 인프라스트럭쳐 : DB나 메시징 시스템과 같은 외부 시스템과의 연동을 처리한다.

  • 도메인 모델 패턴은 아키텍처 상의 도메인 계층을 객체 지향 기법으로 구현하는 패턴을 말한다.

  • 도메인 계층은 도메인의 핵심 규칙을 구현한다.

    • e.q. 주문 도메인의 경우 출고 전에 배송지를 변경할 수 있다
    • e.q. 주문 취소는 배송 전에만 할 수 있다
  • 이런 도메인 모델에 규칙을 두는 이유는, 코드가 도메인 모델에만 위치하기 때문에 규칙이 바뀌거나 확장하더라도 다른 코드에 영향을 덜 주고 변경 내역을 모델에 반영할 수 있기 때문이다.

도메인 모델 노출

주문 도메인과 관련된 아래의 요구사항이 있다.

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

이 요구사항에서 알 수 있는 것은 주문은 출고 상태로 변경하기, 배송지 정보 변경 하기, 주문 취소하기, 결제 완료로 변경하기의 네 기능을 제공한다는 것이다.

위 메소드를 토대로 Order 클래스를 구현해볼 수 있다.

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

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

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

이러면 주문 항목을 표현하는 OderLine은 적어도 주문할 상품, 상품의 가격, 구매 개수를 포함하고 있어야 한다.

추가로 각 구매 항목의 구매 가격도 제공해야한다.

public class OrderLine {
 private Product product;
 private int price;
 private int quantity;
 private int amount;

 ...
}

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

OrderLine으로 부터 총 구매 금액을 구할 수 있다.

public class Order {
 private List<OrderLine> orderLines;
 private int totalAmounts;

 private void setOrderLines(List<OrderLine> orderLines) { ... }
 private void verifyAtLeastOneOrMoeOrderLines(List<OrderLine> orderLines) { ... }
 private void calculateTotalAmounts() { ... }

 ...
}
  • 여기서 주목해야할 점은 Order는 반드시 한개 이상의 OrderLine을 가져야 하므로 verifyAtLeastOneOrMoeOrderLines라는 메소드로 검증한다.
    • 검증 메소드를 Order에 포함시켜도 되는구나! (항상 고민이었음. Validation 메소드를 별도의 클래스로 뽑아야 하는건지?)
    • DDD에서는 검증 또한 해당 도메인 모델이 담당하는 구나
public class ShipingInfo {
 private String receiverName;
 private String receiverPhoneNumber;
 private String shipingAddress1;
 private String shipingAddress2;
 private String shipingZipcode;

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

  • 주문할 때 배송지 정보를 반드시 지정해야한다는 요구사항이 있다.
  • Order를 생성할 때 ShippingInfo도 함께 전달해야 한다.
public class Order {
 private List<OrderLine> orderLines;
 private int totalAmounts;
 private ShipingInfo 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");
    }
 ...
}
  • setShippingInfo에서 shippingInfo가 null이면 예외를 발생하는 것은 배송지 정보 필수라는 도메인 규칙을 구현한 것이다.

  • 도메인을 구현하다 보면 특정 조건이나 상태에 따라 제약이나 규칙이 달리 적용되는 경우가 많다.

    • 출고를 하면 배송지 정보를 변경할 수 없다.
    • 출고 전에 주문을 취소할 수 있다.
    • 고객이 결제를 완료하기 전에는 상품을 준비하지 않는다.

이는 열거 타입을 통해 주문 상태를 표현할 수 있다.

public enum OrderState {
 PAYMENT_WAITING,
 PREPARING,
 SHIPPED,
 DELIVERING,
 DELIVERY_COMPLETED,
 CANCELED
}
  • 배송지 변경, 주문 취소 기능은 출고 전에만 가능하다는 제약 규칙이 있으므로 이 규칙을 적용하기 위해 changeShippingInfo()와 cancel()은 verifyNotYetShipped() 메서드를 먼저 실행한다.
public class Order {
 private OrderState state;

    public void changeShippingInfo(ShippingInfo newShippingInfo) {
        verifyNotYetShipped();
        setShippingInfo(newShippingInfo);
    }

 public void cancel() {
        verifyNotYetShipped();
        this.state = OrderState.CANCELED;

     }
 public void verifyNotYetShipped() {
        if( state != OrderState.PAYMENT_WATING && state != OrderState.PREPARING)
            throw new IllegalStateException("aleady shipped");
     }
}
  • 지금 까지 주문과 관련된 요구 사항에서 도메인 모델을 점진적으로 만들었다.

문서화

  • 문서화의 주 목적은 지식의 공유

  • 전반적인 기능 목록이나 모듈 구조, 빌드 과정은 코드를 보고 이해하는 것 보다 상위 수준에서 정리한 문서가 더 빠르게 소프트웨어를 이해하는데 도움을 줄 수 있다.

  • 코드 자체가 문서가 될 수 있다. 그러니까 코드 잘 짜자.

엔티티와 밸류

  • 도출한 모델은 크게 EntityValue로 구분할 수 있다.

  • EntityValue를 제대로 구분해야 도메인을 올바르게 설계하고 구현할 수 있다.

엔티티

  • Entity의 가장 큰 특징은 식별자를 갖는다는 것이다.

  • 주문 도메인에서 각 주문은 주문번호를 갖는데, 이 주문번호는 각 주문마다 서로 다르기 때문에 식별자로 볼 수 있다.

  • 주문 도메인 모델에서 주문에 해당하는 클래스가 Order이므로 Order가 Entity이고 주문번호를 속성으로 갖게 된다.

엔티티의 식별자 생성

흔히 식별자는 다음 중 한 가지 방식으로 생성한다.

  • 특정 규칙에 따라 생성

  • UUID 사용

  • 값을 직접 입력

  • 일련번호 사용(시퀀스나 DB의 자동 증가 칼럼 사용)

밸류 타입

  • ShippingInfo 클래스는 받는 사람과 주소에 대한 데이터를 갖고 있다.

  • ShippingInfo 클래스의 receiverName 필드와 receiverPhoneNumber 필드는 개념적으로 받는 사람을 의미한다.

  • ShippingInfo 클래스의 shippingAddress1, shippingAddress2, shippingZipcode 필드는 주소라는 하나의 개념을 표현한다.

이를 코드로 표현하면 다음과 같을 수 있다.

public class Receiver {
    private String name;
    private String phoneNumber;

    ...
}
public class Address{
    private String address1;
    private String address2;
    private String zipcode;
    ...
}

밸류 타입을 이용해서 ShippingInfo 클래스를 재 구현 해보자.

배송 정보가 받는 사람과 주소로 구성된다는 것을 쉽게 알 수 있다.

public class ShippingInfo {
    private Receiver receiver;
    private Address address;

    // ... 생성자, get 메서드
}
  • 밸류 타입이 반드시 두 개 이상의 데이터를 가져야 하는 것은 아니다.

  • 의미를 명확하게 표현하기 위해 밸류 타입을 사용하는 경우도 존재한다. (돈을 int가 아닌 Money 클래스로 정의)

엔티티 식별자와 밸류 타입

  • 엔티티 식별자의 실제 데이터는 주로 String과 같은 문자열로 구성된다.

  • 식별자 타입이라는 것을 조금 더 명확히 하기 위해 Order의 식별자를 String이 아닌 OrderNo 밸류 타입을 사용하면 타입을 통해 해당 필드가 주문번호라는 것을 알 수 있다.

public class Order {
    // OrderNo 타입 자체로 id가 주문번호임을 알 수 있다.
    private OrderNo id;

    ...
}

도메인 모델에 set 메서드 넣지 않기

  • 도메인 모델에 get/set 메서드를 무조건 추가하는 것은 좋지 않을 수 있다.

  • 특히 set 메서드는 도메인의 핵심 개념이나 의도를 코드에서 사라지게 만든다.

  • 무분별한 set의 남발은 상태 변경과 관련된 도메인 지식이 코드에서 사라지게 만든다.

  • set 메서드의 또 다른 문제는 도메인 객체를 생성할 때 완전한 상태가 아닐 수도 있다는 것이다.

// set 메서드로 데이터를 전달하도록 구현하면
// 처음 Order를 생성하는 시점에 order는 완전하지 않다.
Order order = new  Order();

// set 메서드로 필요한 모든 값을 전달해야 한다.
order.setOrderLine(lines);
order.setShippingInfo(shipingInfo);

// 주문자(Orderer)를 설정하지 않은 상태에서 주문 완료 처리
order.setState(OrderState.PREPARING);
  • 도메인 객체가 불완전한 상태로 사용되는 것을 막기 위해 생성 시점에 필요한 데이터를 전달해 주어야 한다.
    • 생성자를 통해 필요한 데이터를 모두 받아주어야한다.
Order order = new Order(orderer, lines, shippingInfo, OrderState.PREPARING);

DTO의 get/set 메서드

  • DTO가 도메인 로직을 담는 경우는 없으므로 get/set 메서드를 제공해도 큰 문제는 없다.

도메인 용어

  • 코드를 작성할 때 도메인에 사용하는 용어는 매우 중요하다. - 이름 짓기의 중요성 -

  • 도메인에서 사용하는 용어를 코드에 반영하지 않으면, 그 코드는 개발자에게 코드의 의미를 해석해야하는 부담을 준다.

public OrderState {
 STEP1, STEP2, STEP3, STEP4, STEP5, STEP6
}
  • 위의 상태는 매우 메롱이다.

  • 안다. 한국인에게 영어는 어렵기 때문에 이름을 잘 짓는 것은 어렵다.

  • 그러나 적절한 영어 단어로 이름을 잘 지어두어야 코드와 도메인을 더욱 더 가깝게 유지할 수 있다.

Reference

DDD START! - 최범균님 -

그림 및 코드 참조 : https://incheol-jung.gitbook.io/docs/study/ddd-start/1



© 2022. by minkuk

Powered by minkuk