본문 바로가기
DDD&MSA

[DDD] DDD - 엔티티(Entity)와 밸류(Value) (feat. Java)

by 덩라 2023. 5. 28.

DDD 를 공부한 내용을 기록하기 위한 포스팅 입니다.

출처 : 도메인 주도 개발 시작하기 - DDD 핵심 개념 정리부터 구현까지 (저자. 최범균)

1. DDD 란?

DDD 는 Domain Driven Design 의 약자로, 소프트웨어를 개발함에 있어 도메인을 중심으로 모델링하고 개발하는 개발 기법을 말합니다.

 

1-1. 도메인 (Domain)

도메인은 "소프트웨어를 통해 문제를 해결하고자 하는 업무영역" 을 의미합니다.

우리가 일상생활에서 사용하는 서비스들(ex. 네이버, 쿠팡, 카카오 등등) 도 하나의 거대한 도메인이라고 할 수 있습니다.

하지만, 도메인을 정의하기 용이하게 하기 위해 너무 거대한 영역의 경우 작은 여러 단위의 하위 도메인을 묶어 큰 도메인을 구성하게 됩니다. 아래에는 여러 하위 도메인이 쇼핑몰이라는 하나의 상위 도메인을 구성하는 예시입니다.

 

1-2. 도메인 모델 & 도메인 모델 패턴

도메인 모델 이란, "특정 도메인을 개념적으로 표현한 것" 이라고 정의합니다. 해당 용어에 대해서는 다양한 정의가 존재할 수 있다고 하는데, 공통된 목표는 "개발자, 도메인 전문가, 그 외 관련자들이 도메인을 이해하기 위한 수단" 이라는 것입니다.

여기서 중요한 것은 도메인들 간 용어의 혼선이 있을 수 있기 때문에 너무 큰 도메인을 모델링하기 보단, 작은 단위의 도메인을 따로 모델링 하는 것이 도메인을 이해하기 더 수월하다는 점입니다.
예를 들면, 상품 도메인에서의 상품은 소프트웨어 상에서 상품 데이터를 의미하지만, 배송 도메인에서의 상품은 실제 고객에게 배송되는 물리적인 물건을 의미하기 때문에 이런 경우 관련자에 따라 용어를 혼동할 수 있습니다. 이럴 경우 용어 간의 혼동을 최소화하기 위해 상품도메인과 배송도메인의 도메인 모델을 따로 표현하는 것이 좋습니다.

 

위 목적을 가지고 표현된 도메인 모델에는 여러 종류가 있을 수 있는데, 공통적으로 "도메인이 가지고 있는 규칙" 을 포함하고 있습니다.

예를 들면, 주문 도메인에 대해 아래와 같은 규칙이 있다고 가정해봅시다.

  1. 상품이 출고되기 전에는 주문을 취소할 수 있다.

해당 기능을 코드로 구현할 때, 전 직장에서는 아래처럼 코드를 구현해왔습니다.

public class OrderService{
    public void cancel(Long orderId) {
    	Order order = findById(orderId);
        
        if(order.getOrderStatus != READY) { // READY : 상품 준비중
        	throw new IllegalStateException("상품 준비 중인 주문만 취소할 수 있습니다.");
        }
        order.setOrderStatus(CANCEL);
    }
}

위 코드에는 여러 문제가 있지만, 이번 포스팅의 주제인 DDD 에 맞게 DDD 관점에서 이야기를 해보면 도메인이 가지는 규칙을 도메인 내에 구현하도록 하고 있습니다.

즉, 주문 취소에 대한 구현을 주문 도메인 내에서 하도록 하는 것이 DDD의 기본적인 개발 방법이라고 할 수 있습니다.

public class OrderService{
    public void cancel(Long orderId) {
    	Order order = findById(orderId);
        order.cancel();
    }
}
public class Order {

	// ... 중략

	public void cancel() {
    	if(!isReady()) {
        	throw new IllegalStateException("상품 준비 중인 주문만 취소할 수 있습니다.");
        }
        this.orderStatus = CANCEL;
    }
}

이 처럼 도메인 내에서 객체지향기법을 통해 소프트웨어를 개발하는 패턴도메인 모델 패턴 이라고 합니다.

 

2. 엔티티 vs 밸류

도메인을 표현하는 모델에는 크게 엔티티(Entity) 와 밸류(Value) 로 나눌 수 있습니다.

두 개념의 차이를 명확히 알고 있어야 도메인 개발에 적절히 사용할 수 있습니다.

 

1. 엔티티(Entity)

엔티티의 가장 큰 특징은 객체 내에 식별자를 가진다는 것입니다. 즉, 객체를 유일하게 식별하는 값이 객체 내에 존재한다는 것입니다.

식별자는 기본적으로 중복이 없어야 하기 때문에 아래와 같은 방법으로 생성할 수 있습니다.

  1. UUID 
  2. 특정 규칙에 의한 생성
  3. 직접 입력(중복체크 필수)
  4. DB 내 Sequence 혹은 auto increment 

물론 위 방법 외 에도 다른 방식이 있다면, 사용해도 무방하지만 가장 중요한 중복이 발생하면 안된다. 라는 성질은 있어야 합니다.

그리고 식별자는 한 번 부여되면 바뀌지 않아야 합니다. 즉, 두 객체가 같은 식별자를 가진다면, 두 객체는 논리적으로 같은 객체라고 간주하게 됩니다. 

 

2. 밸류(Value)

밸류는 개념적으로 온전한 하나의 의미를 표현하는데 사용됩니다. 아래 예시로 설명해보겠습니다.

주문 정보에 배송정보가 포함되어 있다고 가정해보겠습니다.

public class Order {
    // ... 중략
    private String receiverName;
    private String receiverPhoneNumber;
    private String address;
    private String detailAddress;
    private String zipCode;
}

위 정보는 크게 1) 배송지 정보 와 2) 수령인 정보 로 구분될 수 있습니다. 

배송지 정보를 의미하는 DeliveryInfo, 수령인 정보를 의미하는 Receiver 로 객체를 새로 생성하면 Order 객체는 아래 처럼 변경됩니다.

public class Order {
    private Receiver receiver;
    private DeliveryInfo deliveryInfo;
}

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

public class DeliveryInfo {
    private String address;
    private String detailAddress;
    private String zipCode;
}

이렇게 밸류 라는 개념을 도입해 객체를 분리하면 아래와 같은 이득을 얻을 수 있습니다.

  1. 코드에 의미를 부여할 수 있다. 
  2. 각 밸류에 필요한 검증 혹은 기능을 수행할 수 있다.

코드에 의미를 부여함으로써 가독성을 높이고, 다른 사람이 코드를 이해하는데 도움을 줄 수 있습니다.

필요한 검증이나 기능을 객체 단위에서 실행할 수 있다는 장점은 밸류를 적용하기 전 상황을 생각해보면 엄청난 의미를 가져옵니다.

만약, Receiver 객체 내에 phoneNumber 라는 값에 대해 전화번호 유효성 검사를 하기 위해 정규식 검증 로직이 추가된다면,

밸류로 Receiver 를 분리하기 전에는 아래와 같이 Order 객체에 해당 로직이 들어가게 됩니다.

public class Order {
    private String receiverPhoneNumber;
    // ... 중략
    
    public Order(...중략, String receiverPhoneNumber) {
        // ... 중략
        
        if(receiverPhoneNumber 의 정규식 검사 성공 여부){
            throw new IllegalArgumentException("전화번호 형식이 맞지 않습니다.");
        }
        this.receiverPhoneNumber = receiverPhoneNumber;
    }
}

위 코드의 경우, Order 라는 주문 을 의미하는 객체에서 수령인의 전화번호 형식을 검사하는 형태로 구성되어 있습니다.

사실 Order 객체는 주문 과 관련된 검증/기능만 있으면 된다고 생각할 수 있기 때문에 그 외 정보에 대한 검증/기능이 같은 객체 내에 위치하면 코드가 상당히 길어 질 수 있어 실질적으로 Order 의 중요 기능을 파악하는데 어려움이 있을 수 있다는 단점이 있습니다.

 

하지만, 이를 Receiver 라는 밸류로 분리한다면 아래와 같이 코드가 수정됩니다.

public class Order {
    private Receiver recevier;
    
    public Order(String receiverName, String phoneNumber) {
        this.receiver = new Receiver(receiverName, phoneNumber);
    }
}

public class Receiver {
    private String name;
    private String phoneNumber;
    
    public Receiver(String name, String phoneNumber) {
        if(phoneNumber 정규식 검사 성공 여부) {
            throw new IllegalArgumentException("전화번호 형식이 아닙니다.");
        }
        this.name = name;
        this.phoneNumber = phoneNumber;
    }
}

이렇게 코드가 수정되면 Order 는 Receiver 에게 수령인 정보에 대한 유효성검증/기능을 위임할 수 있고, Order 는 주문에 대한 기능에 집중할 수 있어 코드가 비교적 간략해지는 효과를 가져올 수 있습니다.

 

밸류는 이 처럼 코드의 가독성을 높이고 코드에 의미를 부여하기 위해 사용되는 방식입니다.

댓글