Eternity's Chit-Chat

aeternum.egloos.com



Domain-Driven Design의 적용-2.AGGREGATE와 REPOSITORY 4부 Domain-Driven Design

OrderLineItem은 상품 정보를 알고 있는 책임을 지닌 Product 클래스와 연관 관계를 가지며, 상품의 수량을 속성으로 포함한다. OrderLineItem의 생성자에 전달된 productName Product ENTRY POINT를 검색하기 위해 사용하는 검색 키이다. ProductREFERENCE OBJECT인 동시에 ENTRY POINT이므로 productName을 가지는 Product 인스턴스는 시스템 내에서 유일해야 한다. 따라서 Product를 관리하는 ProductRepository로부터 해당 인스턴스를 얻어 OrderLineItem product 속성에 할당한다. getPrice() 메소드는 현재 주문 항목의 가격을 반환하는 메소드로 상품 가격에 상품 수량을 곱한 금액을 반환한다.


OrderLineItem.java
package org.eternity.customer;

 

public class OrderLineItem {

private Product product;

private int quantity;

      

private ProductRepository productRepository = new ProductRepository();

      

public OrderLineItem(String productName, int quantity) {

       this.product = productRepository.find(productName);

       this.quantity = quantity;

}

      

public Money getPrice() {

     return product.getPrice().multiply(quantity);

}

 

public Product getProduct() {

       return product;

}

}


Product는 상품 명과 상품의 가격을 알 책임을 지닌 ENTRY POINT로 상품 명을 검색 키로 사용한다.


Product.java
package org.eternity.customer;

 

import org.eternity.common.EntryPoint;

 

public class Product extends EntryPoint {

   private Money price;

   private String name;

      

   public Product(String name, long price) {

       super(name);

       this.price = new Money(price);

   }

      

   public Product(String name, Money price) {

        super(name);

        this.price = price;

   }

 

   public Money getPrice() {

         return price;

   }

      

   public String getName() {

         return name;

   }

}


OrderLineItem.getPrice() 메소드를 구현했으므로 Order에 전체 주문 가격을 구할 수 있는 메소드를 추가할 수 있다. Order.getPrice() 메소드는 주문 항목들의 전체 가격을 더한 금액을 반환한다.


Order.java
public Money getPrice() {

Money result = new Money(0);

 

for(OrderLineItem item : lineItems) {

result = result.add(item.getPrice());

}

 

return result;

}


고객의 주문 한도액을 초과하지 않는 정상적인 주문 처리 시나리오를 테스트했으므로 이번에는 주문 총액이 고객의 주문 한도액을 초과하는 경우를 테스트해보자. 주문 총액이 고객의 주문 한도액을 초과하는 경우 with() 메소드는 OrderLimitExceededException 예외를 던져야 한다.


OrderTest.java
public void testOrderLimitExceed() {

try {

       customer.newOrder("CUST-01-ORDER-01")

.with("상품1", 20)

.with("상품2", 50);

       fail();

} catch(OrderLimitExceededException ex) {

assertTrue(true);

}

}


녹색 막대다. 테스트가 있어서 좋은 점은 자신도 모르게 느끼게 되는 막연한 두려움과 공포를 조절 가능한 수위로 낮춰 준다는 점이다. 테스트가 통과하면 그 다음 단계로 나아갈 수 있는 용기를 얻을 수 있다. 녹색 막대를 통해 용기를 얻었으니 한 걸음 더 전진해 보자.

 

다음은 주문 시에 동일한 상품을 두 번으로 나누어서 구매하는 경우를 테스트해 보자. 다음과 같이 고객이 상품1”을 두 번의 주문 항목으로 나누어 구매할 경우 주문 가격이 정확한지를 검증하는 테스트를 작성한다.


OrderTest.java
public void testOrderWithEqualProductsPrice() throws Exception{

Order order = customer.newOrder("CUST-01-ORDER-01")

.with("상품1", 5)

.with("상품2", 20)

.with("상품1", 5);

orderRepository.save(order);

assertEquals(new Money(110000), order.getPrice());

}


테스트를 통과한다. 동일한 상품을 여러 개의 주문 항목으로 나누어도 주문 총액을 정확하게 계산한다. 가만있자. 그러고 보니 동일한 상품에 대한 별도의 주문 항목은 어떻게 취급해야 할까? 상품이 동일하므로 하나의 주문 항목으로 보아야 할까, 아니면 별도의 독립적인 주문 항목으로 취급해야 할까? 고민할 필요 없다. 고객에게 물어보면 금방 답이 나올 테니까. 주문 업무를 담당하는 고객에게 물어 보니 동일한 상품을 나누어 요청하더라도 업무 상으로는 이들을 취합하여 동일한 주문 항목으로 처리한다고 한다. 새로운 도메인 규칙을 알게 되었으니 구현에 앞서 주문 항목의 개수를 검증하기 위한 테스트를 작성하자.



OrderTest.java
public void testOrdreLineItems() throws Exception {

Order order = customer.newOrder("CUST-01-ORDER-01")

.with("상품1", 5)

.with("상품2", 20)

.with("상품1", 5);

orderRepository.save(order);

            

assertEquals(2,order.getOrderLineItemSize());

}


Order 클래스에 주문 항목의 개수를 반환하는 getOrderLineItemSize() 메소드를 추가한다.


Order.java
public int getOrderLineItemSize() {

return lineItems.size();

}


빨간 막대다! 동일한 상품이더라도 개별적으로 추가되는 경우에는 별도의 주문 항목으로 취급하는 것 같다. 요구사항이란 놈은 정말 변덕이 심한 것 같다. 어떤 때는 요구사항이 변경됐다는 비보를 듣지 않으면 하루 종일 불안하고 초조하기까지 하니 말이다. 그러나 걱정할 것 없다. 우리에게는 비장의 무기인 테스트가 있다. 추가 요구사항을 반영하다가 기존 코드를 망가트리는 일 역시 비일비재하다. 테스트가 존재하면 코드의 어떤 부분이 망가져 버렸는지 즉각적으로 피드백 받을 수 있다. 기존 코드가 망가지는 것을 막을 수는 없더라도 망가졌을 때 비상 경고음이 울리도록 조치를 취해 두는 것이 여러모로 안전하다. 회귀 테스트를 믿어라. 그러면 복이 올지니.

 

그럼 어디부터 고쳐 볼까? 아무래도 Order with() 메소드에서 이미 등록된 상품을 주문하면 두 주문 항목을 합치도록 해야 할 것 같다.


Order.java
private Order with(OrderLineItem lineItem)

throws OrderLimitExceededException {

 

if (isExceedLimit(customer, lineItem)) {

       throw new OrderLimitExceededException();

}

 

for(OrderLineItem item : lineItems) {

if (item.isProductEqual(lineItem)) {

           item.merge(lineItem);

           return this;

       }

}

            

lineItems.add(lineItem);           

return this;

}


주문 항목을 추가할 때 OrderLineItem.isProducEqual() 메소드를 호출하여 현재까지 등록된 주문 항목 내에 동일한 상품에 대한 주문 정보가 있는 지 체크한다. 존재할 경우 하나의 주문 항목으로 병합하도록 OrderLineItem.merge() 메소드를 호출한다. 이제 OrderLineItemisProducEqual() 메소드와 merge() 메소드를 구현하자.


OrderLineItem.java
public boolean isProductEqual(OrderLineItem lineItem) {

return product == lineItem.product;

}

 

public OrderLineItem merge(OrderLineItem lineItem) {

quantity += lineItem.quantity;

return this;

}


테스트를 실행해 보자. 녹색 막대다. 새로운 요구사항에 대한 테스트뿐만 아니라 기존 코드에 대한 테스트도 모두 성공적으로 통과했다. 마음이 편안하다. 축배라도 한잔 해야 하지 않을까. 역시 테스트가 있어 너무 든든하다.


핑백

  • Domain-Driven Design | Jongmin Kim's Blog 2014-09-02 01:18:30 #

    ... main-Driven Design의 적용-2.AGGREGATE와 REPOSITORY 5부 2008/11/27 Domain-Driven Design의 적용-2.AGGREGATE와 REPOSITORY 4부 2008/11/25 Domain-Driven Design의 적용-2.AGGREGATE와 REPOSIT ... more