ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 01. 도메인 모델 시작하기
    설계방법 2024. 5. 7. 14:30

     

    1.1 도메인이란?

    도메인: 소프트웨어로 해결하고자 하는 문제의 영역

     

    1.2 도메인 모델

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

     

    주문 도메인을 생각해 본다. 쇼핑몰에서 주문을 하려면 상품을 몇 개 살지 선택하고 배송지를 입력한다.

    선택한 상품의 가격을 이용해서 지불 금액을 계산하고 금액지불을 위한 결제 수단을 선택한다. 주문 뒤에도 배송전이면 배송지 주소를 변경하거나 주문을 취소할 수 있다.

    아래는 주문 모델을 객체모델로 구성한 그림이다.

     

     

    이 모델을 보면 주문은 주문번호와 지불할 총 금액이 있고 배송정보를 변경 할 수 있음을 알 수 있다. 또한 주문을 취소 할 수 있다는 것도 알 수 있다. 도메인 모델을 사용하면 여러 관계짜들이 동일한 모습으로 도메인을 이해하고 도메인 지식을 공유하는데 도움이 된다.

     

    이렇게 기능과 데이터를 함께 보여주는 객체 모델은 도메인을 모델링하기 적합하다.

     

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

    아래는 상태 다이어그램이다. 이 상태 다이어그램을 이용하여 주문의 상태 전이를 모델링 하고 있다. 

    이 다이어 그램을 보면 상품 준비 중 상태에서 주문을 취소하면 결제 취소가 함께 이루어 진다는 것을 알 수 있다.

     

     

    1.3 도메일 모델 패턴

    일반적인 애플리케이션 아키텍처는 다음과 같이 네 개의 영역으로 구성된다.

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

     

    표현계층: 사용자 요청을 처리하고 사용자에게 정보를 보여준다. 여기서 사용자는 소프트웨어를 사용하는 사람뿐만 아니라 외부 시스템일 수 있다.

    응용계층: 사용자가 요청한 기능을 실행한다. 업무 로직을 직접 구현하지 않으며 도메인 계층을 조합해서 기능을 실행한다.

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

    인프라스트럭처: 데이터베이스나 메시징 시스템과 외부 시스템과 연동을 처리한다.

     

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

     

    도메인 계층은 도메인의 핵심 규칙을 구현한다. 주문 도메인의 경우 "출고 전에 배송지를 변경 할 수 있다"라는 규칙과 "주문 취소는 배송 전에만 할 수 있다"라는 규칙을 구현한 코드가 도메인 계층에 위치하게 된다.

    이런 도메인 규칙을 객체지향 깆법으로 구현하는 패턴이 도메인 모델 패턴이다.

     

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

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

    public enum OrderState{
       PAYMENT_WAITING{
         public boolean isShippingChangeable(){
           return true;
         }
       },
      PREPARING{
        public boolean ishippingChangeable(){
          return true;
        }
      },
      SHIPPED, DELIVERING, DELIVERY_COMPLETED;

       public boolean isShippingChangeable(){
          return false;
       }

    }

     

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

    주문 상태를 표현하는 OrderState는 배송지 변경할 수 있는지를 검사할 수 있는 isShippingChangeable()메서드를 제공하고 있다.

    코드를 보면 주문 대기중(PAYMENT_WAITING) 상태와 상품 준비 중(PREPARING)상태의 isShippingChangeable() 메서드는 true를 리턴한다.

    즉 OrderState는 주문 대기 중이거나 상품 준비 중에는 배송지를 변경할 수 있다는 도메인 규칙을 구현하고 있다.

     

    실제 배송지 정보를 변경하는 Order클래스의 changeShippingInfo() 메서드는 OrderState의 isShippingChangeable() 메서드를 이용해서 변경 가능한 경우에만 배송지를 변경한다.

    큰 틀에서 보면 OrderState는 Order에 속한 데이터이므로 배송지 정보 변경 가능 여부를 판단하는 코드를 Order로 이동할 수도 있다.

    다음은 Order클래스에서 판단하도록 수정한 코드를 보여주고 있다.

     

      private OrderState state;
      private ShippingInfo shippingInfo;

      public void changeShippingInfo(ShippingInfo newShippingInfo){
        if(!state.isShippingChangeable()) {
          throw new IllegalStateException("can't change shipping in"+state);
        }
        this.shippingInfo = newShippingInfo;
      }
      public boolean isShippingChangeable(){
          return state==OrderState.PAYMENT_WAITTING || state==OrderState.PREPARING;
       }
    }

    public enum OrderState{
      PAYMENT_WATING, PREPARING, SHIPPED, DELIVERING, DELIVERY_COMPLETED;
    }

     

    배송지 변경이 가능한지를 판단할 규칙이 주문상태와 다른 정보를 함꼐 사용한다면 OrderState만으로는 배송지 변경 가능 여부를 판단할 수 없으므로 Order에서 로직을 구현해야 한다.

    배송지 변경 가능 여부를 판단하는 기능이 Order에 있던 OrderState에 있던 중요한 점은 주문과 관련된 중요 업무 규칙을 주문 도메인 모델인 Order나 OrderState에서 구현한다는 점이다.

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

     

    1.5 도메인 모델 도출

    도메인에 대한 이해 없이는 코딩이 불가하다. 기획서,유스케이스,사용자 스토리와 같은 요구사항과 관련자와 대화를 통해 도메인을 이해하고 이를 바탕으로 도메인 모델 초안을 만들어야 한다.

     

    도메인 모델링시 기본이 되는 작업은 모델을 구성하는 핵심 구성요소, 규칙,기능을 찾는 것이다,. 이 과정은 요구사항에서 출발한다.

    주문 도메인과 관련된 몇 가지 요구사항을 보자.

     

    요구사항

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

     

    여기서 알 수 있는 것은 주문은

    1.출고 상태로 변경하기

    2.배송지 정보 변경하기

    3.주문 취소하기

    4.결제 완료하기

    의 기능을 제공한다는 것이다. 이 것을 Order에 메서드로 추가한다.

     

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

     

    그리고 2번 4번의 요구사항은 주문 항목이 어떤 데이터로 구성되어 있는지 알려준다.

    2.한 상품을 한 개 이상 주문할 수 있다.

    4.각 상품의 구매 가격 합은 상품 가격에 구매 개수를 곱한 값이다.

     

    두 요구사항에 따르면 주문 항목을 표현하는 OrderLine은 적어도 주문할 상품,상품의 가격,구매 개수를 포함해야 한다.

    추가로 각 구매 항목의 구매 가격도 제공해야 한다. 이를 구현한 OrderLine은 다음과 같다.

     

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

      public OrderLine(Product product,int price, int quantity){
        this.product = product;
        this.price = price;
        this.quantity = quantity;
        this.amount = calculateAmount();
      }

      private int calculateAmounts(){
        return price * quantity;
      }
      public int getAmount(){}
    ....
    }

     

    OrderLine은 한 상품(product)를 얼마(price)에 몇 개 살지(quantity)를 담고 있고 calculateAmounts()메서드를 통해 가격을 구하는 로직을 구현하고 있다.

     

    요구사항 1,3번은 Order과 OrderLine과의 관계를 알려준다.

    1.최소 한 종류 이상의 상품을 주문해야 한다.

    3.총 주문 금액은 각 상품의 구매 가격 합을 모두 더한 금액이다.

     

    한 종류 이상의 상품을 주문할 수 있으므로 Order는 최소 한 개 이상의 OrderLine을 포함해야 한다. 또한 총 주문 금액은 OrderLine에서 구할 수 있다. 두 요구사항은 Order에 다음과 같이 반영할 수 있다.

     

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

      public Order(List<OrderLine> orderLines){
        setOrderLines(orderLines);
      }

      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 calculateTotalAmount(){
        int sum = orderLines.stream().mapToInt(x->x.getAmount()).sum();
        this.totalAmounts = new Money(sum);
      }
    ...
    }

     

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

    생성자에서 호출하는 setOrderLines() 메서드는 요구사항에 정의한 제약 조건을 검사한다.

    요구사항에 따르면 최소 한 종류 이상의 상품을 주문해야 하므로 verifyAtLeastOneOrMoreOrderLines() 메서드를 이용해서 OrderLine이 한 개 이상 존재하는지 검사한다.

    또한 calculateTotalAmount()를 이용해서 총 주문금액을 계산한다.

     

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

     

    public class ShippingInfo{
       private String recevierName;
       private String recevierPhoneNumber;
       private String shippingAddress1;
       private String shippingAddress2;
       private String shippingZipCode;
    ...
    }

     

    앞 서 요구사항 5번에 "주문할 때 배송지 정보를 반드시 지정해야 한다." 라는 내용이 있다. 

    Order생성시 OrderLine목록 뿐 아니라 ShippingInfo도 함께 전달해야 한다는 것을 의미한다.

     

    public class Order{
      private List orderLines;
      private Money totalAmount;
      private ShippingInfo 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");
        this.shippingInfo = shippingInfo;
      }
    ...
    }

     

    생성자에서 호출하는 setShippingInfo 메서드는 ShippingInfo가 null이면 익셉션이 발생하는데 이로서 "배송지 정보 필수"라는 도메인 규칙을 구현한다.

     

    도메인을 구현하다 보면 특정 조건이나 상태에 따라 제약이나 규칙이 달리 적용되는 경우가 많다. 주문 요구사항에는 다음 내용이 제약과 규칙에 해당된다.

     

    7.출고를 하면 배송지를 변경할 수 없다.
    8.출고 전에 주문을 취소할 수 있다.

     

    이 요구사항은 출고 상태가 되기 전과 후의 제약사항을 기술하고 있다. 출고 상태에 따라 배송지 정보 변겨여 기능과 주문 취소 기능은 다른 제약을 갖는다. 이 요구사항을 충족하려면 주문은 최소한 출고 상태를 표현할 수 있어야 한다.

    9번 요구사항도 상태와 관련있다.

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

     

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

    다른 요구사항을 확인해서 추가로 존재할 수 있는 상태를 분석한 뒤,다음과 같이 열거 타입을 이용해서 상태 정보를 표현할 수 있다.

     

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

     

    배송지 변경이나 주문 취소 기능은 출고 전에만 가능하다는 제약 규칙이 있으므로 이 규칙을 적용하기 위해 changeShippingInfo()와 cancle()은 verifyNotYetShipped() 메서드를 먼저 실행한다.

     

    public class Order {
      private OrderState state;

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

      public void cancle(){
        verifyNotYetShipped();
        this.state = OrderState.CANCLED;
      }

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

     

    1.5 엔티티와 밸류

    도출한 모델은 크게 엔티티와 밸류로 구분할 수 있다.

    앞 서 요구사항 분석 과정에서 만드느 모델은 엔티티도 존재하고 밸류도 존재한다.

     

    엔티티와 밸류를 제대로 궂분해야 도메인을 올바르게 설계하고 구현할 수 있기 때문에 이 둘의 차리를 명확하게 이해하는 것은 도메인을 구현하는 데 있어 중요하다.

     

    1.5.1 엔티티

    가장 큰 특징은 식별자를 가진다는 것!이다. 식별자는 엔티티 객체마다 고유해서 각 엔티티는 서로 다른 식별자를 갖는다.

    예를 들어 모든 주문의 주문번호는 다 다르다. 이 주문번호는 식별자가 된다.

    주문에서 배송지주소가 바뀌거나 상태가 바뀌더라도 주문번호가 바뀌지 않는 것처럼 엔티티의 식별자는 바뀌지 않는다.

    엔티티를 생성하고 속성을 바꾸고 삭제할 때까지 식ㅂ졀자는 유지된다.

     

    엔티티의 식별자는 바뀌지 않고 고유하기 때문에 두 엔티티 객체의 식ㅈ별자가 같으면 두 엔티티는 같다고 볼 수 있다.

    엔티티를 구현한 클래스는 다음과 같이 식별자를 이용해서 equals() 메서드와 hashCode() 메서드를 구현할 수 있다.

     

    public class Order{
      private String orderNumber;
      
      @Override
       public boolean equals(Object obj){
         if(this == obj) return true;
         if(obj == null) return false;
         if(obj.getClass() != Order.class) return false;
         Order other = (Order)obj;
         if(this.orderNumber == null) return false;
         return this.orderNumber.equals(other.orderNumber); 
       }

      @Override
      public int hashCode(){
        final int prime=31;
        int result = 1;
        result = prime*result+((orderNumber == null ) ?0 : orderNumber.hashCode());
        return result;
      }
    }

     

    1.5.2 엔티티의 식별자 생성

    엔티티의 식별자를 생성하는 시점은 도메인의 특징과 사용하는 기술에 따라 달라진다. 흔히 식별자는 다음 중 한가지 방식으로 생성한다.

    1. 특정 규칙에 따라 생성
    2. UUID나 Nano ID와 같은 고유 식별자 생성기 사용
    3. 값을 직접 입력
    4. 일련번호 사용(시퀀스나 DB의 자동 증가 칼럼 사용)

     

    주문번호, 운송장번호, 카드번호와 같은 식별자는 특정 규칙에 따라 생성한다. 이 규칙은 도메인에 따라 다르고,같은 주문번호라도 회사마다 다르다. 예를 들어 최근에 두 온라인 서점에서 구매한 책의 주문번호는 각각 '2011129393940'와 '111-A1233r9595'인데 두 번호의 구조가 완전히 다른 것을 알 수 있다.

     

    식별자 생성시 주의할 점은 같은 시간에 동시에 식별자를 생성해도 같은 식별자가 만들어지면 안된다는 것이다.

    UUID를 사용해서 식별자를 생성할 수 있다. 다수의 개발 언어가 UUID생성기를 제공하고 있으므로 마땅한 규칙이 없다면 UUID를 식별자로 사용해도 된다.

    자바는 java.util.UUID클래스를 사용해서 UUID를 생성할 수 있다.

     

    1.5.3 밸류타입

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

     

    receiverName필드와 recevierPhoneNumber필드는 서로 다른 두 데이터를 담고 있지만 두 필드는 개념적으로 받는 사람을 의미한다.

    즉 두 필드는 실제로 하나의 개념을 표현하고 있다.

    비슷하게 Address1,2,ZipCode도 주소라는 하나의 개념을 표현하고 있다.

     

    밸류 타입은 개념적으로 완전한 하나를 표현할 때 사용한다. 예를 들어 받는 사람을 위한 밸류 타입인 Receiver를 다음과 같이 작성할 수 있다.

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

      public Receiver(String name,String phoneNumber){
        this.name = name;
        this.phoneNumber = phoneNumber;
      }

      public String getName(){
        return name;
      }

      public String getPhoneNumber(){
        return phoneNumber;
      }
    }

     

    Recevier는 '받는 사람'이라는 도메인 개념을 표현한다. 앞서 ShippingInfo의 ReciverName필드와 recevierPhoneNumber 필드가 이름을 갖고 받는 사람과 관련된 데이터라는 것을 유추한다면 Receiver는 그 자체로 받는 사람을 뜻한다. 밸류 타입을 사용함으로써 개념적으로 완전한 하나를 잘 표현할 수 있는 것이다.

     

    ShippingInfo의 주소 관련 데이터도 다음의 Address 밸류 타입을 사용해서 보다 명확하게 표현할 수 있다.

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

      public Address(String address1, String address2, String zipcode){
        this.address1 = address1;
        this.address2 = address2;
        this.zipcode = zipcode;
      }
    }

     

    밸류 타입을 이용해서 ShippingInfo클래스를 다시 구현해보자. 배송 정보가 받는 사람과 주소로 구성된다는 것을 쉽게 알 수 있다.

    public class ShippingInfo{
      private Receiver receiver;
      private Address address;
    ...생성자 ,get 메서드
    }

     

    밸류 타입이 꼭 두 개 이상의 데이터를 가져야 하는 것은 아니다. 의미를 명확하게 표현하기 위해 밸류타입을 사용하는 경우도 있다.

    이를 위한 좋은 예가 OrderLine이다.

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

     

    OrderLine의 price와 amounts는 int타입의 숫자를 사용하고 있지만 이들은 '돈'을 의미하는 값이다.

    따라서 '돈'을 의미하는 Money타입을 만들어 사용하면 코드를 이해하는 데 도움이 된다.

    public class Money{
      private int value;

      public Money(int value){
        this.value = value;
      }
    }

     

    다음은 Money를 사용하도록 OrderLine을 변경한 코드이다. Money타입 덕에 price나 amounts가 금액을 의미한다는 것을 쉽게 알 수 있다.

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

     

    밸류 타입의 또 다른 장점은 밸류 타입을 위한 기능을 추가할 수 있다는 것이다. 예를 들어 Money타입은 다음과 같이 돈 계산을 위한 기능을 추가할 수 있다.

    public class Money{
      private int value;
      ..생성자, getValue()

      public Money add(Money money){
        return new Money(this.value+money.value);
      }

      public Moeny multiply(int multiplier){
        return new Money(value*multiplier);
      }
    }

     

    Money를 사용하는 코드는 이제 "정수 타입 연산"이 아니라 "돈 계산"이라는 의미로 코드를 작성할 수 있다.

     

    밸류 객체의 데이터를 변경할 때는 기존 데이터를 변경하기 보다는 변경한 데이터를 갖는 새로운 밸류 객체를 생성하는 방식을 선호한다.

    위의 Money클래스의 add() 메서드를 보면 Money를 새로 생성하고 있다.

     

    Money처럼 데이터 변경 기능을 제공하지 않는 타입을 불변(Immutable)이라고 표현한다. 밸류 타입을 불면으로 구현하는 이유는 여러 이유가 있는데 가장 중요한 이유는 안전한 코드를 작성할 수 있다는데 있다.

     

    OrderLine을 예로 들어보자. OrderLine을 생성하려면 다음 코드처럼 Money객체를 전달해야 한다.

    Money price = ...;
    OrderLine line = new OrderLine(product,price,quantity);

     

    만약 Money가 setValue와 같은 메서드를 제공해서 값을 변경할 수 있다면 어떻게 될까?

    참조투명성 문제로 price값이 잘못 반영되는 상황이 발생하게 된다.

    이런 문제를 방지하기 위해 OrderLine생성자는 다음과 같이 새로운 Money객체를 생성하도록 코드를 작성해야 한다.

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

      public OrderLine(Product product,Money price, int quantity){
        this.product = product;
        this.price = new Money(price.getValue());
        this.quantity = quantity;
        this.amounts = calculateAmounts();
      }
    }

     

    Money가 불변이면 이런 코드를 작성할 필요가 없다. Money의 데이터를 바꿀 수 없기 때문에 파라미터로 전달받은 price를 안전하게 사용할 수 있다.

     

    두 밸류 객체를 비교할 때는 모든 속성이 같은지 비교한다.

    public class Receiver{
      private String name;
      private String phoneNumber;
      
      public boolean equals(Object other){
        if(other == null) return false;
        if(this == other) return true;
        if(! (other instanceof Receiver)) return false;
        Receiver that = (Receiver)other;
        return this.name.equals(that.name) && this.phoneNumber.equals(that.phoneNumber)
      }
    }

     

    1.5.4 엔티티 식별자와 밸류 타입

    엔티티 식별자의 실제 데이터는 String과 같은 문자열로 구성된 경우가 많다. 신용카드 번호도 16개의 숫자로 구성된 문자열이며 많은 온라인 서비스에서 회원을 구분할 때 사용하는 이메일 주소도 문자열이다.

     

    Money가 단순 숫자가 아닌 도메인의 '돈'을 의미하는 것처럼 이런 식별자는 단순한 문자열이 아니라 도메인에서 특ㅈ별한 의미를 지니는 경우가 많기 때문에 식별자를 위한 밸류 타입을 사용해서 의미가 잘 드러나도록 할 수 있다. 예를 들어 주문번호를 표현하기 위해 Order의 식별자 타입으로 String대신 OrderNo 밸류 타입을 사용하면 타입을 통해 해당 필드가 주문번호라는 것을 알 수 있다.

     

    public class Order{
      private OrderNo id;
    ...
      public OrderNo getId(){
        return id;
      }
    }

     

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

    도메인 객체가 불완전한 상태로 사용되는 것을 막으려면 생성 시점에 필요한 것을 만들어 전달해 주어야 한다.

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

    Order order = new Order(orderer, lines, shippingInfo, OrderState.PREPARING);

     

    생성자로 필요한 것을 모두 받으므로 다음처럼 생성자를 호출하는 시점에 필요한 데이터가 올바른지 검사할 수 있다.

    public class Order {
      public Order(Orderer orderer, List<OrderLine> orderLines,ShippingInfo shippingInfo, OrderState state){
        setOrderer(orderer);
        setOrderLines(orderLines);
      }

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

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

      private void verifyAtLeastOneOrMoreOrderLines(List<OrderLine> orderLines){
        if(orderLines ==null || orderLines.isEmpty()) {
          throw new IllegalArgumentException("no OrderLine");
        }
      }

      private void calculateTotalAmounts(){
        this.totalAmount = orderLines.stream().mapToInt(x -> x.getAmounts()).sum();
      }
    }

     

    이 코드의 set 메서드는 앞서 언급한 set 메서드와 큰 차이가 있는데 그것은 접근제어자가 private라는 것이다. 이 코드에서 set메서드는 클래스 내부에서 데이터를 변경할 목적으로 사용된다.

     

    불변 밸류 타입을 사용하려면 자연스럽게 밸류 타입에는 set메서드를 구현하지 않는다. set 메서드를 구현해야 할 특별한 이유가 없다면 불변 타입의 장점을 살릴 수 있도록 밸류 타입은 불변으로 구현한다.

     

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

     

    코드를 작성할 때 도메인에서 사용하는 언어는 매우 중요하다. 도메인에서 사용하는 용어를 코드에 반영해야한다.

    만약 그렇지 않고 다음과 같이 구현했다고 가정해보자

    public OrderState{
      STEP1,STEP2,STEP3,STEP4,STEP5.STEP6
    }

     

    실제 주문 상태는 '결제 대기 중','상품 준비 중','출고 완료됨','배송 중', '배송 완료됨','주문 취소됨'인데 코드는 그냥 개발자가 전체 상태를 6단계로 보고 6단꼐로 표현한 것이다. 이 개발자는 다음과 같이 Order코드를 작성할 확률이 높다.

    public class Order{
      public void changeShippingInfo(ShippingInfo newShippingInfo){
        verifyStep1OrStep2();
        setShippingInfo(newShippingInfo);
      }

      private void verifyStep1OrStep2(){
        if(state != OrderState.STEP1 && state != OrderState.STEP2) throw new illegalStateException("aleady shipped");
      }
    }

     

    의미를 이해하려면 STEP1과 STEP2가 의미하는 바를 알아야 한다.

    이걸 알려면 기획서를 뒤져야한다. 굳이 그럴 필요 없이 다음과 같이 도메인 용어를 사용해서 코드를 작성한다면 그럴 필요가 없다.

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

     

    에릭 에반스는 도메인 주도 설계에서 언어의 중요함을 강조하기 위해 유비쿼터스언어라는 용어를 사용했다. 전문가,관계자 , 개발자가 도메인과 관련된 공통의 언어를 만들고 이를 대화,문서,도메인모델,코드,테스트,등 모든 곳에서 같은 용어를 사용한다. 이렇게 소통하여 소통과정에서 발생하는 용어의 모호함을 줄이고 개발자는 도메인과 코드 사이에서 불필요한 해석 과정을 줄일 수 있다.

     

     

    '설계방법' 카테고리의 다른 글

    이벤트 스토밍  (0) 2024.05.11
    04.리포지터리와 모델 구현  (0) 2024.05.11
    03. 애그리거트  (0) 2024.05.09
    02.아키텍처 개요  (0) 2024.05.07
    DDD(도메인 주도 개발 시작하기) 정리(작성 중)  (0) 2024.05.04
Designed by Tistory.