> ## Content Index
> Fetch the complete content index at: https://www.popit.kr/llms.txt
> Use this file to discover other available public pages before exploring further.

# DDD 값 객체와 마이크로서비스
- URL: https://www.popit.kr/ddd-value-objects-and-microservices/
- Published: 2019-03-18T01:25:54.000Z
- Updated: 2019-03-18T01:25:54.000Z
- Author: 유 영모
- Tags: #소프트웨어, Java, 마이크로서비스, DDD, #아키텍처 및 설계, JPA, Hibernate, 도메인 주도 설계, 도메인 주도 설계 구현, ValueObject, 값 객체, 빌딩블록

이 글은 [도메인 주도 설계Domain-Driven Design](http://www.yes24.com/Product/Goods/5312881?scode=032&OzSrank=3&ref=popit.kr)(이하 DDD)에서 말하는 값 객체Value Object가 무엇인지 알아보고 마이크로서비스 환경에서 값 객체를 활용하는 법을 다룬다.

# 값 객체란 무엇인가?

전자 상거래 사이트에서 상품을 받을 주소(배송지)를 입력하고 주문한다고 생각해 보자. 시스템에서 배송지를 임의로 수정한다면 상품은 정상적으로 배송되지 못할 것이다. 따라서 시스템은 배송지를 **바뀌지 않게 다뤄야** 한다.

배송지를 바뀌지 않게 클래스로 표현하면 아래와 같다. 바뀌지 않는다는 것은 생성 이후에는 변경되지 않음을 의미한다. 이를 구현하기 위해 생성자로만 객체를 생성할 수 있으며 속성을 변경하는 Setter가 없다.  

```java
public class ShippingAddress {
  private String zipCode;
  private String address;
  private String recipient;
 
  public ShippingAddress(String zipCode, String address, String recipient) {
    this.zipCode = zipCode;
    this.address = address;
    this.recipient = recipient;
  }
 
  public String getZipCode() {
    return zipCode;
  }
 
  public String getAddress() {
    return address;
  }
   
  public String getRecipient() {
    return recipient;
  }
}
```

고객이 주문 배송지를 변경하는 경우가 있다. 이때 시스템은 **개별 속성(우편번호, 주소, 수취인)을 변경하지 않고 새로운 배송지로 대체한다.**

```java
order.setShippingAddress(new ShippingAddress("12-334", "Seoul", "Yoo Young-mo"));
```

배송지를 바꿔야 하면 새로운 값으로 변경할 뿐, 굳이 주소 수정 이력을 모두 보관할 필요는 없다. DDD에서는 "*개념적 식별성을 갖지 않으면서 도메인의 서술적 측면을 나타내는 객체"*를 **값 객체**라고 정의한다.

> ENTITY의 식별성을 관리하는 일은 매우 중요하지만 그 밖의 객체에 식별성을 추가하면서 시스템의 성능이 저하되고, 분석 작업이 별도로 필요하며, 모든 객체를 동일한 것으로 보이게 해서 모델이 혼란스러워질 수 있다.  
>  
>  
>  
> 소프트웨어 설계는 복잡성과의 끊임없는 전투다. 그러므로 우리는 특별하게 다뤄야 할 부분과 그렇지 않은 부분을 구분해야 한다.  
>  
> 하지만 이러한 범주에 속하는 객체를 단순히 식별성이 없는 것으로만 생각한다면 우리의 도구상자나 어휘에 추가할 게 그리 많지 않을 것이다. 사실 이 같은 객체는 자체적인 특징을 비롯해 모델에 중요한 의미를 지닌다. 이것들이 사물을 서술하는 객체다.  
>  
> **개념적 식별성을 갖지 않으면서 도메인의 서술적 측면을 나타내는 객체를 VALUE OBJECT라 한다.**  
>  
> \- 도메인 주도 설계 100 쪽

[JPAJava Persistence API](https://en.wikipedia.org/wiki/Java%5FPersistence%5FAPI?ref=popit.kr) [값 타입Value Type](https://docs.jboss.org/hibernate/stable/orm/userguide/html%5Fsingle/Hibernate%5FUser%5FGuide.html?ref=popit.kr#categorization-value)을 사용하여 배송지를 값 객체로 구현할 수 있다. 결과적으로 아래 코드는 데이터베이스에 ShippingAddress 테이블이 아닌 Order 테이블의 컬럼으로 매핑된다.  

```java
@Entity
@Table(name = "orders")
public class Order {
  @Id
  @GeneratedValue
  private Long id;
 
  @Embedded
  private ShippingAddress shippingAddress;
  //...
}
 
@Embeddable
public class ShippingAddress {
  private String zipCode;
  private String recipient;
  private String address;
 
  private ShippingAddress() {
  }
 
  public ShippingAddress(String zipCode, String address, String recipient) {
    this.zipCode = zipCode;
    this.address = address;
    this.recipient = recipient;
  }
 
  public String getZipCode() {
    return zipCode;
  }
 
  public String getAddress() {
    return address;
  }
   
  public String getRecipient() {
    return recipient;
  }
}
```

ShippingAddress를 엔터티가 아닌 값 객체로 설계함으로써 엔터티간 연관 관계나 임의의 복잡한 상태에 놓일 수 있는 것을 고민하지 않아도 된다. 따라서 설계가 단순해졌을 뿐만 아니라 데이터베이스 JOIN이 발생하지 않으므로 성능 측면에서도 더 낫다.

결국 DDD에서 말하는 것처럼 특별히 다루지 않아야 하는 것을 값 객체로 만듦으로써 복잡성을 줄일 수 있다.

# 그리고 마이크로서비스

멀티 판매 채널에서 주문 가능한 마이크로서비스가 있다고 가정해보자.

판매 채널에서는 OrderService로 주문을 요청하고 OrderService는 판매 채널에서 요청한 주문을 저장(구매 주문)하고 비동기 주문 이벤트를 발행한다. 주문 이벤트를 구독하고 있던 각 서비스는 판매 채널 정책에 따라 자신(서비스)의 역할을 수행한다.

![DDD-ValueObject-And-Microservice (5)](https://www.popit.kr/content/images/wp/2019/03/DDD-ValueObject-And-Microservice-5.png)

주문 업무의 특징 중 하나는 주문 당시 데이터(상품, 쿠폰, 결제, 회원, 배송지 등 - 이하 스냅샷Snapshot)를 기준으로 주문 처리를 한다는 것이다. 이러한 스냅샷 데이터를 모두 엔터티로 설계한다면 도메인 모델은 매우 복잡해질 것이다.

스냅샷이라는 말 자체가 의미하는 것처럼 바뀌지 않는 값이다. 이것은 값 객체로 설계할 수 있음을 의미한다. 문제는 멀티 채널에서 주문하기 때문에 스냅샷 데이터 구조가 가변적이라는 것이다.

아래는 JPA로 구현한 코드이다. 주문의 가변적인 스냅샷 데이터를 다루기 위해 String으로 데이터 타입 사용하고 JSON 문자열로 저장한다.  

```java
@Entity
public class PurchaseOrder {
 
  @Id
  @GeneratedValue
  private Long id;
 
  private Long orderId;
 
  private Long channelId;
 
  @Enumerated(EnumType.STRING)
  private PurchaseOrderStatus status;
 
  @OneToMany(mappedBy = "purchaseOrder", cascade = CascadeType.ALL)
  private List<OrderLineItem> orderLineItems = new ArrayList<>();
 
  // 주문 결제 정보
  @Lob
  private String payment;
 
  // 주문 당시 회원 정보
  @Lob
  private String customer;
 
  // 쿠폰
  @Lob
  private String coupons;
 
  // 주문 배송지
  @Lob
  private String shippingAddress;
  // ...
}
 
@Entity
public class OrderLineItem {
 
  @Id
  @GeneratedValue
  private Long id;
 
  private String productId;
 
  private OrderLineItemStatus status;
 
  // 주문 당시 상품 정보
  @Lob
  private String product;
 
  // LineItem 쿠폰
  @Lob
  private String coupons;
 
  @ManyToOne
  @JoinColumn(name = "po_id", referencedColumnName = "id")
  private PurchaseOrder purchaseOrder;
 
  // ...
}
```

![image2019-3-14_10-25-37](https://www.popit.kr/content/images/wp/2019/03/image2019-3-14_10-25-37-1024x422.png)