ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • DDD - Value Object와 Reference Object
    JAVA/DDD 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의 적용

    '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
Designed by Tistory.