JAVA/DDD

DDD - Value Object와 Reference Object

100win10 2020. 12. 21. 14:13

 

DDD 이터니티 조용호 님의 블로그를 보고 다시 한번 공부하기 위해서 정리하게 되었습니다!


 

 

  • 애플리케이션을 구성하는 객체들을 REFERENCE OBJECT와 VALUE OBJECT로 분류할 수 있다.
  • 시스템 내에서 해당 객체를 계속 추적해야 한다던가 객체가 표현하는 개념이 유일하게 하나만 존재해야 하한 다면 REFERENCE OBJECT로 만든다. 

 

  • 단지 객체가 추적할 필요가 없는 단순한 값이라면 속성값이 동일하다면 동일한 객체로 간주해도 무방하다면 고민할 필요 없이 VALUE OBJECT로 만든다.
  • REFERENCE OBJECT와 VALUE OBJECT의 개념은 단순하다. 그러나 추적성의 진정한 의미를 이해하기 위해서는 다양한 문맥 내에서 이들의 차이점을 살펴볼 필요가 있다.

 

 

 동일함(identical)의 의미

 


  • 모든 객체 지향 시스템은 생성된 객체에게 고유한 식별자(identity)를 부여한다. 
  • 대부분의 객체 지향 언어는 객체가 위치하고 있는 메모리 상의 주소를 객체의 식별자로 할당하고 이 주소 값을 사용하여 객체를 구별한다

 

  • 다음은 실세계의 고객을 표현하는 Customer 클래스를 나타낸 것이다. 
  • 고객이 상품을 구매할 때마다 구매액의 1%가 마일리지로 적립된다. 
  • 적립된 마일리지는 다음 상품 구매 시 현금과 동일하게 사용할 수 있다.
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
public 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() 메서드도 함께 오버 라이딩해주는 것이 좋다
  • 다음은 금액을 클래스로 작성한 것이다.
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
public 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)이라고 한다.
  • 별칭을 가진 객체의 상태를 변경할 경우 골치 아픈 버그가 발생할 수 있다만약 다른 참조를 통해 객체에 접근하는 쪽에서 객체가 변경되었다는 사실을 예상하지 못한다면 어떻게 될까?

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public 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)인가에 관한 논쟁은 중요하지 않다
  • 여기에서 중요한 것은 메서드를 호출하는 순간 전달된 인자에 대한 별칭이 자동으로 생성된다는 것이다.

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public 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을 사용할 수는 있지만 이것은 단순히 참조가 다른 객체와 다시 묶이는 것을 막아줄 뿐이다.

 


 

별칭 문제를 해결하기 위한 가장 좋은 방법은 객체를 변경할 수 없는 불변 상태로 만드는 것이다. 

전달된 객체가 변경될 수 없다면 메서드에 객체를 전달한다고 하더라도 별칭을 통한 부작용을 막을 수 있다.

 

 

 

 

참조

 


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