[DDD Start] DDD Start

CHAPTER 1 도메인 모델 시작 도메인… ………………………………………………………………………… 1 도메인 모델… …………………………………………………………………… 3 도메인 모델 패턴………………………………………………………………… 6 도메인 모델 도출……………………………………………………………… 10 엔티티와 밸류………………………………………………………………… 17 엔티티……………………………………………………………………………… 18 엔티티의 식별자 생성…………………………………………………………… 20 밸류 타입… ……………………………………………………………………… 22 엔티티 식별자와 밸류 타입……………………………………………………… 29 도메인 모델에 set 메서드 넣지 않기…………………………………………… 30 도메인 용어…………………………………………………………………… 34

CHAPTER 2 아키텍처 개요 네 개의 영역…………………………………………………………………… 37 계층 구조 아키텍처…………………………………………………………… 40 DIP……………………………………………………………………………… 44 DIP 주의사항……………………………………………………………………… 50 DIP와 아키텍처…………………………………………………………………… 52 도메인 영역의 주요 구성요소… …………………………………………… 54 엔티티와 밸류… ………………………………………………………………… 55 애그리거트………………………………………………………………………… 58 리포지터리………………………………………………………………………… 61 요청 처리 흐름………………………………………………………………… 65 인프라스트럭처 개요… ……………………………………………………… 66 모듈 구성… …………………………………………………………………… 68

목표

01. Domain

image-20190728140928012

Domain Model

도메인 모델에는 다양한 정의가 존재하는데 기본적으로 도메인 모델은 특정 도메인을 개념적으로 표현한 것이다.

Image result for 도메인 모델

즉, 도메인 모델은 기본적으로 도메인 자체를 이해하기 위한 개념 모델. 개념 모델을 이용해서 바로 코드를 작성할 수 있는 것은 아니기에 구현 기술에 맞게 구현 모델이 필요하다.

Notes

하위 도메인과 모델

도메인은 다수의 하위 도메인으로 구성된다. 각 하위 도메인이 다루는 영역은 서로 다르기 때문에 같은 용어라도 하위 도메인마다 의미가 달라질 수 있다. 예를 들어, 카탈로그 도메인의 상품이 상품 가격, 상세 내용을 담고 있는 정보를 의미한다면 배송 도메인의 상품을 고객에게 실제 배송되는 물리적인 상품을 의미.

도메인에 따라 용어의 의미가 결정되므로, 여러 하위 도메인을 하나의 다이어그램에 모델링하면 안 된다. 카탈로그와 배송 도메인 모델을 구분하지 않고 하나의 다이어그램에 함께 표시.

모델의 각 구성요소는 특정 도메인을 한정할 때 비로서 의미가 완전해지기 때문에, 각 하위 도메인마다 별도로 모델을 만들어야 한다. 이는 카탈로그 하위 도메인 모델과 하위 도메인 모델을 따로 만들어야 한다나는 것을 뜻한다.

도메인 모델 패턴

계층 설명
사용자 인터페이스 또는 표현 사용자의 요청을 처리하고 사용자에게 정보를 보여준다. 여기서 사용자는 소프트웨어를 사용하는 사람뿐만 아니라 외부시스템도 사용자가 될 수 있다.
응용(Application) 사용자가 요청한 기능을 실행한다. 업무 로직을 직접 구현하지 않으며 도메인 계층을 조합해서 기능을 실행한다.
도메인 시스템이 제공할 도메인의 규칙을 구현한다.
인스라스트럭처 데이터베이스나 메시징 시스템과 같은 외부 시스템과의 연동을 처리한다.

public class Order {
    @EmbeddedId
    private OrderNo number;

   ...

    protected Order() {
    }

    public Order(OrderNo number, Orderer orderer, List<OrderLine> orderLines,
                 ShippingInfo shippingInfo, OrderState state) {
        setNumber(number);
        setOrderer(orderer);
        setOrderLines(orderLines);
        setShippingInfo(shippingInfo);
        this.state = state;
        this.orderDate = new Date();
        Events.raise(new OrderPlacedEvent(number.getNumber(), orderer, orderLines, orderDate));
    }

...

    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 = new Money(orderLines.stream()
                .mapToInt(x -> x.getAmounts().getValue()).sum());
    }

도메인 모델 패턴이라는 것은 한 도메인 안에서 필요한 기능을 구현해야 한다.

도메인 모델 도출

도메인 모델 도출을 위해서는 기본이 되는 작업은 모델을 구성하는 핵심 구성요소. 규칙, 기능을 찾는 것.

이 과정은 요구사항에서 출발한다.

그리고 그 요구사항을 코드에 반영하면서 각 도메인에서 필요한 기능을 구현한다.

Entity & Value

도출한 모델은 크게 엔티티와 벨류로 구분.

Value 타입은 우리말로 하면 값 타입으로 표현할 수 있지만 “값”이란 단어를 여러 의미로 사용할 수 있기 때문에 이 책에서는 value를 지칭할 떄 “밸류”를 사용한다.

엔티티

벨류

ShippingInfo 클래스의 receiverName필드와 receiverPhoneNumber 필드는 서로 다른 두 데이터를 담고 있지만 두 필드는 개념적으로 받는 사람을 의미한다. 즉, 두 필드는 실제로 한 개념을 표현하고 있다.

public class ShippingInfo {
	private String receiverName;
	private String receiverPhoneNumber;
	
	private String shippingAddress1;
	private String shippingAddress2;
	private String shippingZipcode;
}

/// 변경 후
public class ShippingInfo {
    @Embedded
    @AttributeOverrides({
      @AttributeOverride(name = "zipCode", column = @Column(name = "shipping_zip_code")),
      @AttributeOverride(name = "address1", column = @Column(name = "shipping_addr1")),
      @AttributeOverride(name = "address2", column = @Column(name = "shipping_addr2"))
    })
    private Address address;
    @Column(name = "shipping_message")
    private String message;
    @Embedded
    private Receiver receiver;
  ....
    
@Embeddable
public class Address {
    @Column(name = "zip_code")
    private String zipCode;

    @Column(name = "address1")
    private String address1;

    @Column(name = "address2")
    private String address2;
  
...

엔티티 식별자와 밸류 타입

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

도메인 모델에서 get/set 메서드를 무조건 추가하는 것은 좋지 않은 버릇.

public class Order {
    @EmbeddedId
    private OrderNo number;

    @Version
    private long version;

    @Embedded
    private Orderer orderer;

    @ElementCollection
    @CollectionTable(name = "order_line", joinColumns = @JoinColumn(name = "order_number"))
    @OrderColumn(name = "line_idx")
    private List<OrderLine> orderLines;

    @Column(name = "total_amounts")
    private Money totalAmounts;

    @Embedded
    private ShippingInfo shippingInfo;

    @Column(name = "state")
    @Enumerated(EnumType.STRING)
    private OrderState state;

    @Temporal(TemporalType.TIMESTAMP)
    private Date orderDate;

    protected Order() {
    }

    public Order(OrderNo number, Orderer orderer, List<OrderLine> orderLines,
                 ShippingInfo shippingInfo, OrderState state) {
        setNumber(number);
        setOrderer(orderer);
        setOrderLines(orderLines);
        setShippingInfo(shippingInfo);
        this.state = state;
        this.orderDate = new Date();
        Events.raise(new OrderPlacedEvent(number.getNumber(), orderer, orderLines, orderDate));
    }

    private void setNumber(OrderNo number) {
        if (number == null) throw new IllegalArgumentException("no number");
        this.number = number;
    }

    private void setOrderer(Orderer orderer) {
        if (orderer == null) throw new IllegalArgumentException("no orderer");
        this.orderer = orderer;
    }

    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 = new Money(orderLines.stream()
                .mapToInt(x -> x.getAmounts().getValue()).sum());
    }

    private void setShippingInfo(ShippingInfo shippingInfo) {
        if (shippingInfo == null) throw new IllegalArgumentException("no shipping info");
        this.shippingInfo = shippingInfo;
    }

위 코드의 set메서드는 앞서 set메서드와 중요한 차이점이 있는데 그것은 바로 접근 범위가 private이라는 점. 이 코드에서 set 메서드는 클래스 내부에서 데이터를 변경할 목적으로 사용된다. private이기 때문에 외부에서 데이터를 변경할 목적으로 set메서드를 사용할 수 없다.

도메인 용어

코드를 작성할 때 도메인에서 사용하는 용어는 매우 중요하다.

어떤 클래스 이름, 함수명 이름은 그 도메인과 관계되어 결정되는 것이 중요하다.

그러나, 한국 개발자의 경우 영어이기 때문에 불리한점은 있지만, 알맞은 영어 단어를 찾기 위한 노력을 게을리해서는 안된다.

도메인패턴의 단점은 무엇일까?


아키텍처

아키텍처를 설계할 때 출현하는 전형적인 영역은

‘표현’ - ‘응용’ - ‘도메인’ - ‘인프라스트럭처’

layer

아키텍처를 설계할 때 전형적인 영역이 ‘표현, ‘응용’, ‘도메인’, ‘인프라스트럭처’ 네 개의 영역이다.

계층 구조 아키텍처

표현 영역과 응용 영역은 도메인 영역을 사용하고, 도메인 영역은 인프라스트럭처 영억을 사용하므로 계층 구조를 적용하기에 적당해 보인다. 도메인의 복잡도에 따라 응용과 도메인을 분리하기도 하고 한 게층으로 합치기도 하지만 전체적인 아키텍처는 다음과 같읕 구조를 가진다.

Image result for DDD 네 개의 영역

No Image

이런 코드는 2가지 어려움을 갖는데, 테스트 어려움기능 확장의 어려움

그러므로 이런 문제를 해결하기 위해서 DIP 를 적용한다.

image-20190728161058153

하나의 클래스가 저수준의 모듈들을 나눠서 사용해야 한다. 그런데 앞서 말한 두 가지 문제가 발생한다.

DIP는 이 문제를 해결하기 위해 저수준 모듈이 고수준 모듈에 의존하도록 바꾼다.

고수준 모듈을 구현하려면 저수준 모듈을 사용해야 하는데, 반대로 저수준 모듈이 고수준 모듈에 의존하도록 하려면 어떻게 해야 할까? 비밀은 추상화한 인터페이스에 있다.

public interface RuleDiscounter {
    public Money applyRules(Customer customer, List<OrderLine> orderLines);
}
public class CalculateDiscountService {

    private RuleDiscounter ruleDiscounter;

    public CalculateDiscountService(RuleDiscounter ruleDiscounter) {
        this.ruleDiscounter = ruleDiscounter;
    }

    public Money calculateDiscount(OrderLine orderLines, String customerId){
        Customer customer = findCustomer(customerId);

        return ruleDiscounter.applyRules(customer, orderLines);
    }
}

더이상 CalculateDiscountService 는 Drools에 의존하는 코드를 포함하고 있지 않다.다만 RoleDiscounter 가 룰을 적용한다는거만 알 뿐이다.

public class DroolsRuleDiscounter implements RuleDiscounter {
    private KieContainer kieContainer;

    public DroolsRuleDiscounter(KieContainer kieContainer) {
        KieServices ks = KieServices.Factory.get();
        this.kieContainer = ks.getKieClasspathContainer();
    }

    @Override
    public Money applyRules(Customer customer, List<OrderLine> orderLines) {
        return ... Code emit;
    }
}

dip

이렇게 의존 관계를 역전하는 것을 DIP를 적용하는 것이다. 위 그림과 같이 저수준 모듈이 고수준 모듈에 의존하게 된다. 고수준 모듈이 저수준모듈을 사용하려면 고수준 모듈이 저수준 모듈에 의존해야 하는데, 반대로 저수준 모듈이 고수준 모듈에 의존한다고 해서 이를 DIP(Dependency Inversion Principle, 의존 역전 원칙) 이라고 부른다.

그렇다고해서,

No Image

이런식으로 모듈을 나눈다면 잘못된 DIP 적용이 된것이다. 왜냐?

도메인이 영역은 구현기술을 다루는 인프라스트럭처 영역에 의존하고 있기 떄문에,

하늘색 테두리가 도메인영역에 들어가고, 파란색은 인프라영역에 들어가야지 맞는 DIP 적용예이다.

DIP와 아키텍처

인프라스트럭처 영역은 구현 기술을 다루는 저수준 모듈이고 응용 영역과 도메인 영역은 고수준 모듈이다. 인프라스트럭처 계층의 가장 하단에 위치하는 계층형 구조와 달리 아키첵터에 DIP를 적용하면 다음과 같은 인프라 영역이 응용영역과 도메인 영역에 의존하는 구조가 된다.

No Image

인프라에 위치한 클래스나 도메인이나 응용 영역에 정의한 인터페이스를 상속받아 구현하는 구조가 되므로 도메인과 응용 영역에 대한 영향을 주지 않거나 최소화하면서 구현 기술을 변경하는 것이 가능.

위 그림에서

만약, notifier를 변경해야 한다면, OrderService의 코드를 변경할 필요가 없다.

만약, Repo를 JPA로 변경하다면 OrderService의 코드 또한 변경할 필요가 없다.

image-20190728163418742

이런식으로 변경하면 된다.

도메인 영역의 주요 구성요소로는…

요소 설명
엔티티(ENTITIY) 고유의 식별자를 갖는 객체로 자신의 라이프사이클을 갖는다.
VALUE 고유의 식별자를 갖지 않는 객체로 주로 개념적으로 하나의 도메인 객체의 속성을 표현할 때 사용
AGGREGATE 애그리거트는 관련된 엔티티와 밸류 객체를 개념적으로 하나로 묶음
REPOSITORY 도메인 모델의 영속성을 처리.
DOMAIN SERVICE 특정 엔티티에 속하지 않은 도메인 로직을 제공.

저자왈

신입 시절에 처음 도메인 모델을 만들 때 DB테이블의 엔티티와 도메인 모델의 엔티티를 구분하지 못해 거의 동일하게 만들고 했다. 도메인 모델의 엔티티와 DB모델의 엔티티를(거의) 같은 것으로 생각해서 그랬는데, 경력을 더할수록 도메인 모델에 대한 이해가 쌓이면서 실제 도메인 모델의 엔티티와 DB관계형 모델의 엔티티는 같은 것이 아님을 알게 되었다.

1. 두 모델의 가장 큰 차이점은 도메인 모델의 엔티티는 데이터와 함께 도메인 기능을 함께 제공한다는 점.

2. 도메인 모델의 엔티티는 두 개 이상의 데이터가 개념적으로 하나인 경우 벨류 타입을 이용해서 표현할 수 있다는 것.

Entity와 Value

Aggregate

Repository

image-20190728165558402

요청 처리 흐름

image-20190728165733618

Infrasturcture

표현 영역, 응용 영역, 도메인 영역을 지원한다.도메인 객체의 영속성 처리, 트랜잭션, SMTP클라, REST 클라 등 다른 영역에서 필요로 하는 프레임워크, 구현 기술, 보조 기능을 지원.

모듈 구성

질문

  1. DDD를 잘 실천할줄 아는 사람은 어떤 사람일까?
  2. 도메인 모델링을 해본 경험담?
  3. 특정 도메인을 개념적으로 표현한 것이 도메인 모델.
    즉, 도메인 모델은 기본적으로 도메인 자체를 이해하기 위한 개념 모델. 개념 모델을 이용해서 바로 코드를 작성할 수 있는 것은 아니기에 구현 기술에 맞게 구현 모델이 필요하다.
  4. 모델과 도메인의 차이는?
  5. 표현 / 응용 / 도메인 / 인프라 을 어떤 방법론으로 접근해서 코딩하는가?
  6. 도메인 모델 패턴 이란? 한 도메인 안에서 필요한 기능을 모두 구현해야 하는 것.
  7. Entity / Value 를 설명할 수 있는가?
  8. 좋은 이름을 짓기 위한 나만의 노력?
  9. 도메인패턴의 단점은 무엇일까?
  10. 도메인에 넣어야 하는 기능과 서비스에 넣어야 하는 기능을 어떻게 나누는가?
  11. 도메인 모델 패턴말고 다른 패턴은 무엇?