[DDD] 도메인 주도 개발 시작하기 1장

728x90

여러분이 생각하는 도메인은 무엇인가요?

제가 생각했던 도메인은 분야를 의미하는 뜻이 였습니다

ex) 개발 분야에서, 결제,헬스,의료,등등 이런 분류를 도메인이라고 생각했습니다

그럼 이책에서 말하는 도메인은 무엇일까요??

이제 한번 배워보겠습니다 ✍️

1.1 도메인이란?

온라인 서점을 예시로 들어보자

온라인 서점은 개발자가 구현해야 할 소프트웨어의 대상이 된다

ex) 상품 조회, 구매, 결제, 배송 추적 등의 기능을 제공해야 한다

-> 온라인 서점은 즉 소프트웨어로 해결하고자 하는 문제, 영역 즉 도메인에 해당한다

그리고 상위 도메인을 기준으로 하위 도메인을 생성할 수 있다.

 

위 다이어그램으로 간단하게 표현을 할 수 있습니다.

  • 카탈로그 하위 도메인은, 고객에게 구매할 수 있는 상품 목록을 제공함
  • 주문 하위 도메인은 고객의 주문을 처리한다
    • 혜텍 하위 도메인은 쿠폰이나, 할인 서비스를 제공
  • 배송 하위 도메인은 고객에게 구매한 상품을 전달하는 과정을 처리한다

등등 각자 도메인 별로 맡은 역할이 정해져있고, 주문(=상위 도메인)으로부터 기능이 엮여 있다고 볼 수 있다.

 

1.2 도메인 전문가와 개발자 간 지식 공유

개발자는 코딩만 하는게 아닌, 요구사항을 올바르게 이해할 수 있어야 한다

어떻게 하면 올바르게 요구사항을 이해할수 있을까?

개발자와 전문가가 서로 커뮤니케이션을 통해서 이해도를 높이는 것이다

 

개발자 또한 도메인 지식을 갖춰야 한다. 제품 개발과 관련된 지식을 서로 공유하고 소통을 해야

클라이언트가 원하는 제품을 만들 가능성이 높아진다.

Ex) "Garbage in, Garbage out" 을 검색해보면 자세하게 알 수 있다.

 

1.3 도메인 모델

-> 특정 도메인을 개념적으로 표현한 것 ex) ERD

도메인을 이해하기 위해서는 도메인이 제공하는 기능과 도메인 주요 데이터 구성을 파악해야 합니다.

이런 면에서 기능과 데이터를 함께 보여주는 객체 모델은 도메인을 모델랑하기에 적합합니다.

ex) 상태 다이어그램

도메인 모델을 표현할 때 클래스 다이어그램이나 상태 다이어그램과 같은 UML 표기법만 사용해야하는 것은 아닙니다

관계가 중요한 도메인이라면, 그래프를 이용해서 도메인을 모델링할 수 있습니다.

도메인 모델은 기본적으로 도메인 자체를 이해하기 위한 개념 모델입니다

개념 모델과 구현 모델은 서로 다르지만, 구현 모델이 개념 모델을 최대한 따르도록 하는게 좋습니다

ex) 객체 기반 모델을 기반으로 도메인을 표현했다면, 객체 지향 언어를 이용해 개념 모델에 가깝게 구현한다.

다시 한번 설명 하자면, 도메인은 다수의 하위 도메인으로 구성이 된다

하위 도메인 마다 다루는 영역은 서로 다르기 때문에 하나의 다이어그램에 모델링 하면 안 된다.

모델의 각 구성요스는 특정 도메인으로 한정할 때 비로소 의미가 있기에, 각 하위 도메인 마다 별도로 모델을 만들어야 한다.

 

1.4 도메인 모델 패턴

대부분 스프링 개발자들은 3Layer 아키텍쳐의 구조를 사용할 것이라고 생각합니다.

위 사진도 비슷한 구조로 이루어져있지만, 인프라가 추가 되어 있습니다.

  • 표현 : UI -> Controller
  • 응용 : Service
  • 도메인 : Entity 시스템이 제공할 도메인 규칙을 구현
  • 인프라 : db, message 시스템 등 외부 시스템과의 연동을 처리

위 모델은 도메인 모델 패턴을 의미 합니다.

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

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

주문 도메인의 경우, 출고 전에 배송지를 변경할 수 있다. or 주문 취소는 배송 전에만 할 수 있다

라는 규칙을 구현한 코드가 도메인 계층에 위치하게 된다.

=> 일한 도메인 규칙을 객체지향 기법으로 구현하는 패턴이 도메인 모델 패턴이다

public class Order {
    private OrderState state;
    private ShippingInfo shippingInfo;

    public void changeShippingInfo(ShippingInfo shippingInfo) {
        if(!state.isShippingChangeable()) {
            throw new IllegalStateException("Cannot change shipping info" + state);
        }
        this.shippingInfo = newShippingInfo;
    }
    // 비즈니스 로직
}

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;
  }
}

위 코드는 주문 도메인의 일부 기능을 도메인 모델 패턴으로 구현한 것이다.

위 코드에서 ENUM 클래스에서 대기 중, 준비 중 상태일 때는 배송 정보를 변경할 수 있게 해두었고,

나머지 상태에서는 변경을 할 수 없게 만들어 두었다.

위처럼 도메인에 규칙을 만들어 두는 것을 도메인 모델 패턴이라고 정의 할 수 있습니다.

큰 틀에서 보면 OrderState는 Order에 속한 데이터이다

왜냐하면 주문이 생성이 되야, 주문 상태 또한 생기기 때문이다.

이번에는 Order 클래스에서 배송지 정보를 알아서 판단하도록 수정한 코드를 보여주겠습니다

public class Order {
    private OrderState state;
    private ShippingInfo shippingInfo;

    public void changeShippingInfo(ShippingInfo shippingInfo) {
        if(!state.isShippingChangeable()) {
            throw new IllegalStateException("Cannot change shipping info" + state);
        }
        this.shippingInfo = newShippingInfo;
    }

    private boolean isShippingChangeable() {
        return state == OrderState.PAYMENT_WAITING || state == OrderState.PREPARING;
    }
    // 비즈니스 로직
}

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

}

위 코드 또한 방금 설명했던 코드랑 같은 동작을 하는 코드 입니다.

배송지 변경 가능 여부를 판단하는 기능이 Order에 있든 OrderState에 있든,

핵심은 중요 규칙들은 도메인 모델에서 구현한다는 점 입니다.

핵심 규칙을 규현한 코드는 도메인 모델에만 위치하기 때문에 규칙이 바뀌거나 규칙을 확장해야 할 때

다른 코드에 영향을 덜 주고 변경 내역을 모델에 반영할 수 있게 된다.

🤚개념 모델과 구현 모델

  • 개념 모델은 순수하게 문제를 분석한 결과물이다.
    • db,트랜잭션 처리,성능 등 고려하지 않고 만듬
    • 그렇기에 개념 모델을 구현 가능한 구현 모델로 전환을 해야함
  • 프로젝트 초기에는 기초 수준의 개념 모델로 도메인에 대한 전체 윤곽을 이해하는데 집중하고
    • 구현 과정에서 점진적으로 발전시켜 나가는게 좋은 방법

 

1.5 도메인 모델 추출

아무리 뛰어난 개발자라고 해도, 도메인을 이해하지 못하고 개발을 할 수는 없습니다

이 책에서 말하는 도메인은 제가 유추하기로는, 개발하려는 상품에 대한 지식을 의미한다고 생각합니다

이 책에서는 주문 도메인 을 모델링 하는 것을 다룹니다

그럼 주문 도메인을 모델링할때 의 기본 요구사항을 보겠습니다

  • 최소 한 종류 이상의 상품 주문
  • 한 상품을 한 개 이상 주문
  • 주문할 때 배송지 정보를 반드시 지정해야 한다
  • 총 주문 금액은 각 상품의 구매 가격을 합을 더한 금액
  • 출고를 하면 배송지를 변경할 수 없다
  • 출고 전에 주문을 취소할 수 있다
  • 고객이 결제를 완료하기 전에는 상품을 준비하지 않는다

등등 엄청 여러가지의 요구사항이 있을 것 입니다.

위 요구사항을 평소처럼, 레포지토리 -> 서비스를 거쳐 서비스에 비즈니스 로직을 작성하는게 아닌

도메인 에서 로직을 작성합니다

public class Order {
    // 비즈니스 로직
    public void changeShipped() { }
    public void changeShippingInfo(ShippingInfo shippingInfo) { }
    public void cancel() { }
    public void completePayment() { }
}

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

  • 한 상품을 한 개 이상 주문가능
  • 각 상품의 구매 가격 합은 상품 가격 * 구매 개수 이다.

위 요구사항을 보고, 어떻게 도메인을 구성해야 할지 그려지시나요?

잠깐 고민을 해보니, 조금은 그려지긴 합니다, 한번 코드로 보시죠

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

    public OrderLine(Product product, int price, int quantity, int amounts) {
        this.product = product;
        this.price = price;
        this.quantity = quantity;
        this.amounts = amounts;
    }
    // OrderLine을 객체로 생성할때는 무조건 파라미터 4개를 받아야한다. 즉 상품이 주문되기 위해서 4가지의 값이 필수적으로 들어간다

    private int calculateAmount() {
        return price * quantity;
    }

    public int getAmounts() {
        return amounts;
    }

}

위 요구사항에 부합하는 코드 입니다.

그럼 다음 요구사항을 보겠습니다

  • 최소 한 종류 이상의 상품을 주문해야 한다
  • 총 주문 금액은 각 상품의 구매 가격 합을 모두 더한 금액이다.

위 요구 사항은 Order 과 OrderLine과의 관계를 파악해야 합니다.

주문을 하기 위해서는, 주문 상품이 필요하고, 주문을 할 때 총 주문 금액도 필요하다.

그리고 Order는 최소 한 개 이상의 OrderLine이 있어야 합니다.

두 요구사항을 코드로 보겠습니다

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

    public Order(List<OrderLine> orderLines) {
        this.orderLines = orderLines;
    }

    private void setOrderLines(List<OrderLine> orderLines) {
        verifyAtLeastOneOrMoreOrderLiens(orderLines);
        this.orderLines = orderLines;
        calculateTotalAmounts();
    }

    private void verifyAtLeastOneOrMoreOrderLiens(List<OrderLine> orderLines) {
        if (orderLines.isEmpty()) {
            throw new IllegalArgumentException("At least one order line is required");
        }
    }

    private void calculateTotalAmounts() {
        int sum = orderLines.stream()
                .mapToInt(OrderLine::getAmounts)
                .sum();
        this.totalAmounts = new Money(sum)
    }
}

위 로직을 도메인에 작성을 할 수 있습니다.

Order는 한 개 이상의 OrderLine을 가질 수 있으므로 Order를 생성할 때 OrderLine 목록을 List로 전달합니다

생성자를 통해 제약 조건을 검사한다.

다음으로 배송지 정보 도메인을 작성해보겠습니다

@AllArgsConstructor
@Builder
@Getter
public class ShippingInfo {
    private String receiveName;
    private String receivePhoneNumber;
    private String shippingAddress1;
    private String shippingAddress2;
    private String ShippingZipcode;

}

위 코드는 롬복 어노테이션을 사용하여 모든 생성자를 생성하고, 빌더 패턴으로 만들었고

getter를 통해 나중에 객체를 가져올 수 있게 만들었다

책에는 없는 내용이지만 그냥 아는 기술을 적용해 보았습니다...

처음 요구사항 중에 '주문할 때 배송지 정보를 반드시 지정해야 한다' 는 내용이 있습니다

그러면 위 요구사항을 실천하기 위해선 어떻게 해야 할까요?

바로 Order를 생성할 때 OrderLine의 목록뿐 아니라, ShippingInfo또한 전달이 되어야 함을 의미합니다

코드로 보겠습니다

public class Order {
    private List<OrderLine> orderLines;
    private Money totalAmounts;
    private ShippingInfo shippingInfo;

    public Order(List<OrderLine> orderLines, ShippingInfo shippingInfo) {
        setOrderLines(orderLines);
      setShippingInfo(shippingInfo);
    }

    private void setOrderLines(List<OrderLine> orderLines) {
        verifyAtLeastOneOrMoreOrderLiens(orderLines);
        this.orderLines = orderLines;
        calculateTotalAmounts();
    }

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

 

생성자에 배송 정보가 포함되고, 배송 정보가 null인지 체크해주는 메소드를 만들면 된다.

이렇게 배송지 정보 필수 라는 도메인 규칙을 구현했습니다

다음으로는 도메인을 구현하다보면 특정 조건이나 상태에 따라 제약이나 규칙이 따로 적용이 될 경우가 있습니다

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

위 요구사항은 출고 상태가 되기 전과 후의 제약 사항을 말한다

그러면 위 요구사항을 충족하기 위해서는, 주문에서 최소한 출고 상태를 표현할 수 있어야 한다.

그리고 다음 요구사항도 상태와 관련이 있다

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

즉 결제 완료 전을 의미하는 상태와 결제 완료 내지 상품 준비 중이라는 상태가 필요함을 알려준다

public enum OrderState {
    PAYMENT_WAITING , PREPARING,SHIPPED, DELIVERING, DELIVERY_COMPLETED
    ,CANCELED, COMPLETED;

}

 

다음과 같이 열거타입을 활용해서 상태 정보를 표현하였습니다.

배송지 변경, 주문 취소는 출고 전에만 가능하다는 규칙이 있으니, 이 규칙을 주문 도메인에서 만든다

 

    public void changeShippingInfo(ShippingInfo shippingInfo) {
        verifyNotYetShipped();
        setShippingInfo(shippingInfo);


    }

    private void verifyNotYetShipped() {
        if (state != OrderState.PAYMENT_WAITING && state != OrderState.PREPARING) {
            throw new IllegalStateException("already shipped");
        }
    }

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

 

위 코드를 Order 클래스에 추가 함으로써 규칙을 완성할 수 있습니다.

지금 까지 주문과 관련된 요구사항을 도메인 모델로 점진적으로 만들어 봤습니다

 

⭐️문서화 ⭐

문서화를 하는 주된 이유는 지식을 공유하기 위함이다

실제 구현은 코드를 보면 알 수 있지만, 코드를 통해 S/W를 분석하기에는 많은 시간 투자가 필요합니다

그러므로 문서에 정리를 함으로써 위 과정에 시간을 단축할 수 있습니다.

코드를 보면서 도메인을 이해하게 되므로, 코드 자체도 문서화의 대상이 됩니다

도메인 지식이 잘 묻어나도록 코드를 작성하지 않으면, 코드의 동작 과정은 해석할 수 있어도 도메인 관점에서

왜 코드를 그렇게 작성했는지 이해하는데 도움이 되지 않습니다

그러므로 코드를 보기 좋게 작성하는 것 뿐만 아니라 도메인 관점에서 코드가 도메인을 잘 표현해야지

가독성이 높아지고, 문서로서 코드가 의미를 갖습니다

 


 

 

이상 도메인 모델 시작하기 책 1장에서 반절 정도를 정리해봤습니다.

신입 개발자인 저한테는 상당히 새롭고, 흥미로운 내용이였고, 위 코드를 바탕으로

JPA를 사용하여, 어떻게 연관관계를 맺을 수 있을지 라는 고민을 남겨준 예제들이였습니다.

 

평소와 다른 개발 방식이 흥미로웠고, 위 방법으로 프로젝트를 한번 진행해보고 싶다는 생각이 들었습니다. 

 

책을 읽으며, 새로운 내용을 알고 지식을 쌓는 과정이 저는 너무 흥미롭고 재밌는 것 같습니다. 감사합니다

git: https://github.com/Hyeonqz/TIL/tree/main/Architecture/DDD

 

ref : 도메인 주도 개발 시작하기 _ 저자 최범균

728x90