Eternity's Chit-Chat

aeternum.egloos.com



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

이 글은 제가 2008년 6월부터 10월까지 5개월간 마이크로소프트웨어에 연재했던 "도메인 주도 설계의 적용"이라는 원고의 원글입니다.  잡지에 맞추어 편집을 하는 과정에서 지면 상의 제약으로 인해 수정되거나 삭제된 부분이 있어 제 블로그에 원글을 올립니다. Domain-Driven Design에 관심 있는 분들에게 도움이 되었으면 합니다. 


PART 1. VALUE OBJECT와 REFERENCE OBJECT

 

프로그램을 가장 훌륭하게 작성하는 방법은 상태가 변경되는 오브젝트들과 수학적인 값을 나타내는 오브젝트들의 조합으로 표현하는 것이다.

- Kent Beck


어플리케이션을 구성하는 객체들을 REFERENCE OBJECTVALUE OBJECT로 분류할 수 있다. REFERENCE OBJECT는 고객, 주문과 같이 실 세계의 추적 가능한개념을 표현한다. 시스템 사용자는 오늘 상품을 주문한 “홍길동”라는 고객과 한달 전에 상품을 구입한 “홍길동”이라는 고객이 동일 인물인지를 알기 원한다. 따라서 고객 객체는 시스템 내에서 유일하게 식별 가능해야 한다. VALUE OBJECT는 날짜, 금액과 같은 것들이다. 단지 객체를 구성하는 속성들의 값에만 초점을 맞출 뿐 객체의 추적성에는 관심을 두지 않는다. 오늘 출금한10,000원짜리 지폐가 한달 전에 입금한10,000원짜리 지폐와 동일한 지폐일 필요는 없다. 단지10,000원이라는 금액이 정확하게 인출됐는지 여부가 중요할 뿐이다.

                                                                       

시스템 내에서 해당 객체를 계속 추적해야 하는가? 객체가 표현하는 개념이 유일하게 하나만 존재해야 하는가? 그렇다면 REFERENCE OBJECT로 만든다. 단지 객체가 추적할 필요가 없는 단순한 값인가? 속성값이 동일하면 동일한 객체로 간주해도 무방한가? 고민할 필요 없다. 그냥 VALUE OBJECT로 만든다.

 

REFERENCE OBJECTVALUE OBJECT의 개념은 단순하다. 그러나 추적성의 진정한 의미를 이해하기 위해서는 다양한 문맥 내에서 이들의 차이점을 살펴볼 필요가 있다. 우선 REFERENCE OBJECTVALUE OBJECT의 동일성에 관해 살펴 보기로 하자.

 

동일함(identical)의 의미

모든 객체 지향 시스템은 생성된 객체에게 고유한 식별자(identity)를 부여한다. 대부분의 객체 지향 언어는 객체가 위치하고 있는 메모리 상의 주소를 객체의 식별자로 할당하고 이 주소 값을 사용하여 객체를 구별한다. 각 언어는 객체의 식별자를 비교할 수 있는 연산자를 제공하는데 Java의 경우 “==”와 “!=” 연산자를 사용한다. 두 참조가 가리키는 객체가 동일한 식별자를 가지는 경우, 즉 동일한 주소에 위치하는 경우 “==” 연산자는 true를 반환한다.

 

다음은 실세계의 고객을 표현하는 Customer 클래스를 나타낸 것이다. 고객이 상품을 구매할 때마다 구매액의 1%가 마일리지로 적립된다. 적립된 마일리지는 다음 상품 구매 시 현금과 동일하게 사용할 수 있다.



Customer.java

package org.eternity.customer;

 

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;

}

}


 

고객 개개인은 시스템 내에서 유일해야 하며 시스템은 고객의 구매 기록이나 마일리지 적립 상태를 지속적으로 추적할 수 있어야 한다. 각 고객이 유일하기 때문에 고객이 동일한 지를 판단하기 위해 메모리 주소를 비교하는 “==” 연산자를 사용한다.

 

반면 10,000원이라는 금액은 시스템 내에 유일하게 존재할 필요가 없다. 내 계좌의 입금 내역에 찍힌 10,000원이라는 금액과, 카드 영수증에 출력된 10,000원은 동등한 금액이지만 이들이 반드시 동일한 객체일 필요는 없다. , 금액의 경우 객체의 동일성(identity) 보다는 속성 값의 동등성(equality)을 더 중요하게 생각한다.

 

따라서 “==” 연산자를 사용하여 동일성을 판단하기 보다는 equals() 메소드를 오버라이딩하여 금액의 동등성을 테스트해야 한다. equals() 메소드를 오버라이딩 할 경우에는 hashCode() 메소드도 함께 오버라이딩해주는 것이 좋다. 다음은 금액을 클래스로 작성한 것이다.



Money.java

package org.eternity.customer;

 

import java.math.BigDecimal;

 

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();

  }

}



고객은 REFERENCE OBJECT의 일반적인 예이며, 금액은 VALUE OBJECT의 일반적인 예이다. REFERENCE OBJECT는 유일하기 때문에 동일성 확인 시에 식별자를 사용하는 “==” 연산자를 사용할 수 있다. VALUE OBJECT의 경우 equals() 메소드를 사용하여 속성 값의 동등성을 비교해야 한다.


아마 이 시점에 이르면 자연스럽게 다음과 같은 질문이 떠오를 것이다. 금액과 같은 VALUE OBJECTREFERENCE OBJECT처럼 하나의 인스턴스만 유지하고 “==” 연산자를 사용하여 동일성을 비교할 수 없을까? 왜 객체를 비교할 때 “==” 연산자와 equals() 메소드를 구별하여 적용해야 하는가? 근본적으로 REFERENCE OBJECTVALUE OBJECT를 구별하는 이유가 무엇인가?   이에 대한 해답은 REFERENCE OBJECT 대신 VALUE OBJECT를 사용함으로써 악명 높은 별칭(aliasing) 문제를 피할 수 있기 때문이다.

 

별칭(aliasing) 문제

java에서는 하나의 객체를 서로 다른 변수가 참조할 수 있다. 이처럼 동일한 객체를 서로 다른 변수가 참조하는 것을 별칭(aliasing)이라고 한다. 별칭을 가진 객체의 상태를 변경할 경우 골치 아픈 버그가 발생할 수 있다. 만약 다른 참조를 통해 객체에 접근하는 쪽에서 객체가 변경되었다는 사실을 예상하지 못한다면 어떻게 될까?

 

별칭 문제를 확인하기 위해 우선 Customer 클래스에 대한 테스트 케이스를 작성하자.



CustomerTest.java

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());

}

 


이름이 홍길동인 고객 객체를 생성하고 customeranotherCustomer 두 참조 변수를 사용해서 별칭을 만들었다. 따라서 홍길동이라는 고객은 customer anotherCustomer라는 두 개의 참조를 통해 접근 가능하다. 별칭 생성 후 customer1,000원짜리 상품을 구매하여 마일리지리 1%를 적립한다.

 

customer anotherCustomer가 동일한 고객 객체를 참조한다는 사실을 알지 못하는 불행한 프로그래머는 anotherCustomer의 마일리지가 초기값 그대로 유지될 것이라고 예상할 것이다. 그러나 이런 예상은 모니터를 가로 지르는 악명 높은 빨간 막대를 보는 순간 산산이 부서지고 만다. 그렇다. 테스트가 실패하고 만 것이다.

 

이유는 별칭 때문이다. 세부적인 구현 내용을 알지 못한다면 customer anotherCustomer가 동일한 고객 객체를 참조한다는 사실을 알 수 없을 것이다. 따라서 anotherCustomer가 참조하고 있는 객체의 상태가 변경될 것이라는 사실을 예상하지 못할 경우 위와 같이 미묘하고도 발견하기 어려운 버그에 직면하게 된다. 물론 위 코드처럼 동일한 메소드 내에서 서로 다른 참조 변수를 사용하는 경우에는 원인을 발견하고 문제를 해결하는 것이 비교적 간단하다. 그러나 만약 customer anotherCustomer가 거리적으로 멀리 떨어진 프로그램의 서로 다른 위치에서 사용된다면 어떻게 될까? 아마도 충혈된 눈으로 끔찍한 버그를 찾기 위해 코드 이곳 저곳을 뒤지고 다니는 자신을 발견하게 될 것이다.

 

따라서 고객 객체를 다루는 가장 효과적인 방법은 별칭을 만들지 않는 것이다. 이야기가 여기서 끝난다면 프로그래머 생활도 그렇게 험난하지만은 않았을 것이다. 별칭을 만들지 않는 정책의 가장 큰 문제는 별칭이 만들어지는 것을 막을 수 없다는 점이다.

 

동일한 메소드, 동일한 클래스 내에서라면 의식적으로 별칭을 만들지 않을 수 있다. 그러나 해당 객체를 다른 메소드의 인자로 전달하는 순간 별칭 문제는 다시 끔찍한 고개를 쳐들고 코드 언저리를 스멀거리며 돌아다니기 시작한다. 메소드의 인자로 객체를 전달한다는 것은 자동으로 객체의 별칭을 만든다는 것을 의미한다. 이것이 값에 의한 전달(pass-by-value)인가, 참조에 의한 전달(call-by-reference)인가에 관한 논쟁은 중요하지 않다. 여기에서 중요한 것은 메소드를 호출하는 순간 전달된 인자에 대한 별칭이 자동으로 생성된다는 것이다.

 

다음은 Money 클래스를 사용할 경우의 자동 별칭 문제를 검증하는 테스트 케이스를 나타낸 것이다. 2,000원이라는 금액을 생성하고 이를 doSomethingWithMoney()메소드의 인자로 전달한다. 호출한 쪽에서는 메소드가 종료된 후 금액이 변경되지 않았을 것이라고 가정한다. 그러나 별칭을 통해 금액을 변경하는 것이 가능하기 때문에 이런 예상은 보기 좋게 빗나가고 만다. 다시 빨간 막대다.



MoneyTest.java

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));

}



메소드 인자에 final 사용한다고 해도 별칭 문제를 막을 수 없다. javafinalC++const와 달리 단지 메소드 내부에서 다른 객체를 참조하지 않도록 막아 주는 역할만을 할 뿐이다. 객체가 final로 전달되더라도 전달된 객체 자체의 상태를 바꾸는 것이 가능하다는 사실에 주의하자.

 

Bruce Eckel은 그의 저서 “Thiniking in Java 3rd Edition”에서 객체를 메소드 인자로 전달할 경우 다음 사항을 주의해야 한다고 충고한다.

  • 인자를 전달하는 동안 별칭(aliasing)이 자동으로 생성된다.
  • 지역 객체란 존재하지 않는다. 다만 지역 참조만이 존재할 뿐이다.
  • 참조는 범위(scope)를 가지지만 객체는 그렇지 않다.
  • 객체의 생명주기는 java에서 이슈가 아니다.
  • [java에는] 오브젝트의 수정과 별칭의 부정적인 영향을 막을(const와 같은) 언어적인 지원 메커니즘이 존재하지 않는다. 인자 목록에 final을 사용할 수는 있지만 이것은 단순히 참조가 다른 객체와 다시 묶이는 것을 막아줄 뿐이다.

 

좋은 객체 지향 습관을 따른다면 인자로 전달된 객체의 상태를 바꾸는 메소드는 작성하지 않을 것이다. 그러나 세상은 그리 녹녹치 않다. 신뢰할 수 있는 코드보다는 신뢰할 수 없는 코드가 더 많은 것이 각박한 현실이다. 아빠랑 오빠 빼고 다른 남자는 다 믿지 말라는 남자 친구의 말보다 더 믿기 어려운 것이 다른 코드라는 사실을 잊지 말자. 따라서 우리는 가능한 방어적으로 프로그래밍할 필요가 있다.

 

별칭 문제를 해결하기 위한 가장 좋은 방법은 객체를 변경할 수 없는 불변 상태로 만드는 것이다. 전달된 객체가 변경될 수 없다면 메소드에 객체를 전달한다고 하더라도 별칭을 통한 부작용을 막을 수 있다.


핑백

  • Kim : Java: Identical test 2010-07-10 14:46:25 #

    ... ref: http://aeternum.egloos.com/1105776 동일함(identical)의 의미모든 객체 지향 시스템은 생성된 객체에게 고유한 식별자(identity)를 부여한다. 대부분의 객체 지향 ... more

  • 객체가 같다는 것 | Like Olaf 2014-05-17 03:18:46 #

    ... 태가 변경되는 오브젝트들과 수학적인 값을 나타내는 오브젝트들의 조합으로 표현하는 것이다. - Kent Beck 여기에 있는 4개의 글 추천 http://aeternum.egloos.com/1105776 이 글 공유하기:트위터FacebookGoogle이것이 좋아요:좋아하기 가져오는 중...2012년 7월 20일likeolaf 동치, ... more

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

    ... 의 적용-1.VALUE OBJECT와 REFERENCE OBJECT 2부 [4] 2008/11/15 Domain-Driven Design의 적용-1.VALUE OBJECT와 REFERENCE OBJECT 1부 [14] 출처  : http://aeternum.egloos.com/catego ... more

덧글

  • 피스티스 2009/05/28 11:59 # 삭제

    좋은 글 잘 읽었습니다.
  • 이터너티 2009/06/10 14:08 #

    감사합니다. 더 좋은 글을 올릴 수 있도록 노력하겠습니다
  • 소내기 2009/06/11 14:28 #

    어휴 좋네요~ 잘읽었어요.
  • 윤군 2009/06/11 15:21 # 삭제

    오홋;; 감사합니다 ^^
  • nabina 2009/06/22 18:27 # 삭제

    생각 없이 ==와 equals을 사용했는데 이젠 제대로된 생각을 갖고 사용할 수 있겠네요+ㅁ+ 좋은 글 감사합니다.
  • 이터너티 2009/06/27 12:37 #

    소내기님, 윤군님, nabina님 모두 찾아 주셔서 감사합니다.
    제가 올린 글이 조금이라도 도움이 된다고 생각하니 뿌듯하네요. ^^
  • 야구쟁이 2010/06/29 12:10 # 삭제

    정말 좋네요 ^^ 잘보고갑니다~
  • 이터너티 2010/06/30 10:48 #

    감사합니다. ^^ 도움이 되었으면 좋겠네요.
  • bruce 2010/07/10 14:43 #

    잘봤습니다.
  • 이터너티 2010/07/11 20:54 #

    방문 감사드립니다. ^^
  • 웹눈 2010/07/30 17:59 # 삭제

    감사합니다. 많은 도움이 되었습니다.
  • imposno 2011/06/20 10:00 # 삭제

    저는 이제야 읽어보게 되었습니다. 좋은글 감사합니다.^^
  • 양완수 2011/07/01 10:11 # 삭제

    초보 개발자입니다.
    좋은 글 잘 보았습니다.

    어떤 클래스 A 를 생성하는 시점은 한 곳에서만 하고
    이외 클라이언트 측에서는 new 를 사용해 객체를 생성하지 못하게 하고
    객체를 사용 사용하기위해 꺼내오는 곳을 단일화한다면
    별칭문제가 사라지지 않을까요?
  • 이터너티 2011/07/02 14:02 #

    양완수님 안녕하세요. ^^

    별칭 문제는 객체 생성과 관련된 것이 아니라 이미 생성된 객체를 사용할 경우 발생하는 이슈입니다.

    본문의 MoneyTest.java에서 Money를 생성하는 new Money(2000) 부분을 캡슐화한다고 해도 생성된 Money를 메소드의 인자로 넘기거나 다른 참조에 할당할 경우에는 별칭 문제가 발생하게 됩니다.

    뒤에서 나오겠지만 별칭은 문제라기 보다는 Reference Object(Entity)를 Value Object와 구분 짓는 특성입니다.

    궁금한 사항이 있으면 또 질문 주세요.^^
  • 최성훈 2012/10/17 15:10 # 삭제

    좋은 정보 너무 감사드립니다.
    정독 하면서 궁금한 부분에 대해 여쭤보도록 할께요.
    감사합니다.
  • 이터너티 2012/10/17 18:16 #

    도움이 되셨으면 좋겠습니다.
    질문 주시면 제가 아는 한에서 답변 드리도록 하겠습니다. ^^
  • 박지현 2013/03/06 13:37 # 삭제

    이제야 이 글을 읽게 되다니 정말 안타까울 뿐입니다.
    왜 구글에서 검색할때 이런 글들이 제일 먼저 안올라 올까요?
    DDD로 검색했을때 제일 먼저 뜨면 좋겠습니다.
    토비-백기선님 불러그의 강력 추천을 통해서 이렇게 간접적으로 들어왔음을 고백하며
    다른 분들도 쉽게 DDD에 접근하길 바라면서 이렇게 족적을 남깁니다.
  • 이터너티 2013/03/06 18:37 #

    방문 감사드립니다. ^^
    제 블로그가 편하게 읽으시기에는 다소 부담스러운 내용들이 많아서 그런것 같기도 하네요.
    좀 더 쉽게 내용을 전달할 수 있으면 좋겠는데 아직까지는 많이 부족한 것 같습니다.
    그래도 유익하다고 평가해 주시는 분들이 조금이라도 계셔서 다행이네요. ^^
    토비님과 백기선님께 감사 드려야 겠네요.
  • 정도현 2014/06/05 14:00 # 삭제

    갓 1년지난 개발자입니다
    Orm에 대해서 공부하고잇던도중 어느분께서 이 블로그를 추천해주셔서
    보고잇는데 많은생각을 들게 합니다.

    많이배우고 가겠습니다.
    감사합니다.
  • 이터너티 2014/06/05 16:58 #

    요즘 블로그 포스팅이 뜸한데도 방문해 주셔서 감사드립니다.
    조만간 더 유익한 내용을 올리도록 할께요. ^^
  • 히피하피 2014/06/08 01:18 # 삭제

    좋은 글 잘 읽었습니다.
  • 이터너티 2014/06/10 18:46 #

    감사합니다. ^^
  • 빈즈 2016/02/12 11:33 # 삭제

    ORM 과 DDD 에 대해 검색하던 중 이 블로그에 방문하게 되었습니다.

    좋은 글 잘 읽도록 하겠습니다.

    감사합니다. ^^
  • 이터너티 2016/02/13 23:38 #

    감사합니다.
    요즘 DDD에 관해 좀 더 깊이 있게 고민하고 있는데 정리되면 공유 드리겠습니다. ^^
  • hojak99 2016/07/18 18:03 # 삭제

    글 잘 읽었습니다. 이 글을 읽고 지난 날들을 다시 되돌아보고 다시 생각할 수 있었습니다. 앞으로 조금 주의하고, 별칭 문제, == 그리고 equals 를 주의해서 사용해야겠어요
※ 로그인 사용자만 덧글을 남길 수 있습니다.