JAVA/DDD

AGGREGATE와 REPOSITORY 4부

100win10 2021. 1. 5. 16:39
  • OrderLineItem은 상품 정보를 알고 있는 책임을 지닌 Product 클래스와 연관 관계를 가지며상품의 수량을 속성으로 포함한다.
  • OrderLineItem의 생성자에 전달된 productName Product ENTRY POINT를 검색하기 위해 사용하는 검색 키이다.
  • Product은 REFERENCE OBJECT인 동시에 ENTRY POINT이므로 productName을 가지는 Product 인스턴스는 시스템 내에서 유일해야 한다

 

  • 따라서 Product를 관리하는 ProductRepository로부터 해당 인스턴스를 얻어 OrderLineItem product 속성에 할당한다.
  • getPrice() 메서드는 현재 주문 항목의 가격을 반환하는 메서드로 상품 가격에 상품 수량을 곱한 금액을 반환한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
OrderLineItem.java
 
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;
    }
}
cs

 

  • Product는 상품 명과 상품의 가격을 알 책임을 지닌 ENTRY POINT로 상품 명을 검색 키로 사용한다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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;
    }
}
cs

 

 

  • OrderLineItem.getPrice() 메소드를 구현했으므로 Order에 전체 주문 가격을 구할 수 있는 메서드를 추가할 수 있다.
  • Order.getPrice() 메소드는 주문 항목들의 전체 가격을 더한 금액을 반환한다.
1
2
3
4
5
6
7
8
9
10
Order.java
public
 Money getPrice() {
    
    Money result = new Money(0);
 
    for(OrderLineItem item : lineItems) {
        result = result.add(item.getPrice());
    }
 
    return result;
}
cs
  • 고객의 주문 한도액을 초과하지 않는 정상적인 주문 처리 시나리오를 테스트했으므로 이번에는 주문 총액이 고객의 주문 한도액을 초과하는 경우를 테스트해보자.
  • 주문 총액이 고객의 주문 한도액을 초과하는 경우 with() 메소드는 OrderLimitExceededException 예외를 던져야 한다.

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
OrderTest.java
public class OrderTest {
    public void testOrderLimitExceed() {
        try {
            customer.newOrder("CUST-01-ORDER-01")
                    .with("상품1"20)
                    .with("상품2"50);
            fail();
        } catch (OrderLimitExceededException ex) {
            assertTrue(true);
        }
    }
}
cs

 

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

 

 

1
2
3
4
5
6
7
8
9
10
11
OrderTest.java
public class OrderTest {
    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());
    }
}
cs
  • 테스트를 통과한다. 
  • 동일한 상품을 여러 개의 주문 항목으로 나누어도 주문 총액을 정확하게 계산한다.

 

동일한 상품에 대한 별도의 주문 항목은 어떻게 취급해야 할까? 

  • 상품이 동일하므로 하나의 주문 항목으로 보아야 할까, 아니면 별도의 독립적인 주문 항목으로 취급해야 할까? 
  • 고민할 필요 없다. 고객에게 물어보면 금방 답이 나오게 된다.
  •  주문 업무를 담당하는 고객에게 물어보니 동일한 상품을 나누어 요청하더라도 업무 상으로는 이들을 취합하여 동일한 주문 항목으로 처리한다고 한다. 
  • 새로운 도메인 규칙을 알게 되었으니 구현에 앞서 주문 항목의 개수를 검증하기 위한 테스트를 작성하자.
1
2
3
4
5
6
7
8
9
10
11
OrderTest.java
public class OrderTest {
    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());
    }
}
cs

 

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

 

1
2
3
4
5
6
Order.java
public class Order {
    public int getOrderLineItemSize() {
        return lineItems.size();
    }
}
cs
  • 테스트는 실패한다.
  • 동일한 상품이더라도 개별적으로 추가되는 경우에는 별도의 주문 항목으로 취급하는 것 같다. 
  • 요구사항의 변덕은 비일비재하나 테스트가 존재하면 코드의 어떤 부분이 망가져 버렸는지 즉각적으로 피드백받을 수 있다. 
  • 기존 코드가 망가지는 것을 막을 수는 없더라도 망가졌을 때 비상 경고음이 울리도록 조치를 취해 두는 것이 여러모로 안전하다. 회귀 테스트를 믿자.

 

 

Order with() 메소드에서 이미 등록된 상품을 주문하면 두 주문 항목을 합치도록 해야 할 것 같다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Order {
    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;
    }
}
cs
  • 주문 항목을 추가할 때 OrderLineItem.isProducEqual() 메소드를 호출하여 현재까지 등록된 주문 항목 내에 동일한 상품에 대한 주문 정보가 있는지 체크한다
  • . 존재할 경우 하나의 주문 항목으로 병합하도록 OrderLineItem.merge() 메소드를 호출한다. 
  • 이제 OrderLineItem isProducEqual() 메소드와 merge() 메서드를 구현하자.

 

1
2
3
4
5
6
7
8
9
10
public class OrderLineItem {
    public boolean isProductEqual(OrderLineItem lineItem) {
        return product == lineItem.product;
    }
    
    public OrderLineItem merge(OrderLineItem lineItem) {
        quantity += lineItem.quantity;
        return this;
    }
}
cs

 

  • 이제 테스트는 성공한다.

 

 

 

 

참조


이터니티 - Domain-Driven Design의 적용