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

DaeHoon·2023년 2월 28일
0

1-1. 도메인이란?

  • 온라인 서점을 예로 들어 도메인을 설명해보자
    • 개발자 입장에서 온라인 서점은 구현해야 할 소프트웨어의 대상이 된다. 온라인 서점 소프트웨어는 상품 조회, 구매, 결제, 배송 추적 등의 기능을 제공해야 한다. 이때 온라인 서점은 소프트웨어로 해결하고자 하는 문제 영역, 즉 도메인에 해당된다.
    • 도메인은 다시 하위 도메인으로 나눌 수 있다. 예를 들면 온라인 서점 도메인은 주문, 회원, 결제, 배송 등 하위의 도메인으로 나눌 수 있다.
    • 나눠진 하위 도메인은 다른 하위 도메인과 연동하여 완전한 기능을 제공한다. 예를 들어 고객이 물건을 구매하면 주문, 결제, 배송, 혜택 하위 도메인의 기능이 엮이게 된다.
    • 하지만 도메인이 제공해야 할 모든 기능을 구현할 필요는 없다. 결제 같은 경우는 외부 PG를 가져다 연동하거나, 배송은 외부 물류 업체와 연계하여 도메인을 구성할 수 있다.
    • 하위 도메인을 어떻게 구성할지 여부는 상황에 따라 달라진다. B2B 회사 같은 경우는 온라인으로 카탈로그를 제공하고 주문서를 받는 정도만 필요한 반면 B2C 같은 경우에는 고객을 물건을 판매하고 카탈로그, 리뷰, 주문, 결제 등 여러가지의 하위 도메인이 필요할 것이다.

1-2. 도메인 전문가와 개발자 간 지식 공유

  • 도메인 전문가가 요구사항을 요청하면, 개발자는 이런 요구사항을 분석하고 설계한다.
  • 요구사항을 올바르게 이해하기 위해 개발자와 도메인 전문가의 원활한 소통이 필요하다. 또한 이를 위해 개발자도 도메인 지식을 갖출 필요가 있다.

1-3. 도메인 모델

  • 도메인 모델은 특정 도메인을 개념적으로 표현한 것이다
    • 도메인 모델을 사용하면 여러 관계자들이 동일한 모습으로 도메인을 이해하고 도메인 지식을 공유하는데 도움이 된다.
  • 도메인 모델을 표현하는 방법들은 여러가지가 존재한다. 중요한 부분이 무엇인가에 따라서 표현하는 방식도 달라진다. 예를 들면,
    • 객체간 관계가 중요하면 Class Diagram 이용하여 도메인을 표현한다.
    • 상태가 중요하면 State Diagram을 이용하여 표현한다.
    • 관계가 중요한 도메인이면 그래프를 이용해서 표현한다.
    • 계산 규칙이 중요하다면 수학 공식을 이용해서 표현한다.
  • 도메인에 따라 용어 의미가 결정되므로 여러 하위 도메인을 하나의 다이어그램에 모델링하면 안된다.
    • 예를 들어 카탈로그와 배송 도메인 모델을 구분하지 않고 하나의 다이어그램에 표시하면, 다이어그램에 표시한 상품은 카탈로그의 상품과 배송의 상품 의미를 함께 제공하기에, 카탈로그 도메인의 상품을 제대로 이해하는 데 방해가 된다.
    • 모델의 구성요서는 특정 도메인으로 한정할 때 의미가 완전해진다. 결과적으로 카탈로그 하위 도메인 모델과 배송 하위 도메인 모델을 따로 만들어야 한다.

1-4. 도메인 모델 패턴

  • 도메인 모델의 아키텍처는 아래와 같다.
    • Presentation: 사용자의 요청을 처리하고 사용자에게 정보를 보여준다. 여기서 사용자는 소프트웨어를 사용하는 사람뿐만 아니라 외부 시스템일수도 있다.
    • Application: 사용자가 요청한 기능을 실행한다. 업무 로직을 직접 구현하지 않으며 도메인 계층을 조합해서 기능을 실행한다.
    • Domain: 시스템이 제공할 도메인 규칙을 구현한다.
    • Infrastructure: 데이터베이스나 메시징 시스템과 같은 외부 시스템과의 연동을 처리한다.
  • 도메인과 관련된 중요 업무 규칙은 도메인 모델에서 구현한다. 예를 들어 주문 도메인의 경우 '출고 전에 배송지를 변경할 수 있다.' 라는 규칙과 '주문 취소는 배송 전에만 할 수 있다.' 라는 규칙을 구현한 코드가 도메인 계층에 위치하게 된다.
 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;
	}
}
  • 위의 코드는 주문 도메인의 일부 기능을 도메인 모델 패턴으로 구현한 것이다. 주문 상태를 표현하는 OrderState는 배송지를 변경할 수 있는지를 검사할 수 있는 isShippingChangeable()을 메서드를 제공하고 있다.
  • 실제 배송지 정보를 변경하는 Order 클래스의 changeShippingInfo() 메서드는 OrderState 클래스의 isShippingChangeable의 메서드를 이용해서 변경 가능한 경우에만 배송지를 변경한다.
  • 큰 틀에서 보면 OrderState는 Order에 속한 데이터이므로 배송지 정보 변경 가능 여부를 판단하는 코드를 Order로 이동할 수도 있다.
  • 배송지 변경 가능 여부를 판단하는 기능이 Order에 있든, OrderState에 있든 중요한 점은 주문과 관련된 중요 업무 규칙을 주문 도메인 모델에서 구현한다는 점이다. 핵심 규칙을 구현한 코드는 도메인 모델에만 위치하기 때문에 규칙이 바뀌거나 규칙을 확장해야 할 때 다른 코드에 영향을 덜 주고 변경 내역을 모델에 반영할 수 있게 된다.

1-5. 도메인 모델 도출

  • 해당 책 참고

1-6. 엔티티와 밸류

  • 도출한 모델은 크게 Entity와 Value로 구분할 수 있다.

1-6-1. 엔티티 (Entity)

  • 엔티티는 식별자를 가진다.
    • 예를 들어 주문 도메인에서 각 주문은 주문번호를 가지고 있는데 각 주문마다 주문번호가 다르다. 따라서 주문번호가 주문의 식별자가 된다.
    • 엔티티의 식별자는 바뀌지 않는다.
  • 엔티티의 식별자는 바뀌지 않고 고유하므로 두 엔티티의 식별자가 같으면 두 엔티티는 같다고 판단할 수 있다.
    • 엔티티를 구현한 클래스는 다음과 같이 식별자를 이용해서 equals 메서드와 hashCode 메서드를 구현할 수 있다.

1-6-2. 엔티티의 식별자 생성

  • 엔티티의 식별자를 생성하는 시점은 도메인의 특징과 사용하는 기술에 따라 달라진다.
    • 특정 규칙에 따라 생성 (주문번호, 운송장번호, 카드번호 등, 예를 들어 온라인 서점에서 구매한 책의 주문번호는 현지 시간과 다른 값을 조합해서 식별 번호를 만든다.)
    • UUID나 Nano ID와 같은 고유 식별자 생성기 사용 (마땅한 규칙이 없을 때)
    • 값을 직접 입력 (회원 아이디, 이메일)
    • 일련번호 사용(시퀀스나 DB의 자동 증가 칼럼 사용)
// 엔티티를 생성하기 전에 식별자 생성
String orderNumber = orderRepository.generateOrderNumber();

Order order = new Order(orderNumber, ...);
orderRepository.save(order)
  • 자동 증가 칼럼을 제외한 다른 방식은 다음과 같이 식별자를 먼저 만들고 엔티티 객체를 생성할 때 식별자를 전달한다.
Article article = new Article(author, title, ...);
articleRepository.save(article); // DB에 저장한 뒤 구한 식별자를 엔티티에 반영
Long savedArticleId = article.getId(); // DB에 저장한 후 식별자 참조 가능
  • 자동 증가 칼럼은 DB 테이블에 데이터를 삽입해야 비로소 값을 알 수 있으므로 테이블에 데이터를 추가하기 전까지는 식별자를 알 수 없다. 따라서 엔티티 객체를 생성할 때 식별자를 전달 할 수 없다.

1-6-3. 밸류 타입

  • ShippingInfo 클래스는 받는 사람과 주소에 대한 데이터를 갖고 있다.
    • receiverName과 receiverPhoneNumber 필드는 서로 다른 두 데이터를 담고 있지만 두 필드는 개념적으로 받는 사람을 의미한다. 즉 두 필드는 하나의 개념을 표현하고 있다.
    • 마찬가지로 shppingAddress1, shppingAddress2, shippingZipcode 필드는 주소라는 하나의 개념을 표현하고 있다.
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;
    }
}

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;
    }
}
  • Receiver 클래스는 받는 사람이라는 도메인 개념을 표현하고 Address는 주소라는 도메인 개념을 표현하고 있다.
  • Value Type을 이용해 배송정보가 받는 사람과 주소로 구성된다는 것을 쉽게 알 수 있다.
public class OrderLine {
    private Product product;
    private Money price;
    private int quantity;
    private Money amounts;
}



public class Money {
    private int value;

    ... 생성자, getValue()

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

    public Money multiply(int multiplier) {
        return new Money(value * multiplier);
    }
}
  • 밸류 타입이 꼭 두 개 이상의 데이터를 가져야 하는 것은 아니다. 의미를 명확하게 표현하기 위해 밸류 타입을 사용하는 경우도 있다.
  • 위의 코드에서 OrderLine 클래스에 price와 amount는 돈을 의미하는 값이다. 이를 명확하게 표현하기 위해 Money라는 밸류 타입을 만들어 지정했다.
  • 밸류 타입의 또 다른 장점은 밸류 타입을 위한 기능을 추가할 수 있다는 것이다. 예를 들어 Money 타입은 돈 계산을 위해 add와 muliply이라는 돈 계산 관련 기능을 만들었다.
  • 밸류 객체의 데이터를 변경할 때는 기존 데이터를 변경하기보다는 변경한 데이터를 갖은 새로운 밸류 객체를 생성하는 방식을 선호한다. 위의 Money는 데이터 변경 기능을 제공하지 않는다. 이처럼 데이터 변경 기능을 제공하지 않는 타입을 불변 immutable이라고 한다.

밸류 타입을 불변으로 구현하는 이유

Money price = ...;
OrderLine line = new OrderLine(product, price, quantity);
// 만약 price.setValue(0)로 값을 변경할 수 있다면?
  • 만약 setValue 메서드로 값을 변경할 수 있다면?
Money price = new Money(1000);
OrderLine line = new OrderLine(product, price, 2);
price.setValue(2000); 
  • 결과가 참조 투명하지 않다.
  • OrderLine의 price 값이 잘못 반영되는 상황이 발생하게 된다.
public class OrderLine{
 	...
    private Money price;
    
    public OrderLine(Product product, Money price, int quantity){
    	this.product = product;
        // Money가 불변 객체가 아니라면,
        // price 파라미터가 변경될 때 발생하는 문제를 방지하기 위해
        // 데이터를 복사한 새로운 객체를 생성해야 한다.
        this.price = new Money(price.getValue());
        this.quantity = quantity;
        this.amounts = calculateAmount();
    }
}
  • Money가 불변이면 위와 같은 코드를 작성할 필요가 없다.

1-7. 도메인 용어와 유비쿼터스 언어

profile
평범한 백엔드 개발자

0개의 댓글