-
DDD - Value Object와 Reference ObjectJAVA/DDD 2020. 12. 21. 14:13
DDD 이터니티 조용호 님의 블로그를 보고 다시 한번 공부하기 위해서 정리하게 되었습니다!
- 애플리케이션을 구성하는 객체들을 REFERENCE OBJECT와 VALUE OBJECT로 분류할 수 있다.
- 시스템 내에서 해당 객체를 계속 추적해야 한다던가 객체가 표현하는 개념이 유일하게 하나만 존재해야 하한 다면 REFERENCE OBJECT로 만든다.
- 단지 객체가 추적할 필요가 없는 단순한 값이라면 속성값이 동일하다면 동일한 객체로 간주해도 무방하다면 고민할 필요 없이 VALUE OBJECT로 만든다.
- REFERENCE OBJECT와 VALUE OBJECT의 개념은 단순하다. 그러나 추적성의 진정한 의미를 이해하기 위해서는 다양한 문맥 내에서 이들의 차이점을 살펴볼 필요가 있다.
동일함(identical)의 의미
- 모든 객체 지향 시스템은 생성된 객체에게 고유한 식별자(identity)를 부여한다.
- 대부분의 객체 지향 언어는 객체가 위치하고 있는 메모리 상의 주소를 객체의 식별자로 할당하고 이 주소 값을 사용하여 객체를 구별한다
- 다음은 실세계의 고객을 표현하는 Customer 클래스를 나타낸 것이다.
- 고객이 상품을 구매할 때마다 구매액의 1%가 마일리지로 적립된다.
- 적립된 마일리지는 다음 상품 구매 시 현금과 동일하게 사용할 수 있다.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465public class Customer {private String customerNumber;private String name;private String address;private long mileage;public Customer(String customerNumber, String name, String address) {this.customerNumber = customerNumber;this.name = name;this.address = address;}public void purchase(long price) {mileage += price * 0.01;}public boolean isPossibleToPayWithMileage(long price) {return mileage > price;}public boolean payWithMileage(long price) {if (!isPossibleToPayWithMileage(price)) {return false;}mileage -= price;return true;}public long getMileage() {return mileage;}}cs - 고객 개개인은 시스템 내에서 유일해야 하며 시스템은 고객의 구매 기록이나 마일리지 적립 상태를 지속적으로 추적할 수 있어야 한다.
- 각 고객이 유일하기 때문에 고객이 동일한 지를 판단하기 위해 메모리 주소를 비교하는 “==” 연산자를 사용한다.
- 반면 10,000원이라는 금액은 시스템 내에 유일하게 존재할 필요가 없다.
- 내 계좌의 입금 내역에 찍힌 10,000원이라는 금액과, 카드 영수증에 출력된 10,000원은 동등한 금액이지만 이들이 반드시 동일한 객체일 필요는 없다.
- 즉, 금액의 경우 객체의 동일성(identity) 보다는 속성 값의 동등성(equality)을 더 중요하게 생각한다.
- 따라서 “==” 연산자를 사용하여 동일성을 판단하기 보다는 equals() 메서드를 오버라이딩하여 금액의 동등성을 테스트해야 한다.
- equals() 메소드를 오버 라이딩할 경우에는 hashCode() 메서드도 함께 오버 라이딩해주는 것이 좋다.
- 다음은 금액을 클래스로 작성한 것이다.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071public 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 - 고객은 REFERENCE OBJECT의 일반적인 예이며, 금액은 VALUE OBJECT의 일반적인 예이다.
- 각 REFERENCE OBJECT는 유일하기 때문에 동일성 확인 시에 식별자를 사용하는 “==” 연산자를 사용할 수 있다.
- VALUE OBJECT의 경우 equals() 메서드를 사용하여 속성 값의 동등성을 비교해야 한다.
금액과 같은 VALUE OBJECT도 REFERENCE OBJECT처럼 하나의 인스턴스만 유지하고
“==” 연산자를 사용하여 동일성을 비교할 수 없을까?
- 왜 객체를 비교할 때 “==” 연산자와 equals() 메소드를 구별하여 적용해야 하는가?
- 근본적으로 REFERENCE OBJECT와 VALUE OBJECT를 구별하는 이유가 무엇인가?
- 이에 대한 해답은 REFERENCE OBJECT 대신 VALUE OBJECT를 사용함으로써 악명 높은 별칭(aliasing) 문제를 피할 수 있기 때문이다.
별칭(aliasing) 문제
- java에서는 하나의 객체를 서로 다른 변수가 참조할 수 있다. 이처럼 동일한 객체를 서로 다른 변수가 참조하는 것을 별칭(aliasing)이라고 한다.
- 별칭을 가진 객체의 상태를 변경할 경우 골치 아픈 버그가 발생할 수 있다. 만약 다른 참조를 통해 객체에 접근하는 쪽에서 객체가 변경되었다는 사실을 예상하지 못한다면 어떻게 될까?
1234567891011121314151617181920public void testAliasing() {Customer customer = new Customer("CUST-01", "홍길동", "경기도 안양시");Customer anotherCustomer = customer;long price = 1000;customer.purchase(price);assertEquals(price*0.01, anotherCustomer.getMileage(), 0.1);assertEquals(0, anotherCustomer.getMileage());}cs - 이름이 “홍길동”인 고객 객체를 생성하고 customer와 anotherCustomer 두 참조 변수를 사용해서 별칭을 만들었다
- 따라서 “홍길동”이라는 고객은 customer와 anotherCustomer라는 두 개의 참조를 통해 접근 가능하다. 별칭 생성 후 customer가 1,000원짜리 상품을 구매하여 마일리 지리 1%를 적립한다
- customer와 anotherCustomer가 동일한 고객 객체를 참조한다는 사실을 알지 못한다면 anotherCustomer의 마일리지가 초기값 그대로 유지될 것이라고 예상할 것이다.
- 이유는 별칭 때문이다. 세부적인 구현 내용을 알지 못한다면 customer와 anotherCustomer가 동일한 고객 객체를 참조한다는 사실을 알 수 없을 것이다.
- 따라서 anotherCustomer가 참조하고 있는 객체의 상태가 변경될 것이라는 사실을 예상하지 못할 경우 위와 같이 미묘하고도 발견하기 어려운 버그에 직면하게 된다.
- 만약 customer와 anotherCustomer가 거리적으로 멀리 떨어진 프로그램의 서로 다른 위치에서 사용된다면 버그를 찾기 위한 시간은 상당히 오래 걸릴 것이다.
- 따라서 고객 객체를 다루는 가장 효과적인 방법은 별칭을 만들지 않는 것이다.
- 하지만 별칭을 만들지 않는 정책의 가장 큰 문제는 별칭이 만들어지는 것을 막을 수 없다는 점이다.
- 동일한 메서드, 동일한 클래스 내에서라면 의식적으로 별칭을 만들지 않을 수 있다.
- 그러나 해당 객체를 다른 메서드의 인자로 전달하는 순간 별칭 문제는 다시 시작된다.
- 메소드의 인자로 객체를 전달한다는 것은 자동으로 객체의 별칭을 만든다는 것을 의미한다.
- 이것이 값에 의한 전달(pass-by-value)인가, 참조에 의한 전달(call-by-reference)인가에 관한 논쟁은 중요하지 않다.
- 여기에서 중요한 것은 메서드를 호출하는 순간 전달된 인자에 대한 별칭이 자동으로 생성된다는 것이다.
12345678910111213141516171819public 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 - 다음은 Money 클래스를 사용할 경우의 자동 별칭 문제를 검증하는 테스트 케이스를 나타낸 것이다.
- 2,000원이라는 금액을 생성하고 이를 doSomethingWithMoney() 메서드의 인자로 전달한다.
- 호출한 쪽에서는 메서드가 종료된 후 금액이 변경되지 않았을 것이라고 가정한다.
- 그러나 별칭을 통해 금액을 변경하는 것이 가능하기 때문에 테스트는 실패하고 만다.
- 메소드 인자에 final을 사용한다고 해도 별칭 문제를 막을 수 없다.
- java의 final은 C++의 const와 달리 단지 메소드 내부에서 다른 객체를 참조하지 않도록 막아 주는 역할만을 할 뿐이다.
- 객체가 final로 전달되더라도 전달된 객체 자체의 상태를 바꾸는 것이 가능하다는 사실에 주의하자.
- java에는 오브젝트의 수정과 별칭의 부정적인 영향을 막을(const와 같은) 언어적인 지원 메커니즘이 존재하지 않는다.
- 인자 목록에 final을 사용할 수는 있지만 이것은 단순히 참조가 다른 객체와 다시 묶이는 것을 막아줄 뿐이다.
별칭 문제를 해결하기 위한 가장 좋은 방법은 객체를 변경할 수 없는 불변 상태로 만드는 것이다.
전달된 객체가 변경될 수 없다면 메서드에 객체를 전달한다고 하더라도 별칭을 통한 부작용을 막을 수 있다.
참조
'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 2부 (0) 2020.12.22