-
DDD - Value Object와 Reference Object 2부JAVA/DDD 2020. 12. 22. 16:38
DDD 이터니티 조용호 님의 블로그를 보고 다시 한번 공부하기 위해서 정리하게 되었습니다!
불변성
불변 클래스는 다음과 같은 규칙을 따른다.
- 객체를 변경하는 메서드(mutator)를(mutator) 제공하지 않는다.
- 재정의할 수 있는 메서드를 제공하지 않는다.
- 모든 필드를 final로 만든다.
- 모든 필드를 private으로 만든다.
- 가변 객체를 참조하는 필드는 배타적으로 접근해야 한다.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172public class Money {private BigDecimal amount;public Money(BigDecimal amount) {this.amount = amount;}public Money(long amount) {this(new BigDecimal(amount));}public boolean equals(Object object) {if (this == object) {return true;}if (!(object instanceof Money)) {return false;}return amount.equals(((Money)object).amount);}public int hashCode() {return amount.hashCode();}public Money add(Money added) {this.amount = this.amount.add(added.amount);return this;}public String toString() {return amount.toString();}}cs - Money 클래스를 불변으로 만들기 위해 위 규칙을 적용해 보자.
- Money 클래스를 불변으로 만들기 위해 우선 Remove Setting Method 리팩토링을 적용하자.
- amount 필드를 private final로 변경한다.
1234567891011121314151617public class Money {private final BigDecimal amount;public Money(BigDecimal amount) {this.amount = amount;}...}cs 123456789101112131415161718public class Money {private final BigDecimal amount;..public Money add(Money added) {return new Money(this.amount.add(added.amount));}...}cs - 객체의 상태를 변경하는 public 메소드가 있는지 확인한다.
- Money 클래스에는 내부 상태를 변경할 수 있는 add() 메소드가 존재한다.
- add() 메서드는 일반적인 setting 메서드는 아니지만 현재 진행 중인 리팩터링 문맥 상에서는 setting 메소드로 간주할 수 있다.
- 한 가지 문제는 Money 클래스에서 add() 메서드를 제거할 경우 금액에 대한 증가 연산을 수행할 수 없다는 것이다.
- Money 클래스의 일부로 add() 메서드를 제공하면서도 객체의 불변성을 유지할 수 있는 방법은 새로운 객체를 생성해서 반환하는 것이다.
- 변경된 Money의 add() 메소드는 수신 객체의 amount와 인자로 전달된 객체의 amount의 합을 속성으로 가지는 새로운 Money 객체를 생성하여 반환한다
- amount의 타입인 BigDecimal 역시 불변 객체이며 BigDecimal.add(BigDecimal) 메서드는 내부 상태를 변경하지 않고 새로운 BigDecimal 객체를 생성하여 반환한다
- 따라서 Money 클래스는 금액을 증가시키는 add() 메서드를 제공하면서도 객체 자체의 불변성을 유지할 수 있게 되었다.
123456789101112131415161718@Testpublic void testMehodAlaising() {Money money = new Money(2000);doSomethingWithMoney(money);assertEquals(new Money(2000), money);}private void doSomethingWithMoney(final Money money) {money.add(new Money(2000));}cs - 이제 다음과 같은 테스트는 성공하게 된다.
- 별칭 문제는 객체 생성과 관련된 것이 아니라 이미 생성된 객체를 사용할 경우 발생하는 이슈이다.
- 본문의 MoneyTest.java에서 Money를 생성하는 new Money(2000) 부분을 캡슐화한다고 해도 생성된 Money를 메서드의 인자로 넘기거나 다른 참조에 할당할 경우에는 별칭 문제가 발생하게 된다.
- 별칭은 Reference Object(Entity)를 Value Object와 구분 짓는 특성이 된다.
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의 적용
'JAVA > DDD' 카테고리의 다른 글
AGGREGATE와 REPOSITORY 2부 (0) 2020.12.29 AGGREGATE와 REPOSITORY 1부 (0) 2020.12.28 DDD - Value Object와 Reference Object 4부 (0) 2020.12.24 DDD Value Object와 Reference Object 3부 (0) 2020.12.23 DDD - Value Object와 Reference Object (0) 2020.12.21