Eternity's Chit-Chat

aeternum.egloos.com



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

유창하게(Fluently) 구현하기

AGGREGATEENTRY POINT, REPOSITORY를 사용하여 대략적인 도메인 모델을 스케치했으므로 테스트 주도 방식을 적용하여 도메인 로직을 개발하자.

 

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

 

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

 

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

 

FLUENT INTERFACECOMMAND-QUERY SEPARATION 원칙은 위배하지만 읽기 쉽고 사용하기 편리한 객체 인터페이스를 설계할 수 있도록 한다. FLUENT INTERFACEMethod Chaining 스타일에 기반을 둔다. SmalltalkRuby처럼 void타입을 가지지 않는 동적 타입 언어에서는 명시적으로 값을 반환하지 않을 경우 자동적으로 메소드를 실행한 객체 자신이 반환된다. , 메소드 내에서 명시적으로 return문을 호출하지 않으면 자동으로 return this호출된다. 따라서 특정 메소드를 호출한 후 반환된 객체를 사용하여 연속적으로 다른 메소드를 호출하는 것이 가능하다.

 

java에서 Method Chaining 스타일을 가장 빈번히 사용하는 경우는 내부 구조가 복잡한 복합 객체를 생성하는 경우이다. 대표적인 경우가 Hibernate 프레임워크의 Configuration 클래스로 SessionFactory를 생성하기 위해 Method Chaining 스타일을 사용한다.


Method Chaining 스타일의 Hibernate SessionFactory 생성 인터페이스

SessionFactory sessionFactory = new Configuration()
  .configure("/persistence/auction.cfg.xml")

  .setProperty(Environment.DEFAULT_SCHEMA, "CAVEATEMPTOR")

  .addResource("auction/CreditCard.hbm.xml")

  .buildSessionFactory();


스타일을 가장 광범위하게 수용한 코드는 TimeAndMoney 라이브러리로, 코드의 가독성을 향상시키고 객체의 흐름을 효과적으로 전달하기 위해 Method Chaining 스타일을 사용한다. 다음은 TimeAndMoney 라이브러리의 테스트 코드에서 발췌한 Money의 인터페이스이다.


Method Chaining 스타일의 Money 인터페이스
assertEquals(new BigDecimal(2.50),
 
Money.dollars(5.00)
   .dividedBy(Money.dollars(2.00))
   .decimalValue(1, Rounding.UNNECESSARY));



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

 

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


OrderTest.java
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);
 
}



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

 

우선 두 가지 상품을 주문한 후 주문의 총액을 계산하는 테스트 코드를 작성하자. 주문 AGGREGATE를 생성하기 위해 FLUENT INTERFACE 스타일을 사용한다.


OrderTest.java
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());

}


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

 

위 코드는 아직 컴파일조차 되지 않는다. 실패하는 테스트를 가졌으니 이 테스트를 통과시키도록 하자. Customer 클래스에 newOrder() 메소드를 추가하는 것으로 시작하자. long타입이었던  mileageVALUE OBJECT Money 타입으로 변경했음에 주목하자.


Customer.java
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);
 
}


Customer 클래스에 고객의 주문 한도를 검증하기 위해 필요한 limitPrice 속성을 추가했다. limitPrice 속성은 Customer 객체 생성 시 생성자의 인자로 전달되어 초기화된다. limitPrice 속성을 Customer 클래스에 추가했으므로 INFORMATION EXPERT 패턴에 따라 한도액을 검증하는 isExceedLimitPrice() 메소드를 Customer 클래스에 추가했다.

newOrder() 메소드는 ENTRY POINT 검색에 사용될 주문 ID를 인자로 받아 새로운 Order를 생성한다. Orderorder() CREATION METHOD를 사용하여 Order를 생성한다.


Order.java
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;
 
}


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

 

주문 항목을 생성하는 with() 메소드를 추가하자. 주문 AGGREGATE의 생성 인터페이스에 METHOD CHAINING 스타일을 적용하기로 했으므로 with() 메소드는 this를 반환한다. Order는 주문 AGGREGATEENTRY POINT이므로 주문 항목이 추가될 때마다 주문 총액이 고객의 한도액을 초과했는지 여부를 검증하는 책임을 진다.



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()));

}


with() 메소드는 제품 명과 수량을 인자로 전달 받아 OrderLineItem 인스턴스를 생성한다. 이 때 주문 AGGREGATE의 불변식을 검증하기 위해 isExceedLimit() 메소드를 호출한다. isExceedLimit() 메소드는 현재 주문 총액을 구한 후 Customer 클래스의 isExceedLimitPrice()를 호출하여 주문 가격이 고객의 한도액을 초과했는지 여부를 체크한다. isExceedLimitPrice() 메소드는 한도액 초과 시 OrderLimitExceededException을 던진다.


핑백

  • Domain-Driven Design | Jongmin Kim&#039;s Blog 2014-09-02 01:18:32 #

    ... main-Driven Design의 적용-2.AGGREGATE와 REPOSITORY 4부 2008/11/25 Domain-Driven Design의 적용-2.AGGREGATE와 REPOSITORY 3부 [2] 2008/11/23 Domain-Driven Design의 적용-2.AGGREGATE와 REP ... more

덧글

  • Kevin 2009/06/30 02:51 # 삭제

    http://blog.lckymn.com/2009/06/30/method-chaining-to-use-or-not-to-use/
    일정이 빠듯해서, 어쩌다가 이제서야 마무리를 짓게 됐네요.
    이글은 전에 말씀드렸던 이터너티님 글을 인용하고 싶다고 했던 그글이 아니고
    그냥 잡담처럼 쓴글이라서 잠깐 언급한 정도입니다.
  • 이터너티 2009/07/05 22:25 #

    좋은 글 잘 읽었습니다(영어의 압박이... ^^;).
    Method Chaining을 적용하는데 좋은 참고가 될 것 같습니다.
    좋은 글 기대하고 있겠습니다. ^^
  • 맹한 북극의눈물 2013/12/04 17:47 #

    질문이 있습니다.

    Customer에서 newOrder() 구현하신 부분에서
    굳이 Order.order()로 생성하신 이유가 있나요??

    위에 설명에 보면 "order() CREATION METHOD를 통해서만 객체를 생성할 수 있도록 제한하기 위해 생성자에게 public이 아닌 기본 가시성을 부여했다." 라고 하셨는데 new Order()로 하면 안되는 이유가 무엇인지 잘 모르겠습니다.
  • 최빈 2013/12/20 15:57 # 삭제

    고객과 주문에 연관관계를 맺어주고
    주문(Order)은 고객(Customer)에 의해서만 생성돼야 하기 때문입니다.

  • cob 2016/05/19 19:55 # 삭제

    고객(Customer)만이 주문(Order)을 생성하기 위해서라면, order() 메소드를 Customer만이 사용할 수 있어야 합니다. - 그게 가능하냐 혹은 가능하지 않냐에 대해 말하려고 하는 것은 아닙니다. - order() 메소드가 public static이기 때문에 Customer가 아니라도 누구나 Order를 생성할 수 있습니다. 위 코드만 보면 기능적으로는 order()나 new Order()나 동일합니다. 때문에 기능적인 차이가 아니라 의미적은 차이를 생각해봐야 할 것 같습니다.
    의미적인 관점에서 보면 분명 메소드와 생성자는 엄연히 다르죠. 위 코드만 보더라도 Customer 입장에선 order()를 사용해 Order를 생성할 때 어떻게 Order가 생성 되는 지는 전혀 모릅니다. 얘가 바로 생성자로 처리해서 리턴을 하는 지, 파라미터를 바꾸는 지, 혹은 리턴하는 Order가 호출 시점에 생성 되는지, 혹은 미리 만들어 놓은 풀에서 가져오는 지 등을 전혀 알 수 없습니다. 즉 메소드를 사용한다라는 것은 사용자 측면에서 봤을 때, 새로운 Order 객체의 생성의 구체적인 과정을 몰라도 되는 영역으로 만듭니다. - 이 것이 옳으냐 그르냐는 논외로 할게요. - 물론 위에서는 구체적인 과정이 메소드를 사용하나 생성자를 사용하나 동일하지만(기능은 동일) 말이죠. 때문에 기능적인 관점이 아니라 의미적인 관점(메소드를 왜 사용하는가)에서 생각해봐야 할 것 같네요.
※ 로그인 사용자만 덧글을 남길 수 있습니다.