Eternity's Chit-Chat

aeternum.egloos.com



Domain-Driven Design의 적용-1.VALUE OBJECT와 REFERENCE OBJECT 2부 Domain-Driven Design

불변성

불변 클래스는 다음과 같은 규칙을 따른다.


  • 객체를 변경하는 메소드(mutator)를 제공하지 않는다.
  • 재정의할 수 있는 메소드를 제공하지 않는다.
  • 모든 필드를 final로 만든다.
  • 모든 필드를 private으로 만든다.
  • 가변 객체를 참조하는 필드는 배타적으로 접근해야 한다.


Money 클래스를 불변으로 만들기 위해 위 규칙을 적용해 보자. Money 클래스를 불변으로 만들기 위해 우선 Remove Setting Method 리팩토링을 적용하자.

 

amount 필드를 private final로 변경한다.



Money.java

private final BigDecimal amount;



객체의 상태를 변경하는 public 메소드가 있는지 확인한다. Money 클래스에는 내부 상태를 변경할 수 있는 add() 메소드가 존재한다. add() 메소드는 일반적인 setting 메소드는 아니지만 현재 진행중인 리팩토링 문맥 상에서는 setting 메소드로 간주할 수 있다. 한 가지 문제는 Money 클래스에서 add() 메소드를 제거할 경우 금액에 대한 증가 연산을 수행할 수 없다는 것이다. 이것은 기다리던 봉급 날 아침이 밝았음에도 내 통장의 잔고가 변함 없이 그대로 유지된다는 것을 의미한다.

 

Money 클래스의 일부로 add() 메소드를 제공하면서도 객체의 불변성을 유지할 수 있는 방법은 새로운 객체를 생성해서 반환하는 것이다. , 기존 객체의 값 자체는 변경시키지 않고 연산 결과를 상태로 가지는 새로운 불변 객체를 생성하는 것이다.



Money.java

public Money add(Money added) {

  return new Money(this.amount.add(added.amount));

}



변경된 Money add() 메소드는 수신 객체의 amount와 인자로 전달된 객체의 amount의 합을  속성으로 가지는 새로운 Money 객체를 생성하여 반환한다. amount의 타입인 BigDecimal 역시 불변 객체이며 BigDecimal.add(BigDecimal) 메소드는 내부 상태를 변경하지 않고 새로운 BigDecimal 객체를 생성하여 반환한다. 따라서 Money 클래스는 금액을 증가시키는 add() 메소드를 제공하면서도 객체 자체의 불변성을 유지할 수 있게 되었다.

 

이제 테스트를 다시 실행해 보자. 드디어 녹색 막대다. 평온과 안전의 상징인 녹색 막대가 Money 클래스의 불변성을 축하해 주고 있다. 이제 용기를 내서 좀 더 앞으로 나아가 보자.

 

VALUE OBJECT와 불변성

객체를 불변으로 만들면 별칭 문제를 피할 수 있다. 객체의 상태를 바꿀 수는 없으므로 새로운 상태로 변경해야 할 경우 새로운 불변 객체를 만들어 기존의 불변 객체를 대체 시켜야 한다. 객체가 불변이라면 객체를 어디에 어떤 방식으로 노출시키더라도 예상하지 못한 부작용(side effect)으로 인해 놀랄 일은 없어질 것이다.

 

결론부터 이야기하자면 VALUE OBJECT는 불변 객체여야 한다. VALUE OBJECT는 속성을 바꿀 수 없으며 새로운 값이 필요할 경우 기존 객체의 상태를 변경하는 대신 새로운 VALUE OBJECT를 생성해서 이를 대체해야 한다. 10,000원이 들어 있는 지갑의 금액을 20,000원으로 변경하고 싶다면 기존의 지갑과 연결되어 있는 Money 객체의 속성 값을 10,000에서 20,000으로 변경시키는 것이 아니라 20,000원을 속성 값으로 가지는 Money 객체를 새로 생성한 후 기존의 Money 객체 대신 새로 생성된 Money 객체를 지갑 객체와 연결시킨다. 이제 10,000원을 속성으로 가지고 있던 기존의 Money 객체는 가비지 컬렉션의 대상이 될 것이다.

 

VALUE OBJECT를 불변 객체로 만드는 이유는 별칭 문제와 같이 골치 아픈 문제를 피할 수 있기 때문이다. VALUE OBJECT는 일반적으로 날짜, 금액과 같이 작은 개념을 의미하기 때문에 새로운 객체를 만들어 대체할 경우의 오버헤드가 적다. 추적성에도 관심을 가질 필요가 없기 때문에 굳이 동일한 객체를 계속 유지하고 있을 필요가 없다.

 

VALUE OBJECT는 전체 도메인의 복잡성을 낮추는 유용한 분석 개념이다. 풍부한 도메인 모델(rich domain model)의 작성을 위해서는 유용하지만 비즈니스 적인 관점에서 가치가 없는 작은 개념을 VALUE OBJECT로 모델링함으로써 추적성과 별칭 문제에 대한 부담 없이 해당 객체를 참조할 수 있도록 한다. 어떤 개념을 VALUE OBJECT로 취급하는 순간 해당 객체의 생명 주기가 얼마나 단순해질 지를 상상해 보라.

 

VALUE OBJECT가 반드시 불변이어야 하는 반면 REFERENCE OBJECT는 일반적으로 불변 객체가 아니다. 고객이나 주문과 같은 도메인 개념들은 시간에 따라 상태가 변경된다. 오늘 회원으로 가입한 고객의 상태가 1년 후에도 동일하게 유지될 것으로 예상하는 사람은 없을 것이다. 고객은 지속적으로 상품을 구매하고, 지불하고, 마일리지를 적립하며, 그에 따라 고객의 상태는 계속 변경된다.

 

시스템이 이런 이벤트에 따라 정확히 고객의 상태를 갱신하고 추적하기 위해서는 항상 동일한 고객 객체가 시스템의 각 부분으로 전달되어야 한다. 따라서 시스템의 모든 부분이 동일한 고객 객체를 공유해야 하며 이로 인한 REFERENCE OBJECT에 대한 별칭 문제를 피할 수는 없다. 정확하게 말하면 REFERENCE OBJECT에 있어 별칭은 문제가 아니라 요구사항이다. , 시스템은 REFERENCE OBJECT의 변경 사항을 추적해야 한다.

 

홍길동이라는 고객의 마일리지 포인트는 시스템의 어느 부분에서 참조하더라도 동일해야 한다. 어느 시점에 홍길동이라는 고객의 마일리지가 적립되었다면 시스템의 다른 부분에서도 변경된 마일리지 포인트를 조회할 수 있어야 한다. 이것이 REFERENCE OBJECT에 있어서의 추적성의 의미이다. 따라서 REFERENCE OBJECT에 대한 별칭은 필요악이다.

 

물론 REFERENCE OBJECT를 불변 객체로 만들 수 있다면 그렇게 하는 것이 최선의 방법이다. REFERENCE OBJECT를 불변 객체로 취급할 지의 여부는 요구사항에 달려 있다. 만약 대상이 최초 생성 시에 설정된 속성이 그대로 유지되는 추적 가능한 도메인 개념이라면 불변성을 가진 REFERENCE OBJECT로 취급하는 것이 복잡성을 낮추는 최상의 방법이다.

 

가능하다면 불변 객체로 시작하라. 만약 객체에 대한 변경 사항이 시스템의 다른 부분으로 전파될 필요가 있다면 이를 가변 객체로 변경하라. 그러나 REFERENCE OBJECT의 상태를 바꾸기 위해 VALUE OBJECT의 경우처럼 새로운 REFERENCE OBJECT를 생성해서 기존 객체를 대체해서는 안 된다. REFERENCE OBJECT는 시스템 내에 유일해야 한다. 동일한 REFERENCE OBJECT가 두 개 이상 만들어지면 시스템의 일관성이 깨진다. 독립적인 두 REFERENCE OBJECT에 의해 시스템의 다른 부분으로 전파될 변경 사항이 전파되지 않는 결과를 낳게 된다. REFERENCE OBJECT를 불변으로 만드는 유일한 방법은 REFERENCE OBJECT의 인터페이스에 상태를 변경하는 메소드를 포함시키지 않는 것이다.

 

정리하면 VALUE OBJECT는 객체의 상태를 변경하는 메소드를 포함할 수 있다. 그러나 실제로는 메시지를 수신하는 VALUE OBJECT의 상태를 변경하는 것이 아니라 변경된 상태 결과를 포함하는 새로운 VALUE OBJECT를 생성하여 반환하는 것이다. 일반적인 REFERENCE OBJECT는 상태 변경이 가능하다. 만약 REFERENCE OBJECT가 불변이고 별칭 문제에 관해 신경 쓰고 싶지 않다면 객체에 상태 변경 메소드를 포함시키지 말아야 한다. VALUE OBJECT는 새로운 값이 필요할 때마다 값을 가진 객체를 새로 생성한다. 반면 REFERENCE OBJECT는 오직 유일한 식별자를 가진 하나의 객체만이 존재해야 한다.

 

따라서 REFERENCE OBJECT를 다룰 때는 VALUE OBJECT와 달리 오직 하나의 객체만이 생성되고 동일한 객체를 시스템의 필요 부분으로 전달하기 위한 생명 주기 제어 메커니즘이 필요하다.


핑백

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

    ... sign의 적용-1.VALUE OBJECT와 REFERENCE OBJECT 3부 2008/11/16 Domain-Driven Design의 적용-1.VALUE OBJECT와 REFERENCE OBJECT 2부 [4] 2008/11/15 Domain-Driven Design의 적용-1.VALUE OBJ ... more

덧글

  • 2009/11/01 18:03 # 삭제 비공개

    비공개 덧글입니다.
  • 이터너티 2009/11/06 20:08 #

    안녕하세요.
    아래 Test Case에서 money를 2000과 비교한 것을 두고 질문 하신것 같네요.

    public void testMehodAlaising() {
    Money money = new Money(2000);
    doSomethingWithMoney(money);
    assertEquals(new Money(2000), money);
    }

    doSomethingWithMoney() 메소드가 다른 사람에 의해 작성된 코드이거나 다른 라이브러리에 속한 메소드라고 가정해 보겠습니다.
    이 경우 일반적으로 doSomethingWithMoney() 메소드의 내부 구현은 알 수가 없기 때문에 파라미터로 전달한 money의 값이 변경되지 않을 것이라고 예상하게 됩니다.
    위 Test Case의 assert 구문은 파라미터로 전달된 Money 객체 상태가 변경되지 않을 것이라는 개발자의 가정을 보여주는 것이고요.
    일반적으로 파라미터로 전달된 객체의 상태를 변경하는 것은 좋은 습관은 아니며 위와 같이 aliasing으로 인해 발생하는 side effect는 알 수 없는 버그를 낳는 온상이라고 할 수 있습니다.
    설명이 되었는지 모르겠네요. ^^;
  • ologist 2010/01/02 21:47 # 삭제

    결국 객제지향, DDD라는 것은 complexity and side effects management라는 생각입니다. 시간되면 회사에서 같이 얘기를 나누는 것도 좋겠네요.

    좋은 글 감사합니다. 저희 팀원에게 항상 강조하던 것인데 정리가 잘 되어 있어서 차용하도록 하겠습니다...^^
  • 이터너티 2010/01/06 19:02 #

    예 관련해서 함께 토론해 보는 것도 좋을 것 같습니다. ^^

    기본적인 사항이지만 의외로 관련된 내용에 대해 심사숙고하시는 분은 많지 않은 것 같습니다.
  • 핼리 2014/05/09 02:12 # 삭제

    다시 찾아보고 있어요 ㅎㅎ 좋네요
※ 로그인 사용자만 덧글을 남길 수 있습니다.