ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Aggregate와 Repository 3부
    JAVA/DDD 2020. 12. 30. 14:18

     

     

    유창하게(Fluently) 구현하기


    • AGGREGATE와 ENTRY POINT, REPOSITORY를 사용하여 대략적인 도메인 모델을 만들었다.
    • 이제 테스트 주도 방식을 적용하여 도메인 로직을 개발해보자.

     

     

    • 테스트 주도 개발 방식에서는 애플리케이션 코드를 작성하기 전에 실패하는 테스트부터 작성한다.
    • 테스트를 작성할 때는 테스트 대상 객체의 인터페이스가 어떻게 사용될 지를 상상해 보는 것이 중요하다. 
    • 테스트 중인 시나리오를 실행하기 위해 객체의 어떤 오퍼레이션을 어떤 순서로 호출하는 것이 효율적인지를 결정한다. 
    • 따라서 테스트를 작성함과 동시에 자연스럽게 사용하기 편리한 인터페이스를 설계하게 되는 부수적인 효과도 얻을 수 있다.

     

     

    • 객체의 인터페이스 설계와 관련해서 FLUENT INTERFACE라는 방식이 있다.
    • FLUENT INTERFACE를 설명하기 위해 전통적인 방식의 객체 인터페이스부터 살펴보자.

     

     

     

    • java에서 객체의 상태를 변경하는 setting 메서드를 작성하는 일반적인 관습은
    • 메서드의 반환형을 void로 설정하는 것이다. 
    • 즉, setting 메서드는 값을 반환하지 않는다. 
    • 이 관습은 void 타입을 지원하는 C++, Java, C#과 같은 정적 타입 언어에서 널리 사용되는 방식으로
    • 상태를 변경하는 메서드와 상태를 조회하는 메서드를 명시적으로 분리해야 한다는 COMMAND-QUERY SEPARATION 원칙을 따른다.

     

     

    • FLUENT INTERFACE는 COMMAND-QUERY SEPARATION 원칙은 위배하지만 읽기 쉽고 사용하기 편리한 객체 인터페이스를 설계할 수 있도록 한다.
    • FLUENT INTERFACE는 Method Chaining 스타일에 기반을 둔다.
    • 메소드 내에서 명시적으로 return문을 호출하지 않으면 자동으로 return this가 호출된다.
    • 따라서 특정 메소드를 호출한 후 반환된 객체를 사용하여 연속적으로 다른 메소드를 호출하는 것이 가능하다.

     

     

    • java에서 Method Chaining 스타일을 가장 빈번히 사용하는 경우는 내부 구조가 복잡한 복합 객체를 생성하는 경우이다
    • 대표적인 경우가 Hibernate 프레임워크의 Configuration 클래스로 SessionFactory를 생성하기 위해 Method Chaining 스타일을 사용한다.
    1
    2
    3
    4
    5
    SessionFactory sessionFactory = new Configuration()
                    .configure("/persistence/auction.cfg.xml")
                    .setProperty(Environment.DEFAULT_SCHEMA, "CAVEATEMPTOR")
                    .addResource("auction/CreditCard.hbm.xml")
                    .buildSessionFactory();
    cs

     

     

     

    • 스타일을 가장 광범위하게 수용한 코드는 TimeAndMoney 라이브러리로, 
    • 코드의 가독성을 향상하고 객체의 흐름을 효과적으로 전달하기 위해 Method Chaining 스타일을 사용한다. 
    • 다음은 TimeAndMoney 라이브러리의 테스트 코드에서 발췌한 Money의 인터페이스이다.
    1
    2
    3
    4
    assertEquals(new BigDecimal(2.50),
                    Money.dollars(5.00)
                            .dividedBy(Money.dollars(2.00))
                            .decimalValue(1, Rounding.UNNECESSARY));
    cs

     

     

     

    • Method Chaining 스타일을 도메인 객체 인터페이스의 설계에 적용한 것이 FLUENT INTERFACE 방식이다. 
    • 도메인 모델에서 FLUENT INTERFACE를 사용하기에 적절한 경우는 AGGREGATE 내부를 생성하는 단계가 간단하지 않지만 
    • BUILDER와 같은 별도의 FACTORY 객체를 도입할 경우 불필요한 복잡성(Needless Complexity)의 악취가 나는 경우이다. 
    • 주문 도메인에서는 주문 AGGREGATE를 생성하기 위해 FLUENT INTERFACE 스타일을 사용한다.

     

     

    • 주문 처리를 테스트하기 위한 테스트 클래스를 작성하자.
    • 각 테스트를 고립시키기 위해 setUp() 메서드를 오버 라이딩하여 테스트 픽스처(fixture)를 초기화한다.

     

     

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class OrderTest extends TestCase {
            private Customer customer;
            private OrderRepository orderRepository;
            private ProductRepository productRepository;
            public void setUp() throws Exception {
                Registrar.init();
                orderRepository = new OrderRepository();
                productRepository = new ProductRepository();
                productRepository.save(new Product("상품1"1000));
                productRepository.save(new Product("상품2"5000));
     
                customer = new Customer("CUST-01""홍길동""경기도 안양시"200000);
            }
    }
    cs

     

    • 테스트 코드를 작성하면서 도메인 객체에게 의미가 명확한 오퍼레이션을 할당하도록 노력하자.
    • 오퍼레이션의 명칭은 INTENTION-REVEALING NAME 패턴을 따르도록 한다. 
    • 오퍼레이션은 구현 전략이나 알고리즘과 독립적으로 오퍼레이션을 호출할 사용자의 사용 의도에 적합한 이름을 가져야 한다. 
    • 즉, 오퍼레이션의 이름은 메서드의 내부 구현 방식이나 컴퓨터의 관점이 아니라 이를 사용하는 클라이언트의 관점을 반영해야 한다.
    • INTENTION-REVEALING NAME 패턴을 따른 메소드의 경우 가독성이 높아진다.

     

     

    우선 두 가지 상품을 주문한 후 주문의 총액을 계산하는 테스트 코드를 작성하자.

     주문 AGGREGATE를 생성하기 위해 FLUENT INTERFACE 스타일을 사용한다.

    1
    2
    3
    4
    5
    6
    7
    8
     public void testOrderPrice() throws Exception {
            Order order = customer.newOrder("CUST-01-ORDER-01")
                    .with("상품1"10)
                    .with("상품2"20);
            orderRepository.save(order);
            assertEquals(new Money(110000), order.getPrice());
     
        }
    cs

     

     

    • 가격이 1,000원인 “상품 1”을 10개 주문하고가격이 5,000원인 “상품 2”를 20개 주문한 후 총액이 110,000원인지를 테스트한다.
    • Order 객체는 주문 AGGREGATE의 ENTRY POINT이므로 OrderRepository를 사용하여 등록한다.
    • REPOSITORY를 사용하기 때문에 주문이 시스템 내에 유일하게 하나만 존재하도록 제어할 수 있으며 상태 변경을 추적할 수 있다.

     

    • 실패하는 테스트를 가졌으니 이 테스트를 통과시키도록 하자.
    • Customer 클래스에 newOrder() 메서드를 추가하는 것으로 시작하자. 
    • long타입이었던 mileage를 VALUE OBJECT인 Money 타입으로 변경했음에 주목하자
    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 Customer extends EntryPoint {
            private String customerNumber;
            private String name;
            private String address;
            private Money mileage;
            private Money limitPrice;
     
            public Customer(String customerNumber, String name, String address,
                            long limitPrice) {
     
                super(customerNumber);
                this.customerNumber = customerNumber;
                this.name = name;
                this.address = address;
                this.limitPrice = new Money(limitPrice);
            }
     
            public Order newOrder(String orderId) {
                return Order.order(orderId, this);
            }
     
            public boolean isExceedLimitPrice(Money money) {
                return money.isGreaterThan(limitPrice);
            }
    }
    cs
    • Customer 클래스에 고객의 주문 한도를 검증하기 위해 필요한 limitPrice 속성을 추가했다.
    • limitPrice 속성은 Customer 객체 생성 시 생성자의 인자로 전달되어 초기화된다.
    • limitPrice 속성을 Customer 클래스에 추가했으므로 INFORMATION EXPERT 패턴에 따라 한도액을 검증하는 isExceedLimitPrice() 메소드를 Customer 클래스에 추가했다.
    • newOrder() 메서드는 ENTRY POINT 검색에 사용될 주문 ID를 인자로 받아 새로운 Order를 생성한다.
    • Order는 order() CREATION METHOD를 사용하여 Order를 생성한다.

     

     

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class Order extends EntryPoint {
            private Set<OrderLineItem> lineItems = new HashSet<OrderLineItem>();
            private Customer customer;
     
            public static Order order(String orderId, Customer customer) {
                return new Order(orderId, customer);
            }
     
            Order(String orderId, Customer customer) {
                super(orderId);
                this.customer = customer;
            }
    }
    cs

     

    • Order 클래스는 주문 AGGREGATE의 ENTRY POINT이므로 EntryPoint 클래스를 상속받고 검색 키로 orderId를 사용한다.
    • order() CREATION METHOD는 Order 클래스의 생성자를 호출해서 새로운 Order 인스턴스를 생성하고 Customer와의 연관 관계를 설정한다.
    • order() CREATION METHOD를 통해서만 객체를 생성할 수 있도록 제한하기 위해 생성자에게 public이 아닌 기본 가시성을 부여했다.

     

     

    • 주문 항목을 생성하는 with() 메소드를 추가하자. 주문 AGGREGATE의 생성 인터페이스에 METHOD CHAINING 스타일을 적용하기로 했으므로 with() 메소드는 this를 반환한다.
    • Order는 주문 AGGREGATE의 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
    Order.java
     
        public Order with(String productName, int quantity)
                throws OrderLimitExceededException {
            return with(new OrderLineItem(productName, quantity));
        }
     
        private Order with(OrderLineItem lineItem)
                throws OrderLimitExceededException {
            if (isExceedLimit(customer, lineItem)) {
                throw new OrderLimitExceededException();
            }
     
            lineItems.add(lineItem);
            return this;
     
        }
     
     
     
        private boolean isExceedLimit(Customer customer, OrderLineItem lineItem) {
            return customer.isExceedLimitPrice(getPrice().add(lineItem.getPrice()));
     
        }
    }
    cs
    • with() 메소드는 제품 명과 수량을 인자로 전달받아 OrderLineItem 인스턴스를 생성한다
    • 이때 주문 AGGREGATE의 불변식을 검증하기 위해 isExceedLimit() 메서드를 호출한다.
    • isExceedLimit() 메서드는 현재 주문 총액을 구한 후 Customer 클래스의 isExceedLimitPrice()를 호출하여 주문 가격이 고객의 한도액을 초과했는지 여부를 체크한다.
    • isExceedLimitPrice() 메소드는 한도액 초과 시 OrderLimitExceededException을 던진다.

     

    참조


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

    'JAVA > DDD' 카테고리의 다른 글

    AGGREGATE와 REPOSITORY 5부  (0) 2021.01.08
    AGGREGATE와 REPOSITORY 4부  (0) 2021.01.05
    AGGREGATE와 REPOSITORY 2부  (0) 2020.12.29
    AGGREGATE와 REPOSITORY 1부  (0) 2020.12.28
    DDD - Value Object와 Reference Object 4부  (0) 2020.12.24
Designed by Tistory.