본문 바로가기

도메인 주도 개발 시작하기

[도메인 주도 개발 시작하기] 엔티티와 밸류

1) 엔티티와 밸류

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

  엔티티와 밸류를 제대로 구분해야 도메인을 올바르게 설계하고 구현할 수 있기 때문에

  이 둘의 차이를 명확하게 이해하는 것은 도메인을 구현하는데 있어 중요하다. 

 

(1) 엔티티

 

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

  식별자는 엔티티 객체마다 고유해서 각 엔티티는 서로 다른 식별자를 갖는다.

  예를 들어, 주문 도메인에서 각 주문은 주문번호를 가지고 있는데,

  이 주문번호는 각 주문마다 서로 다르다. 

  따라서 주문번호가 주문의 식별자가 된다. 

 

- 주문 도메인 모델에서 주문에 해당하는 클래스가 Order이므로

  Order가 엔티티가 되며 주문번호를 속성으로 갖게 된다. 

Order

-orderNumber: String
-orderLines: List<OrderLine>
-totalAmounts: Money
-shippingInfo: ShippingInfo
-state: OrderState

 

- 주문에서 배송지 주소가 바뀌거나 상태가 바뀌더라도 주문번호가 바뀌지 않는 것처럼

  엔티티의 식별자는 바뀌지 않는다.

  엔티티를 생성하고 속성을 바꾸고 삭제할 때까지 식별자는 유지된다. 

 

- 엔티티의 식별자는 바뀌지 않고 고유하기 때문에 

  두 엔티티 객체의 식별자가 같으면 두 엔티티는 같다고 판단할 수 있다.

  엔티티를 구현한 클래스는 다음과 같이 식별자를 이용해서 equals() 메서드와 hashCode() 메서드를 구현할 수 있다. 

 

public class Order {
   private String orderNumber;
   
   @Override
   public boolean equals(Object obj){
      if(this == obj) return true;
      if(obj == null) return false;
      if(obj.getClasee() != 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; 
   }
}

 

(2) 엔티티의 식별자 생성

엔티티의 식별자를 생성하는 시점은 도메인의 특징과 사용하는 기술에 따라 달라진다.

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

 

(1) 특정 규칙에 따라 생성

(2) UUID나 Nano ID와 같은 고유 식별자 생성기 사용

(3) 값을 직접 입력

(4) 일련번호 사용(시퀀스나 DB의 자동 증가 칼럼 사용)

 

- 주문번호, 운송장번호, 카드번호와 같은 식별자는 특정 규칙에 따라 생성한다.

  이 규칙은 도메인에 따라 다르고, 같은 주문번호라도 회사마다 다르다.

  예를 들어, 최근에 두 온라인 서점에서 구매한 책의 주문번호는

  각각 '2021112831728OOOO'와 '001-A88277OOOO'인데

  두 번호의 구조가 완전히 다른 것을 알 수 있다. 

 

- 흔히 사용하는 규칙은 현재 시간과 다른 값을 함께 조합하는 것이다. 

  한 온라인 쇼핑 사이트에서 구매한 책의 주문번호는 '2021112831728OOOO'인데,

  이 주문번호의 앞 번호인 '2021128'는 2021년 11월 28일을 의미한다. 

 

 - 날짜와 시간을 이용해서 식별자를 생성할 때, 주의할 점은 

   같은 시간에 동시에 식별자를 생성해도 같은 식별자가 만들어지면 안된다는 것이다. 

 

- UUID를 사용해서 식별자를 생성할 수 있다.

  다수의 개발 언어가 UUID 생성기를 제공하고 있으므로,

  마땅한 규칙이 없다면 UUID를 식별자로 사용해도 된다.

  자바는 java.util.UUID 클래스를 사용해서 UUID를 생성할 수 있다. 

UUID uuid = UUID.randomUUID();

String struuid = uuid.toString();

 

- 회원의 아이디나 이메일 같은 식별자는 값을 직접 입력한다.

  사용자가 직접 입력하는 값이기 때문에 식별자를 중복해서 입력하지 않도록 사전에 방지하는 것이 중요하다.

  

 

(3) 밸류 타입

- ShippingInfo 클래스는 다음과 같이 받는 사람과 주소에 대한 데이터를 갖고 있다.

public class ShippingInfo { 
   
    private String receiverName;  
    private String receiverPhoneNumber; 
    
    private String shippingAddress1;
    private String shippingAddress2;
    private String shippingZipcode;
    
    ... 생성자, getter
}

 

- ShippingInfo 클래스의 receiverName 필드와 receiverPhoneNumber 필드는 

  서로 다른 두 데이터를 담고 있지만, 두 필드는 개념적으로 받는 사람을 의미한다.

  즉, 두 필드는 실제로 하나의 개념을 표현하고 있다.

  비슷하게 shippingAddress1 필드, shippingAddress2 필드, shippingZipcode 필드는

  주소라는 하나의 개념을 표현한다.

 

- 밸류 타입은 개념적으로 완전한 하나를 표현할 때 사용한다.

  예를 들어 받는 사람을 위한 밸류 타입인 Receiver를 다음과 같이 작성할 수 있다. 

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

 

- Receiver는 '받는 사람'이라는 도메인 개념을 표현한다.

  앞서 ShippingInfo의 receiverName 필드와 receiverPhoneNumber 필드가

  이름을 갖고 받는 사람과 관련된 데이터라는 것을 유추한다면 

  Receiver는 그 자체로 받는 사람을 뜻한다.

  밸류 타입을 사용함으로써 개념적으로 완전한 하나를 잘 표현할 수 있는 것이다. 

 

- ShippingInfo의 주소 관련 데이터도 다음의 Address 밸류 타입을 사용해서 보다 명확하게 표현할 수 있다. 

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

 

- 밸류 타입을 이용해서 ShippingInfo 클래스를 다시 구현해보자.

  배송정보가 받는 사람과 주소로 구성된다는 것을 쉽게 알 수 있다. 

public class ShippingInfo {
   private Receiver receiver;
   private Address address;
   
   ... 생성자, get 메서드
}