ID로 다른 애그리게잇을 참조하라
필자는 지난 글 전반부에서 도메인 주도 설계Domain-Driven Design에서 말하는 구조물 중 하나인 조립물AGGREGATE[1]에 대해 이야기했다. 조립물을 구현하다 보면 다른 조립물을 참조해야 할 때가 있다. 이를 구현하는 방식은 크게 조립물의 루트 엔터티Root Entity 객체를 참조(포인터) 하는 것과 식별자로 어디서든 쓸 수 있는 값을 사용하는 방법(ID)이 있다.
도메인 주도 설계 구현Implementing Domain-Driven Design의 저자 반 버논Vernon, Vaughn은 "ID로 다른 애그리게잇을 참조하라"라고 말했다. 이 글은 지난 글에 이어 주문 도메인 예시를 통해 ID로 다른 조립물을 참조하는 방식에 대해 설명한다.
주문 예시
아래와 같이 Order 조립물과 Customer 조립물이 존재하고 Order 조립물이 Customer 조립물을 참조한다고 생각해보자.
Customer 조립물은 JPAJava Persistence API 코드로 아래와 같이 구현할 수 있다.(Order 코드는 이전 글을 참조하라)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
@Entity @Table(name = "customers") public class Customer { @Id @GeneratedValue private Long id; private String name; @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL) private List<BillingAddress> billingAddresses = new ArrayList<>(); // ... } @Entity @Table(name = "billing_addresses") public class BillingAddress { @Id @GeneratedValue private Long id; private String zipCode; @ManyToOne @JoinColumn(name = "customer_id", referencedColumnName = "id") private Customer customer; // ... }
객체 참조
객체 참조(포인터) 방식은 Order 조립물 루트 엔터티에 참조할 Customer 조립물 루트 엔터티를 추가함으로써 구현할 수 있다.
1 2 3 4 5 6 7 8 9
@Entity @Table(name = "orders") public class Order { // ... @ManyToOne @JoinColumn(name = "customer_id", referencedColumnName = "id") private Customer customer; // ... }
Order 조립물과 Customer 조립물은 직접 연결되기 때문에 Order 조립물을 생성, 수정할 때 Customer 조립물 객체 참조가 필요하다.
1 2 3 4 5 6 7 8 9
Customer customer = customerRepository.findById(customerId); Order order = new Order.Builder() .customer(customer) .shippingAddress(new ShippingAddress("12345", "Yoo Young-mo")) .addLineItem(new LineItem("P-0001", "상품 A", 1000l, 2)) .addLineItem(new LineItem("P-0002", "상품 B", 2000l, 1)) .addOrderPayment(new CreditCardPayment(2000l, "1234-123")) .addOrderPayment(new CreditCardPayment(2000l, "010-0000-0000")) .build();
데이터베이스에서 Order 조립물 객체 인스턴스를 가져올 때[2] Customer 조립물을 함께 가져오기 때문에 Order 조립물만 가져오는 것보다 더 많은 시간과 메모리를 사용한다.
또한, Order 조립물은 언제 참조하게 될지 모르는 Customer 조립 객체 참조를 항상 가지고 있어야 한다. 이 글에서는 이해를 위해 단순하게 Order 조립물이 하나의 객체 참조만 가지고 있지만 실제로는 여러 개의 조립물 객체 참조를 가지는 경우가 많다.
전역 고유 식별자 참조
반 버논은 그의 책에서 전역 고유 식별자를 이용하여 다른 애그리게잇을 참조하라고 말했다.
외부 애그리게잇보다는 참조를 사용하되, 객체 참조(포인터)를 직접 사용하지 말고 전역 고유 식별자를 이용하자. - 도메인 주도 설계 구현, 465 쪽
이전 코드를 아래와 같이 리팩토링 할 수 있다.
1 2 3 4 5 6
@Entity @Table(name = "orders") public class Order { // ... private Long customerId; }
1 2 3 4 5 6 7 8
Order order = new Order.Builder() .customerId(customerId) .shippingAddress(new ShippingAddress("12345", "Yoo Young-mo")) .addLineItem(new LineItem("P-0001", "상품 A", 1000l, 2)) .addLineItem(new LineItem("P-0002", "상품 B", 2000l, 1)) .addOrderPayment(new CreditCardPayment(2000l, "1234-123")) .addOrderPayment(new CreditCardPayment(2000l, "010-0000-0000")) .build();
객체 참조에 비해 조립물 간의 경계가 명확해지며, 성능 면에서 이점이 있다.
추론 객체 참조inferred object reference를 가진 애그리게잇은 참조를 즉시 가져올 필요가 없기 때문에 당연히 더 작아진다. 인스턴스를 가져올 때 더 짧은 시간과 메모리가 필요하기 때문에, 모델의 성능도 나아진다. - 도메인 주도 설계 구현, 466 쪽
모델 탐색
Order 조립물에서 주문(placeOrder)을 하려면 Customer 조립물 객체 참조가 필요하다고 가정해 보자. ID로 참조하고 있는 상황에서 어떻게 Customer 조립 객체 참조를 찾을까?
애그리게잇의 행동을 호출하기에 앞서 리파지토리나 도메인 서비스를 통해 의존 관계에 있는 객체를 조회하는 방법도 추천할 만하다. 클라이언트 에플리케이션 서비스는 이를 제어하며 애그리게잇으로 디스패치할 수 있게 된다. - 도메인 주도 설계 구현, 467 쪽
아래와 같은 코드로 구현할 수 있다. 애플리케이션 서비스에서 Order 조립물의 placeOrder 메서드(행동)를 호출하기 앞서 리파지토리REPOSITORY를 사용하여 Customer 조립 객체를 얻는다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
@Service public class OrderService { // ... @Transactional public void placeOrder(final Long orderId) { Order order = orderRepository.findById(orderId); // ID를 사용하여 Customer 획득 final Long customerId = order.getCustomerId(); Customer customer = customerRepository.findById(customerId); // 주문 order.placeOrder(customer); // ... } }
주석
[1] 조립물이라는 표현은 글을 검토해 주신 안영회 님이 제안한 애그리게잇을 대신하는 우리말 표현이다. 자세한 내용은 Aggregate를 애그리게잇 대신 조립물로 쓴 사연 참고하라.
[2] 도메인 주도 설계에서는 이를 재구성reconstiution이라고 부른다.