본문 바로가기

독후감/도메인 주도 개발 시작하기

1. 도메인 모델 시작하기

도메인 모델 패턴

애플리케이션 아키텍처는 아래와 같이 4개의 영역으로 구성된다.

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

영역 설명
Presentation 사용자의 요청을 처리하고 사용자에게 정보를 보여준다.
Application 사용자가 요청한 기능 실행(업무 로직을 직접 구현하지 않으며 도메인 계층을 조합해서 기능을 구성)
Domain 시스템이 제공할 도메인 규칙 구현
Infrastructure DB나 메시징 시스템과 같은 외부 시스템과의 연동 처리

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

Entity 와 Value

Entity

엔티티의 가장 큰 특징은 식별자를 가지는 것이다.

엔티티의 식별자는 아래와 같이 생성할 수 있다.

  • 특정 규칙에 따라 생성
  • UUID나 Nano ID와 같은 고유 식별자 생성기 사용
  • 값을 직접 입력
  • 일련번호 (시퀀스) 사용

엔티티의 식별자는 바뀌지 않고 고유하기에 두 엔티티의 객체의 식별자가 같으면 두 엔티티는 같다고 판단할 수 있다.

이에 식별자를 이용하여 equals와 hascode메서드를 구현할 수 있다.

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

Value

밸류는 개념적으로 완전한 하나를 표현할 때 사용한다. 예를 들어 아래와 같은 주소에 대한 데이터를 가진 클래스가 있다면

받는 사람을 위한 밸류 타입인 Receiver를 다음과 같이 작성한다.

public class ShippingInfo {
    // 받는 사람 정보
    private String receiverName;
    private String receiverPhoneNumber;
	
    // 받는 사람 주소
    private String shippingAddress1;
    private String shippingAddress2;
    private String 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 address22;
    private String shippingZipCode;
    
    // get 메서드
}

ShippingInfo를 다시 구현해보자

public class ShippingInfo {
    private Receiver receiver;
    private Address address;

    // 생성자, get 메서드
}

또한 밸류타입은 의미를 명확하게 표현하기 위해 밸류 타입을 사용하는 경우도 있다.

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

해당 코드에서 price와 amounts는 돈을 의미한다.

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

public class Money {
    private int value;

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

밸류의 또 다른 장점은 밸류 타입을 위한 기능을 추가할 수 있다는 것이다.

public class Money {
    private int value;

    public Money add(Money money) {
        // 새로운 객체 생성
        return new Money(this.value + money.value);
    }

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

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

데이터 변경 기능을 제공하지 않는 타입을 불변(immutable)이라고 표현한다. 밸류 타입을 불변으로 구현하는 이유는 밸류 타입을 수정했을 경우 우리가 의도치 않은 에러가 발생할 가능성이 생기기에 안전한 코드를 작성하기 위하여 변경 기능을 제공하지 않는다.

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

  • setOrderState() 메서드는 단순히 상태 값을 변경하는 것인지, 상태 값에 따른 다른 처리도 함께 구현하는 것인지 알기 어렵다.
  • 도메인 객체를 생성할 때 완전한 상태가 아닐 수도 있다.
  • 메인 객체가 불완전한 상태로 사용되는 것을 막으려면 생성 시점에 필요한 모든 데이터를 전달해 주어야 한다. 그렇게 되면 생성 시점에 데이터들의 유효성 검사도 함께 진행할 수 있게 된다.
  • 불변 밸류 타입을 사용하게 되면 자연스럽게 밸류 타입에 set 메서드를 구현하지 않는다.

DTO의 get / set 메서드

더보기

DTO는 프레젠테이션 계층과 도메인 계층이 서로 데이터를 주고받을 때 사용하는 구조체이다.

프레임워크에서 필드에 직접 값을 할당하는 기능을 제공하여 set 메서드를 최대한 넣지 않도록 하자.

이렇게 하면 DTO도 불변 객체가 되어 불변의 장점을 DTO까지 확장할 수 있다.