티스토리 뷰

728x90
반응형

도메인이란?


  • 도메인이란 소프트웨어로 해결하고자 하는 문제 영역입니다. 
  • 하나의 도메인은 여러 하위 도메인의 협력을 통해 완전한 기능을 제공합니다. 도메인을 하나의 객체라는 관점으로 바라본다면 객체지향의 본질처럼 협력을 통해 공동체를 구성하고, 객체는 고립된 존재가 아닌 하나의 애플리케이션 기능을 수행하기 위해 협력하는 사회적 존재로 바라볼 수 있을거 같습니다.

메인 도메인과 하위 도메인

 

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


  • 우리가 소프트웨어로 해결하고자 하는 문제 영역에는 각 분야의 전문가가 있습니다. 예를들어 정산, 배송, 회계 영역에는 전문가가 있습니다. 해당 전문가는 해당 도메인에 대한 지식과 경험을 바탕으로 요구사항을 전달하고, 개발자들은 이런 요구사항을 토대로 개발을 진행합니다. 이때 요구사항을 제대로 분석하지 않으면 코드를 다시 작성해야 하거나 일정에 차질이 발생하게 됩니다. 또는 정보 전문가와 개발자의 커뮤니티 사이에 내용을 전파하는 전달자가 많으면 많을수록 정보가 왜곡되기 싶습니다. 이러한 문제로 인해 우리는 직접적으로 소통을 해야합니다!
  • 또한 정보 전문가와 직접 소통할수록 정보가 왜곡될 가능성은 줄어들고 더 나은 방향으로 흘러갈 수 있습니다.

💡 Garbage in, Garbage out

  • "잘못된 값이 들어가면 잘못된 결과가 나온다" 이 말은 잘못된 요구사항아 들어가면 잘못된 제품이 나온다는 의미입니다.

 

도메인 모델


  • 도메인 모델에는 다양한 정의가 존재한다고 합니다. 기본적으로는 도메인 모델은 특정 도메인을 개념적으로 표현한 것이라고 합니다.
  • 도메인 모델을 표현하는 방법에는 서로가 이해할 수 있도록 그 상황에 적합한 것을 선택하면 되는거 같습니다. 

 

💡 객체를 이용한 도메인 모델

객체를 이용한 도메인 모델

💡 상태 다이어그램을 이용한 도메인 모델

 

도메인 모델 패턴


  • 도메인 규칙을 객체지향 기법으로 구현하는 패턴을 도메인 모델 패턴이라 합니다.
  • 예를들어 주문 도메인의 경우 "출고 전에는 배송지를 변경할 수 있다" 라는 규칙과 "주문 취소는 배송 전에만 할 수 있다" 라는 규칙이 있습니다.

💡 정보 전문가를 OrderState로 선택하여 도메인 모델 패턴 구현

public class Order {
    
    private OrderState orderState;
    private ShippingInfo shippingInfo;
    
    public void changeShippingInfo(ShippingInfo shippingInfo) {
        if (!orderState.isShippingChangeable()) {
            throw new IllegalStateException("처리할 수 없는 상태입니다.");
        }
        this.shippingInfo = shippingInfo;
    }
}

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

 

💡 Order에서 변경 가능 여부 판단하는 방법

public class Order {

    private OrderState orderState;
    private ShippingInfo shippingInfo;

    public void changeShippingInfo(ShippingInfo shippingInfo) {
        if (!isShippingChangeable()) {
            throw new IllegalStateException("처리할 수 없는 상태입니다.");
        }
        this.shippingInfo = shippingInfo;
    }
    
    private boolean isShippingChangeable() {
        return orderState == OrderState.PAYMENT_WAITING || orderState == OrderState.PREPARING;
    }
}

public enum OrderState {

    PAYMENT_WAITING,
    PREPARING ,
    SHIPPED,
    DELIVERING,
    DELIVERY_COMPLETED;
}

 

도메인 모델 도출


  • 우리는 도메인에 대한 이해 없이 작업을 시작할 수 없습니다. 기획서, 스토리보드 등을 통해 요구사항을 파악하며 도메인을 이해하고
    이를 바탕으로 도메인 모델 초안을 만들어 작업을 시작할 수 있습니다.
  • 도메인을 모델링할 때 기본적인 요소는 모델을 구성하는 핵심 구성요소, 규칙, 기능을 우선적으로 찾는것입니다.
  • 도메인 모델을 도출하는 일련의 과정은 한번에 완성되는게 아닌 도메인 전문가나 다른 개발자와 논의하는 과정에서 더욱 더 발전해나갑니다.

 

💡 문서화

  • 문서화를 하는 주된 이유는 지식을 공유하기 위해서입니다. 도메인을 깊게 이해하기 위해서는 코드 자체도 문서화의 대상이 됩니다. 
    도메인 지식이 잘 묻어나도록 코드를 작성하지 않으면 코드의 동작은 이해할수 있더라도 도메인 관점에서 왜 이렇게 작성했는지 파악하기 힘듭니다. 그렇기 때문에 이쁜 코드를 만드는 것도 중요하지만 도메인 관점에서 코드가 도메인을 잘 표현해야 비로소 코드의 가독성도 높아지고 문서로서 코드가 의미를 갖습니다.

 

엔티티와 벨류


  • 도메인을 도출한 모델은 크게 엔티티(Entity)와 벨류(Value)로 구분할 수 있습니다.
  • 엔티티와 벨류를 제대로 구분해야 도메인을 올바르게 설계하고 구현할 수 있기 때문에 명확히 구분할 필요가 있습니다.

엔티티와 벨류 구분

💡 엔티티

  • 엔티티의 가장 큰 특징은 식별자를 가지는 것입니다. 식별자는 엔티티마다 고유하기 때문에 각 엔티티는 서로 다른 식별자를 가지고 있습니다.
  • 엔티티의 식별자를 생성하는 방법에는 사용하는 기술에 따라 다르지만 보통 아래와 같습니다.
    1. 회사의 특정 규칙에 의해 생성
    2. UUID나 Nano ID와 같은 고유 식별자 생성기
    3. 값을 직접 입력
    4. 데이터 베이스의 자동 증가 컬럼 사용

 

💡 밸류 타입

  • 밸류 타입이란 개념적으로 완전한 하나를 표현할 때 사용한다고 합니다. 또한 밸류 타입을 사용함으로써 의미를 명확하게 표현할 수 있습니다.

 

밸류 타입을 사용하지 않은 코드

  • 아래 주문자정보를 나타내는 코드에는 밸류 타입을 사용하지 않고 여러 변수를 가지고 있습니다. 이를 밸류 타입을 사용한다면 조금 더 명확하게 표현할 수 있습니다.
public class Orderer {

    private String receiverName;     // 받는 사람
    private String receiverPhone;    // 받는 사람 연락처
    private String receiverAddress1; // 주소 1
    private String receiverAddress2; // 주소 2
    private String receiverZipcode;  // zipcode
}

 

밸류 타입을 사용한 코드

  • 사실 밸류 타입을 사용했다 하더라도 그게 옳은 방법인지는 또는 명확한지의 판단은 그 상황에 맞게 적절히 사용해야할 거 같습니다.
    오히려 너무 잘게 쪼게면 관리해야하는 클래스만 조금 더 늘어나는게 아닌가? 라는 생각을 가지게 됩니다.
public class Orderer {

    private Receiver receiver; // 받는 사람
    private Address address;   // 주소
}

public class Receiver {

    private String name;
    private String phone;
}

public class Address {

    private String address1;
    private String address2;
    private String zipcode;
}

 

👍 밸류 타입을 사용한 좋은 예

  • 아래의 예제 코드는 밸류 타입을 적용하기에 좋은 점을 가지고 있습니다. 주문한 상품의 가격을 나타내는 price의 경우 명확하게 표현하기 위해서는 Money같은 객체 타입으로 선언하는게 좋습니다. 
  • Money 객체를 사용하여 밸류 타입을 나타낸다면 add, multiply 등 부가 기능을 제공할 때 불변성을 가질 수 있습니다. 불변성을 가지면 좋은 이유는 스레드에 안전하며 따로 동기화를 하지 않아도 됩니다. 이는 여러 스레드가 동시에 접근 시 훼손이 되지 않고 스레드에 안전합니다. 
  • 이펙티브 자바 - 아이템 17: 변경 가능성을 최소하하라
public class OrderList {
    
    private Product product; // 주문한 상품
    private int price;       // 주문한 상품의 가격
    private int quantity;    // 주문한 상품의 수량
}

// 아래와 같이 변경
public class OrderList {

    private Product product; // 주문한 상품
    private Money price;     // 주문한 상품의 가격
    private int quantity;    // 주문한 상품의 수량
}

public class Money {

    private final int value;

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

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

    public Money multiply(Money money) {
        return new Money(this.value * money.value);
    }
}

 

도메인 모델에 set 메서드 사용 줄이기


💡 Setter를 사용한 의존성 주입

  • Setter를 사용하여 의존성을 주입하게 된다면 도메인 객체를 생성할 때 온전하지 않는 상태가 될 수 있습니다.
    이를 방지하고자 생성시점에 필요한 인자를 전달해주는 게 더욱 좋습니다.
@Setter
public class Order {

    private OrderState orderState;
    private Orderer orderer;
}

@Service
public class OrderService {

    public void order() {
        Order order = new Order();
        order.setOrderState(...);
        order.setOrderer(...);
    }
}

 

💡 도메인 객체 생성 시 의존성 주입

  • 도메인 객체 생성 시 의존성을 주입하게 된다면 객체를 생성하는 시점에 컴파일 오류 및 온전한 객체를 생성할 수 있습니다. 
public class Order {

    private OrderState orderState;
    private Orderer orderer;

    public Order(@NonNull OrderState orderState, @NonNull Orderer orderer) {
        setOrderState(orderState);
        setOrderer(orderer);
        this.orderState = orderState;
        this.orderer = orderer;
    }

    private void setOrderState(OrderState orderState) {
        if (orderState == null) throw new IllegalArgumentException();
        this.orderState = orderState;
    }

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

@Service
public class OrderService {

    public void order() {

        Order order = new Order(..., ...);
    }
}

 

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


  • 코드를 작성할 때 도메인에서 사용하는 요어는 매우 중요합니다. 도메인에서 사용하는 용어를 코드에 반영하지 않으면 추후에 코드를 해석할 때 힘들어질 수 있습니다.
  • 아래 STEP_1, STEP_2 등은 어떤 의미를 포함하고 있는지 유추하기 매우 힘듭니다. 반면 PATMENT_WAITING 등은 코드만을 봤을 때 어떤 의미를 가지고 있는지 유추하기 쉽습니다.
  • 에릭 에반스는 도메인 주도 설계에서 언어의 중요함을 강조하기 위해 유비쿼터스 언어라는 용어를 사용했습니다. 전문가, 관계자, 개발자가 도메인과 관련된 공통의 언어를 만들고 이를 대화, 문서, 코드 등 모든 곳에서 사용합니다. 이렇게 하면 소통 과정에서 발생하는 용어의 모호함을 줄일 수 있고 개발자는 도메인과 코드 사이에서 불필요한 해석과정을 줄일 수 있습니다.
// 나쁜 예
public enum OrderState {
    
    STEP_1, STEP_2, STEP_3, STEP_4, STEP_5
}

// 좋은 예
public enum OrderState {

    PAYMENT_WAITING, PREPARING , SHIPPED, DELIVERING, DELIVERY_COMPLETED;
}

 

✔️ 궁금증

  • 생성자에서 Lombok의 @NonNull 어노테이션은 컴파일 체크가 가능하며, 간단한 로직에는 적용할 수 있으나 자세한 유효성 검증을 하기 위해서는 따로 메서드를 만드는게 좋습니다.
public class Order {

    private OrderState orderState;
    private Orderer orderer;

    public Order(@NonNull OrderState orderState, @NonNull Orderer orderer) {
        this.orderState = orderState;
        this.orderer = orderer;
    }
}

 

 

 

 

 

 

728x90
반응형

'스터디 > 도메인 주도 개발 시작하기' 카테고리의 다른 글

바운디드 컨텍스트  (0) 2023.02.18
응용 서비스와 표현 영역  (0) 2023.01.26
리포지터리와 모델 구현  (0) 2023.01.06
애그리게이트  (0) 2022.12.30
아키텍처 개요  (0) 2022.12.25