도메인 주도 개발 시작하기 - 1. 도메인 모델 시작하기

 Date: 2022-09-12

1.1 도메인이란?
1.2 도메인 전문가와 개발자 간 지식 공유
1.3 도메인 모델
1.4 도메인 모델 패턴
1.5 도메인 모델 도출
1.6 엔티티와 밸류
1.7 도메인 용어와 유비쿼터스 언어

1.1 도메인이란?

  • 도메인이란, 소프트웨어로 해결하고자 하는 문제 영역이다.

    ex) 온라인 서점은 상품조회, 구매, 결제, 배송 추적 등의 기능을 제공해야 한다.

  • 한 도메인은 다시 하위 도메인으로 나눌 수 있다.
    • 온라인 서점은 주문, 카탈로그, 리뷰, 회원, 결제, 혜택, 정산 등 여러 하위 도메인으로 구성된다.
    • 각 하위 도메인이 다루는 영역은 서로 다르기 때문에 같은 용어라도 하위 도메인마다 의미가 달라질 수 있다.
  • 한 하위 도메인은 다른 하위 도메인과 연동하여 완전한 기능을 제공한다.
    • 고객이 물건을 구매하면 주문, 결제, 배송, 혜택 하위 도메인의 기능이 엮이게 된다.
  • 특정 도메인을 위한 소프트웨어라고 해서 도메인이 제공해야 할 모든 기능을 구현하는 것은 아니다.
    • 쇼핑몰에서 자체적으로 배송 시스템을 구축하기보다 외부 배송 업체의 시스템을 사용하고, 배송 추적에 필요한 기능만 일부 연동한다.
  • 도메인마다 고정된 하위 도메인이 존재하는 것은 아니다.
    • 쇼핑몰에서 혜택을 제공하지 않을 수 있고, 엑셀 같은 도구로 수작업으로 정산할 수도 있다.
  • 하위 도메인을 어떻게 구성할지 여부는 상황에 따라 달라진다.
    • 어떤 고객(B2B, B2C)을 대상으로 할지에 따라 하위 도메인 구성이 달라진다.

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

  • 각 영역의 전문가들은 도메인에 대한 지식과 경험을 바탕으로 기능 개발을 요구한다.
  • 개발자는 이런 요구사항을 분석하고 설계하는데, 이때 요구사항을 올바르게 이해하는 것이 중요하다.
  • 요구사항을 이해하기에 비교적 간단한 방법은 개발자와 전문가와 직접 대화하는 것이다.

1.3 도메인 모델

  • 도메인 모델은 특정 도메인을 개념적으로 표현한 것이다.

  • 도메인을 이해하려면 도메인이 제공하는 기능과 도메인의 주요 데이터 구성을 파악해야 한다.
    • 객체 모델은 도메인을 모델링하기에 적합하다.
    • 클래스 다이어그램, 상태 다이어그램, 그래프, 수학 공식 등 도메인을 이해하는데 도움이 되는 표현 방식은 무엇이든 사용할 수 있다.
  • 도메인 모델은 기본적으로 도메인 자체를 이해하기 위한 개념 모델이고, 개념 모델이 있더라도 구현 기술에 맞는 구현 모델이 따로 필요하다.
    • 개념 모델과 구현 모델은 서로 다른 것이지만 구현 모델이 개념 모델을 최대한 따르도록 할 수는 있다.
    • 개념 모델은 순수하게 문제를 분석한 결과물이므음로음 처음부터 완벽한 개념 모델을 만들기보다는 전반적인 개요를 알 수 있는 수준으로 개념 모델을 작성해야 한다.
    • 구현하는 과정에서 도메인을 더 잘 이해하게 되고, 새로운 지식이 쌓이면서 모델을을 보완하거나 변경하며 점진적으로 발전시켜 나가야 한다.

1.4 도메인 모델 패턴

  • 일반적인 애플리케이션의 아키텍처에서 의미하는 도메인 계층에서의 도메인 모델은 마틴 파울러가 쓴 ⟪엔터프라이즈 애플리케이션 아키텍처 패턴⟫의 도메인 모델 패턴을 의미한다.
  • 도메인 계층은 도메인의 핵심 규칙을 구현한다.
  • 도메인 규칙을 객체 지향 기법으로 구현하는 패턴이 도메인 모델 패턴이다.

1.5 도메인 모델 도출

  • 도메인을 모델링할 때 기본이 되는 작업은 모델을 구성하는 핵심 구성요소, 규칙, 기능을 찾는 것이다.

      - 최소 한 종류 이상의 상품을 주문해야 한다. ➡️ List<OrderLine>
      - 한 상품을 한 개 이상 주문할 수 있다. ➡️ OrderLine.quantity
      - 총 주문 금액은 각 상품의 구매 가격 합을 모두 더한 금액이다. ➡️ Order.calculateTotalAmounts()
      - 각 상품의 구매 가격 합은 상품 가격에 구매 개수를 곱한 값이다. ➡️ OrderLine.calculateAmounts()
      - 주문할 때 배송지 정보를 반드시 지정해야 한다. ➡️ ShippingInfo
      - 배송지 정보는 받는 사람 이름, 전화번호, 주소로 구성된다. ➡️ ShippingInfo
      - 출고를 하면 배송지를 변경할 수 없다. ➡️ OrderState
      - 출고 전에 주문을 취소할 수 있다. ➡️ OrderState
      - 고객이 결제를 완료하기 전에는 상품을 준비하지 않는다. ➡️ OrderState
    
  • 도메인을 구현하다 보면 특정 조건이나 상태에 따라 제약이나 규칙이 달리 적용되는 경우가 많다.

      - 출고를 하면 배송지 정보를 변경할 수 없다. ➡️ PAYMENT_WAITING, PREPARING
      - 출고 전에 주문을 취소할 수 있다. ➡️ PAYMENT_WAITING, PREPARING
      - 고객이 결제를 완료하기 전에는 상품을 준비하지 않는다. ➡️ PAYMENT_WAITING
    
  • 이렇게 도출된 도메인 모델은 도메인 전문가나 다른 개발자 등 누구나 쉽게 접근하도록 문서화 하면 좋다.
  • 코드를 보면서 도메인을 깊게 이해하게 되므로 코드 자체도 문서화의 대상이 된다.
    • 도메인 지식이 잘 묻어나도록 코드를 작성하지 않으면 코드의 동작 과정은 해석할 수 있어도 도메인 관점에서 왜 코드를 그렇게 작성했는지 이해하는데는 도움이 되지 않는다.

1.6 엔티티와 밸류

  • 도출한 모델은 크게 엔티티와 밸류로 구분할 수 있다.
    • 엔티티와 밸류를 제대로 구분해야 도메인을 올바르게 설계하고 구현할 수 있다.

1.6.1) 엔티티

  • 엔티티의 가장 큰 특징은 식별자를 가진다는 것이다.
  • 엔티티의 식별자는 바뀌지 않고 고유하기 때문에 두 엔티티 객체의 식별자가 같으면 두 엔티티는 같다고 판단할 수 있다.

      @Override
      public boolean equals(Object obj) {
          if (this == obj) return true;
          if (!(obj instanceof Order)) return false;
          Order order = (Order) obj;
          return Objects.equals(number, order.number);
      }
        
      @Override
      public int hashCode() {
          int result = 1;
          result = 31 * result + ((number == null) ? 0 : number.hashCode());
          return result;
      }
    

1.6.2) 엔티티의 식별자 생성

  • 엔티티의 식별자를 생성하는 시점은 도메인의 특징과 사용하는 기술에 따라 달라진다.
  • 흔히 식별자는 다음 중 한 가지 방식으로 생성한다.
    • 특정 규칙에 따라 생성
      • 주문번호, 운송장번호, 카드번호
      • 흔히 사용하는 규칙은 현재 시간과 다른 값을 함께 조합하는 것인데, 같은 시간에 동시에 식별자를 생성하더라도 같은 식별자가 만들어지지 않도록 주의해야 한다.
    • UUID, Nano ID와 같은 고유 식별자 생성기 사용
      • UUID.randomUUID()
    • 값을 직접 입력
      • 아이디, 이메일
    • 일련번호 사용 (시퀀스나 DB의 자동 증가 컬럼)
      • 주로 데이터베이스가 제공하는 자동 증가 기능을 사용한다.

1.6.3) 밸류 타입

  • 밸류 타입은 개념적으로 완전한 하나를 표현할 때 사용한다.

      public class Address {
          private String zipCode;
          private String address1;
          private String address2;
      }
    
  • 밸류 타입이 꼭 두 개 이상의 데이터를 가져야 하는 것은 아니다.

      public class Money {
          private int value;
      }
    
  • 밸류 타입을 사용하면 밸류 타입을 위한 기능을 추가할 수 있다.

      private Money calculateAmounts() {
          return price.multiply(quantity);
      }
    
  • 밸류 객체의 데이터를 변경할 때는 기존 데이터를 변경하기보다는 변경한 데이터를 갖는 새로운 밸류 객체를 생성하는 방식을 선호한다.

      public Money multiply(int multiplier) {
          return new Money(value * multiplier);
      }
    
  • 밸류 타입을 불변으로 구현하는 가장 중요한 이유는 안전한 코드를 작성할 수 있다는데 있다.
  • 밸류 객체를 비교할 때는 모든 속성이 같은지 비교한다.

      @Override
      public boolean equals(Object obj) {
          if (this == obj) return true;
          if (!(obj instanceof Receiver)) return false;
          Receiver receiver = (Receiver) obj;
          return Objects.equals(name, receiver.name) && Objects.equals(phone, receiver.phone);
      }
    

1.6.4) 엔티티 식별자와 밸류 타입

  • 엔티티 식별자의 실제 데이터는 String과 같은 문자열로 구성된 경우가 많은데, 식별자에도 밸류 타입을 사용해서 의미가 잘 드러나도록 할 수 있다.
    • 주문번호를 표현하기 위해 String id 대신 OrderNo id를 사용하면 실제 의미를 파악하기 쉽다.

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

  • 도메인 모델에 get, set 메서드를 무조건 추가하는 것은 좋지 않은 버릇이다.
  • 특히 set 메서드는 도메인의 핵심 개념이나 의도를 코드에서 사라지게 한다.
  • set 메서드의 또 다른 문제는 도메인 객체를 생성할 때 온전하지 않은 상태가 될 수 있다.
    • 도메인 객체가 불완전한 상태로 사용되는 것을 막으려면 생성 시점에 필요한 것을 전달해 주어야 한다

      즉, 생성자를 통해 필요한 데이터를 모두 받아야 한다.

  • set 메서드를 사용한다면, 접근 범위를 private으로 설정하여 내부에서 데이터를 변경할 목적으로 사용하고, 외부에서 데이터를 변경하지 못하도록 해야 한다.

      private void setOrderer(Orderer orderer) {
      		if (orderer == null) throw new IllegalArgumentException("no orderer");
      		this.orderer == orderer;
      }
    
  • DTO 에서도 set 메서드가 아닌 private 필드에 직접 값을 할당할 수 있게 발전되어 불변의 장점을 DTO까지 확장할 수 있다.

1.7 도메인 용어와 유비쿼터스 언어

  • 코드를 작성할 때 도메인에서 사용하는 용어는 매우 중요하다.
  • 도메인 용어를 사용해서 코드를 작성하면, 의미 변환 과정에서 발생하는 시간과 버그가 줄어든다.
  • 에릭 에반스는 유비쿼터스 언어라는 용어를 사용했는데, 모든 관계자가 같은 용어를 사용하면 소통 과정에서 용어의 모호함으로 발생하는 불필요한 해석 과정을 줄일 수 있다.
  • 알맞은 영단어를 찾는 것은 쉽지 않은 일이지만 시간을 들여 찾는 노력을 해야 한다.
    • 상태: state/status, 종류: kind/type/gubun(구분)