티스토리 뷰

programming/DDD

[DDD] chapter 1

AGAPE1225 2024. 1. 13. 22:20
반응형

chapter 1

1.1 도메인이란?

  • 도메인: 소프트웨어로 해결하고자 하는 문제 영역
  • 한 도메인은 다시 하위 도메인으로 나눌 수 있다.
    • 하나의 하위 도메인은 다른 하위 도메인과 연동하여 완전한 기능을 제공한다.
  • 특정한 도메인을 위한 소프트웨어라고 해서 모든 기능을 직접 구현하지는 않는다.
  • 하위 도메인을 어떻게 구성할지 여부는 상황에 따라 달라진다.
    • 도메인마다 고정된 하위 도메인이 존재하는 것은 아니다.

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

요구사항의 중요성

  • 개발자는 요구사항을 분석하고 설계하여 코드를 작성하고 테스트하고 배포한다.
  • 요구사항을 처음부터 제대로 설계하지 않으면 코드를 수정해야한다.
    • 잘못 개발한 코드를 수정하는 것은 많은 비용이 소모된다.

요구사항을 올바르게 이해하고 설계하는 방법

  • 개발자와 전문가가 직접 대화하는 것이 가장 좋은 방법이다.
  • 도메인 전문가까지는 아니더라도, 이해관계자와 개발자도 도메인 지식을 갖춰야 한다.
  • 제품 개발과 관련된 도메인 전문가, 관계자, 개발자가 같은 지식을 공유하고 직접 소통할수록 도메인 전문가가 원하는 제품을 만들 가능성이 높아진다.

1.3 도메인 모델

도메인 모델의 정의

  • 도메인 모델: 특정 도메인을 개념적으로 표현한 것.
  • 도메인 모델은 기본적으로 도메인 자체를 이해하기 위한 개념 모델이다.
    • 개념 모델를 통해 바로 코드를 작성할 수 없기 때문에 구현 기술에 맞는 구현 모델이 필요하다.
      • ex) 객체 기반 모델을 기반으로 도메인을 표현했다면 객체 지향 언어를 이용해 개념 모델에 가깝게 구현할 수 있다.
  • 도메인 모델의 종류
    • 객체
    • 다이어그램
    • 그래프
    • 수학 공식

1.4 도메인 모델 패턴

  • 어플리케이션 아키텍쳐의 구성
    • 사용자 인터페이스 또는 표현: 사용자의 요청을 처리하고 사용자에게 정보 제공
    • 응용: 사용자가 요청한 기능을 실행
    • 도메인: 시스템이 제공할 도메인 규칙 구현
    • 인프라스트럭쳐: 데이터베이스나 메시징 시스템과 같은 외부 시스템과의 연동 처리
  • 새로운 도메인 모델의 개념
    • 도메인 모델 == 도메인 모델 패턴: 도메인 규칙을 객체 지향 기법으로 구현하는 패턴
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 isShippingChangeable(){
            return true;
        }
    },
    SHIPPED, DELIVERING, DELIVERY_COMPLETED;

    public boolean isShippingChangeable(){
        return false;
    }
}
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;
    }
    
    private boolean isShippingChangeable(){
        return state == OrderState.PAYMENT_WAITING || state == OrderState.PREPARING;
    }
}

public enum OrderState {
    PAYMENT_WAITING, PREPARING, SHIPPED, DELIVERING, DELIVERY_COMPLETED;
}
  • 위 두개의 코드에서 가장 중요한점은 배송지 변경 가능 여부를 판단하는 기능이 Order에 있든, OrderState에 있든 중요한 점은 주문과 관련된 중요 업무 규칙을 주문 도메인 모델인 Order나 OderState에서 구현한다는 점이다.
  • 핵심 규칙을 구현한 코드는 도메인 모델에만 위치하기 때문에 규칙이 바뀌거나 규칙을 확장해야 할 때 다른 코드에 영향을 덜 주고 변경 내역을 모델에 반영할 수 있게 된다.

1.5 도메인 모델 도출

도메인 모델 작성 예시

  • 요구사항에서 모델을 구성하는 핵심 구성요소, 규칙, 기능을 먼저 찾는다.
    • 예시 요구사항
      • 최소 한 종류 이상의 상품을 주문해야 한다.
      • 한 상품을 한 개 이상 주문할 수 있다.
      • 총 주문 금액은 각 상품의 구매 가격 합을 모두 더한 금액이다.
      • 각 상품의 구매 가격 합은 상품 가격에 구매 개수를 곱한 값이다.
      • 주문할 때 배송지 정보를 반드시 지정해야 한다.
      • 배송지 정보는 받는 사람 이름, 전화번호, 주소로 구성된다.
      • 출고를 하면 배송지 정보를 변결할 수 없다.
      • 출고 전에 주분을 취소할 수 있다.
      • 고객이 결제를 완료하기 전에는 상품을 준비하지 않는다.
    • 주문의 기능 추리기
      • 출고 상태로 변경하기
      • 배송지 정보 변경하기
      • 주문 취소하기
      • 결제 오나료하기
    • Order 객체의 메소드로 정의하기
    • public class Order { public void changeShipped() {} public void changeShippingInfo(ShippingInfo newShipping){} public void cancel(){} public void completePayment(){} }
    • OrderLine 구현하기
    • public class OrderLine { private Product product; private int price; private int quantity; private int amounts; public OrderLine(Product product, int price, int quantity){ this.product = product; this.price = price; this.quantity = quantity; this.amounts = calculateAmounts(); } private int calculateAmounts() { return price * quantity; } }
    • OrderLine과 Order의 관계를 정의한 Order 업데이트
    • ShippingInfo 생성 후 Order에 적용 
    • 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) { verifyAtLeastOneOrMoreOrderLines(orderLines); this.orderLines = orderLines; calculateTotalAmounts(); } private void setShippingInfo(ShippingInfo shippingInfo){ if(shippingInfo == null) throw new IllegalArgumentException("no ShippingInfo"); this.shippingInfo = shippingInfo; } private void verifyAtLeastOneOrMoreOrderLines(List<OrderLine> orderLines){ if(orderLines == null || orderLines.isEmpty()){ throw new IllegalArgumentException("no OrderLine"); } } private void calculateTotalAmounts(){ int sum = orderLines.stream().mapToInt(x -> x.getAmounts()).sum(); this.totalAmounts = new Money(sum); } }
    • public class ShippingInfo { private String receiverName; private String receiverPhoneNumber; private String shippingAddress1; private String shippingAddress2; private String shippingZipcode; }
    • 주문 상태 메소드 수정
      public enum OrderState {
          PAYMENT_WAITING, PREPARING, SHIPPED, DELIVERING, DELIVERY_COMPLETED,
          CANCELED;
      }
      
public class Order {
    private List<OrderLine> orderLines;
    private Money totalAmounts;
    private ShippingInfo shippingInfo;
    private OrderState state;

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

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

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

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

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

    public void changeShippingInfo(ShippingInfo newShipping){
        verifyNotYetShipped();
        setShippingInfo(newShipping);
    }
    public void cancel(){
        verifyNotYetShipped();
        this.state = OrderState.CANCELED;
    }

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

 

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

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

1.6 엔티티와 밸류

  • 도출한 모델은 Entity와 Value로 구분할 수 있다.
  • 이 둘을 제대로 구분해야 도메인을 올바르게 설계하고 구현할 수 이기 때문에 이 둘의 차이를 명확핳게 이해해야한다.

1.6.1 Entity

  • 도메인 모델 내에서 식별자를 가지는 하나의 Class
  • Entity의 식별자를 사용하는 예시 코드
  • 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 order = (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.6.2 엔티티의 식별자 생성

  • 엔티티의 식별자를 생성하는 시점은 도메인의 특징과 사용하는 기술에 따라 달라진다.
  • 식별자 생성 방식
    • 특정 규칙
      • 주문번호
      • 운송장번호
      • 카드번호
    • UUID나 Nano ID와 같은 고유 식별자 생성기 사용
    • 값을 직접 입력
      • 회원 아이디
      • 회원 이메일
    • 일렬번호 사용(시퀀스나 DB Auto Increment)
      • 주로 DB에서 제공하는 기능

1.6.3 Value type

  • Value type은 개념적으로 완전한 하나를 표현할 때 사용된다.
  • Receiver Value type 작성
  • 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; } }
  • address value type 작성 후 코드 가독성 강화
    public class ShippingInfo {
        private Receiver receiver;
        private Address address;
        /*private String receiverName;
        private String receiverPhoneNumber;
        private String shippingAddress1;
        private String shippingAddress2;
        private String shippingZipcode;*/
    }
    
  • 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; } }
  • Money value type 생성 후 가독성 강화
    public class OrderLine {
        private Product product;
        private Money price;
        private int quantity;
        private Money amounts;
    }
    
  • public class Money { private int value; public Money(int value){ this.value = value; } public int getValue(){ return value; } }
  • Value type의 장점 예시 (Money 타입을 활용한 기능 추가)
    • Money를 사용하는 코드에서 “정수형 연산”이 아닌 “돈 계산”이라는 의미로 코드를 작성할 수 있다.
    • 벨류 객체의 데이터를 변경할 때는 기존 데이터를 변경하는 것이 아닌 새로운 밸류 객체를 생성하는 방식을 선호함
      • Money의 add() 메소드의 경우 new 메소드를 사용해 새로운Money객체를 생성하였다.
  • public class Money { private int value; public Money(int value){ this.value = value; } public int getValue(){ return value; } public Money add(Money money){ return new Money(this.value + money.value); } public Money multiply(int multiplier){ return new Money(value * multiplier); } }
  • Money처럼 데이터 변경 기능을 제공하지 않는 타입을 불변이라고 표현한다.
  • Value type을 불변으로 구현하는 이유는 안전한 코드를 위해서이다.
    • OrderLine에 Price값이 잘못 반영된다.
  • Money price = new Money(1000); OrderLine line = new OrderLine(product, price, 2); price.setValue(2000);

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

  • Entity의 식별자 같은 경우 String과 같은 문자열로 구성된 경우가 많다.
  • Money가 단순 숫자가 아닌 도매인 ‘돈’을 의미하는 것처럼 식별자도 Value type을 사용해 구현할 수 있다.

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

  • 도메인 모델에 get/set 메서드를 무조건 추가하는 것은 좋지 않다.
  • 특히 set 메서드는 도메인의 핵심 개념이나 의도를 코드에서 사라지게 한다.
    • chageShippingInfo() → setShippingInfo()
      • 배송지 정보를 새로 변경한다는 의미에서 단순히 배송지 값을 설정한다는 의미를 가짐.
    • completePayment() → setOrderState()
      • 결제 완료했다는 의미에서 주문 상태 값을 설정한다는 의미를 가짐.
  • set 메서드는 도메인 객체를 생성할 때 온전하지 않은 상태가 될 수 있다.
    Order order = new Order(order, lines, shippingInfo, OrderState.PREPARING);
    
    • 첫번째 코드는 주문자 설정을 누락하고 있다.
    • 마지막 코드처럼 생성 시점에 필요한 것을 전달해 주어야 한다.
      • 생성자로 필요한 것을 모두 받으므로 생성자를 호출하는 시점에 필요한 데이터가 올바른지 검사할 수 있다.
    • Type value의 set 메서드는 접근 범위를 private로 함으로서 외부에서는 변경할 수 없도록 한다.
  • Order order = new Order(); order.setOrderLine(lines); order.setShippingInfo(shippingInfo); order.setState(OrderState.PREPARING);
  • 불변 밸류 타입을 사용하면 set 메서드를 구현하지 않는다.
  • set 메서드를 구현해야 할 특별한 이유가 없다면 밸류 타입은 불변으로 구현한다.

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

  • 도메인에서 사용되는 언어는 개발자의 코드 의미 해석과 관련되어 있기에 매우 중요하다. 
    • 위의 코드처럼 도메인의 용어를 사용해 버그와 시간을 줄일 수 있다.
  • public enum OrderState { STEP1, STEP2, STEP3, STEP4, STEP5, STEP6; }
  • public enum OrderState { PAYMENT_WAITING, PREPARING, SHIPPED, DELIVERING, DELIVERY_COMPLETED, CANCELED; }
  • 유비쿼터스 언어는 용어의 모호함을 줄일 수 있고 도메인과 코드 사이에서 불필요한 해석 과정을 줄일 수 있다.
  • 도메인 용어에 시간을 아까워하지 말자.
반응형

'programming > DDD' 카테고리의 다른 글

[DDD] chapter 6  (0) 2024.02.10
[DDD] chapter 5  (1) 2024.02.07
[DDD] chapter 4  (0) 2024.02.03
[DDD] chapter 3  (0) 2024.01.18
[DDD] chapter2  (1) 2024.01.13
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/01   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함