DDD - 도메인 모델 시작

2020-12-02

도메인 모델 시작

0. 도메인

도메인이란, 소프트웨어로 해결하고자 하는 문제 영역이다. 한 도메인은, 몇 개의 하위 도메인으로 나눌 수 있다.

특정 모메인을 위한 소프트웨어라고 해서 도메인이 제공해야 할 모든 기능을 구현하는것은 아니다. 자체적인 시스템을 구축하기 보다 외부 연동을 통해 시스템을 구축하기도 한다. (ex. oauth, PaaS, …)


1. 도메인 모델

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

도메인 모델을 사용하면 여러 관계자들이 동일한 모습으로 도메인을 이해하고 도메인 지식을 공유하는데 도움된다.

도메인 모델은 기본적으로 도메인 자체를 이해하기 위한 개념 모델이다. 개념 모델을 이요해서 바로 코드를 작성할 수 있는 것은 아니기에 구현 기술에 맞는 모델이 따로 필요하다.

예를들어, 객체 기반 모델을 이용해서 도메인모델을 표현했다면 객체 지향 언어를 이용해서 개념 모델에 가깝게 구현할 수 있다.

비슷하게 수학적 모델을 사용한다면 함수를 이용해 도메인 모델과 유사한 구현 모델을 만들 수 있다.


2. 도메인 모델 패턴

일반적인 애플리케이션의 아키텍처는 네 개의 계층으로 구성 된다.

계층(layer) 설명
사용자 인터페이스 또는 표현 사용자의 요청을 처리, 사용자에게 정보를 보여준다. 외부 시스템도 사용자가 될 수 있다.
응용 사용자가 요청한 기능을 수행한다. 업무 로직을 직접 구현하지 않으며 도메인 계층을 조합해서 기능을 실행 한다.
도메인 시스템이 제공할 도메인의 규칙을 구현한다.
인프라스트척처 데이터베이스나 메시징 시스템과 같은 외부 시스템과의 연동을 처리한다.

도메인 계층은 도메인의 핵심 규칙을 구현한다.

주문 도메인의 경우 ‘출고 전에 배송지를 변경할 수 있다’ 또는 ‘주문 취소는 배송 전에만 가능하다’ 라는 규칙의 코드가 도메인 계층에 위치하게 된다. (음원재생 도메인의 경우 ‘권리가 있으면 재생할 수 있다’ 와 같은 규칙)

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

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

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


3. 도메인 모델 도출

천재 개발자라 할지라도 도메인에 대한 이해 없이 코딩을 시작할 수 없다. 구현을 시작하려면 도메인에 대한 초기 모델이 필요하다. 도메인을 모델링 할 때 기본 작업은 모델을 구성하는 핵심 구성 요소, 규칙, 기능을 찾는 것이다.


0. 엔티티와 밸류

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

1. 엔티티

엔티티의 가장 큰 특징은 식별자를 갖는다는 것이다. 엔티티 객체마다 서로 다른 식별자를 갖는다.

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

따라서 두 엔티티의 식별자가 같으면 같은 엔티티라고 판단할 수 있다. equals()hashCode() 를 통해 같은 식별자인지 확인할 수 있다.


엔티티의 식별자 생성

흔히 식별자는 다음 중 한가지 방식으로 생성된다.

  • 특정 규칙에 따라 생성

주문번호, 운송장번호, 카드번호와 같은 식별자는 특정 규칙에 따라 생성한다. 이 규칙은 도메인에 따라 다르고 팀마다 다르다

보통 사용하는 규칙은 현재 시간과 다른 값을 함께 조합하는 것이다. (예/ 2020122514211000001 -> 2020년12월25일14시21분11초000001번) 주의할 점은 같은 식별자가 만들어지면 안된다는 것이다.

  • UUID 사용

UUID를 사용해서 식별자를 생성할 수 있다. 자바의 경우 java.util.UUID를 사용하면 된다.

UUId uuid = UUID.randomUUI();
  • 값을 직접 입력

회원의 아이디나 이메일과 같은 식별자는 값을 직접 입력 한다. 사용자가 직접 입력하는 값이기 때문에 중복해서 입력하지 않도록 하는것이 중요하다.

  • 일련번호 사용

주로 DB가 제공하는 자동 증가 기능을 사용한다. 오라클의 시퀀스, MySQL의 auto increment가 이에 해당한다.

자동 증가 컬럼을 제외한 다른 방식은 다음과 같이 식별자를 먼저 만들고 엔티티 객체를 생성할때 식별자를 전달할 수 있다.

String orderNumber = orderRepository.generate();
Order order = new Order(orderNumber, ...);
orderRepository.save(order);

자동 증가 칼럼은 디비에 데이터를 삽입해야 비로소 값을 알 수 있기 때문에 데이터를 추가 하기전에는 식별자를 알 수 없다.

이는 엔티티 객체를 생성할 때 식별자를 전달할 수 없음을 뜻한다.

Article article = new Article(autor, title, ...);
articleRepository.save(article);
Long savedArticleId = article.getId();

2. 밸류 타입

public class ShippingInfo {
    // 받는 사람
    private String receiverName;
    private String receiverPhoneNumber;

    // 주소
    private String shippingAddress1;
    private String shippingAddress2;
    private String shippingZipCode;
}

ShippingInfo 클래스의 receiverName 필드와 receiverPhoneNumber 필드 는 서로 다른 두 데이터를 담고 있지만, 개념적으로 받는 사람을 뜻한다.

비슷하게 shippingAddress1, shippingAddress2, shippingZipCode 또한 주소라는 하나의 개념을 뜻한다.

따라서 아래와 같이 밸류 타입인 Receiver, Adress를 작성할 수 있다.

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

public class Address {
    private String address1;
    private String address2;
    private String zipCode;
}

밸류 타입을 이용해서 아래와 같이 리팩토링할 수 있다.

public class ShippingInfo {
    // 받는 사람
    private Receiver receiver;

    // 주소
    private Adress adress;
}

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

public class Money {
    private int value;
}

밸류 타입을 사용할 때의 또 다른 장점은 밸류 타입을 위한 기능을 추가할 수 있다.

예를 들어, Money 타입은 돈 계산을 위한 기능을 추가할 수 있다.

public class Money {
    private int value;

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

위 코드를 통해 정수 타입 연산이 아닌 돈 계산 이라는 의미로 코드를 작성할 수 있다.

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

Money처럼 데이터 변경 기능을 제공하지 않는 타입을 불변(immutable)이라고 표현한다.

밸류 타입을 불변으로 구현하는 이유는 여러 가지가 있는데, 가장 중요한 이유는 불변 타입을 사용하면 보다 안전한 코드를 작성할 수 있다.

엔티티 타입의 두 객체가 같은지 비교할 때 주로 식별자를 사용한다면, 두 밸류 타입이 같은지 비교할 때는 모든 속성이 같은지 비교해야 한다.


엔티티 식별자와 벨류 타입

Money가 단순 숫자가 아닌 도메인의 ‘돈’을 의미하는 것 처럼 식별자는 단순한 문자열이 아니라 도메인에서 특별한 의미를 지니는 경우가 많다.

예를들어, 주문번호를 표현하기 위해 Order의 식별자 타입으로 String 대신 OrderNo 밸류 타입을 사용하면 타입을 통해 해당 필드가 주문 번호임을 알 수 있다.

public class Order {
    private OrderNo id;

    public OrderNo getId() {
        return id;
    }
}

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

도메인 모델에 get/set 메소드를 무조건 추가하는 것은 좋지 않은 버릇이다. 특히 set 메서드는 도메인의 핵심 개념이나 의도를 코드에서 사라지게 한다.

public class Order {
    public void setShippingInfo(ShippingInfo shippingInfo) { }
    public void setOrderState(OrderState state) { }
}

cahngeShippingInfo 가 배송지 정보를 새로 변경한다는 의미를 가진다면, setShippingInfo 는 단순히 배송지 값을 설정한다는 뜻이다.

completePayment 는 배결제를 완료했다는 의미를 갖는 반면에, setOrderState는 단순히 주문 상태 값을 설정한다는것을 뜻한다.

습관적으로 코드를 작성하는 경우라면 필드 값만 변경하고 끝나는 경우가 많기 때문에 상태 변경과 관련된 도메인 지식이 코드에서 사라지게 된다.

set 메소드의 또 다른 문제는 도메인 객체를 생성했을때 완전한 상태가 아닐 수 있다.

// set 메소드로 데이터를 전달하도록 구현
Order order = new Order();

order.setOrderLine(lines);
order.setShippingInfo(shippingInfo);
// 주문자(Orderer)를 설정하지 않은 상태에서 주문 완료 처리
order.setState(OrderState.PREPARING);

위 코드는 주문자 설정을 누락하고 있다. 도메인 객체가 불완전한 상태로 사용되는것을 막으려면 생성 시점에 필요한 것을 전달해 주어야 한다.

// 생성자를 통한 데이터 전달
Order order = new Order(orderer, lines, shippingInfo, OrderState.PREPARING);

// 또는 Builder 패턴을 통한 데이터 전달

Order order = Order.byBuilder().orderer(orderer).orderLines(lines).shippingInfo(shippingInfo).state(OrderState.PREPARING).build();

생성자로 필요한 것을 모두 받으면, 생성자를 호출하는 시점에 유효성 검사를 할 수 있다.

public class Order {
    public Order(Orderer orderer, ...) {
        setOrderer(orderer);
    }

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

클래스 내부에서 데이터를 변경할 목적으로 사용하는 private set 메서드는 사용해도 괜찮다.

불변 밸류타입을 사용하면 자연스럽게 밸류 타입에는 set 메소드를 구현하지 않는다.


번외

get 메소드 줄이기 - 참고 링크

일급 컬렉션 - 참고 링크


0. 도메인 용어

코드를 작성할 때 도메인에서 사용하는 용어를 코드에 반영하지 않으면 그 코드는 개발자에게 코드의 의미를 해석해야하는 부담을 준다.

예를들어 OrderState를 다음과 같이 구현했다고 가정해보자

public OrderState{
    STEP1, STEP2, STEP3, STEP4
}

실제 주문 상태는 ‘결제대기중’, ‘상품준비중’, ‘출고완료됨’, ‘배송중’, ‘배송완료’ 됨 인데 이 상태를 단계로 보고 표현한 것이다.

public OrderState{
    PAYMENT_WAITNG, PREPARING, SHIPPED, DELIVERING, ...
}

위와 같은 코드를 사용하면 도메인 지식 없이도 어떤 상태인지를 알 수 있게 된다.

그러니 도메인 용어에 알맞은 단어를 찾는 시간을 아까워 하지 말자.