logo
새로운 블로그로 이전하였습니다.

[번역] 값 객체(Value Object) by 마틴 파울러

  • 값객체
  • 참조객체
  • 마틴파울러
  • value_object
  • reference_object

들어가며

마틴 파울러리팩터링 2판 CH06을 읽다가 값 객체라는 용어를 처음 알게 되었습니다. '내가 알고 있는 객체 중에 값 객체라는 게 있었나? 값 객체는 무엇일까?' 하는 호기심에 책에 링크된 마틴 파울러의 블로그 글을 읽게 되었고, 읽는 김에 번역을 해보았습니다.

더 나은 방향으로 해석될 수 있는 부분이 있다면 댓글로 알려주시면 적극 반영하도록 하겠습니다. 감사합니다.


값 객체 Value Object

프로그래밍할 때, 사물을 복합 객체(compound)로 표현하는 것이 유용하다고 느낄 때가 많습니다. 예를 들어, 2D 좌표는 x 좌표과 y 좌표로 이뤄지고, 금액은 숫자와 통화로 이뤄지며, 날짜 범위는 시작 날짜와 종료 날짜로 이뤄지는데, 이들 각각은 년, 월, 일로 이루어질 수 있습니다.

이렇게 하다 보면 두 복합 객체(compound)가 동일한지 여부를 확인해야 할 때가 있습니다. (2, 3)의 데카르트 좌표를 나타내는 두 개의 점 객체가 있으면, 이들을 동일하다고 보는 게 합리적입니다.

속성 값(이 경우 x 및 y 값)이 동일하기 때문에 동일한 것으로 간주되는 객체를 값 객체(value object)라고 합니다.

그러나 프로그래밍할 때 주의하지 않으면 이런 동작이 발생하지 않을 수 있습니다.

JavaScript에서 점을 표현한다고 가정해봅시다.

const p1 = {x: 2, y: 3};
const p2 = {x: 2, y: 3};
assert(p1 !== p2);  // 내가 원하지 않는 결과

안타깝게도, 이 테스트는 통과합니다. JavaScript객체의 값을 무시하고 참조를 통해 객체의 동등성을 검사하기 때문입니다.

많은 상황에서 참조를 사용하는 것이 합리적입니다. 예를 들어, 많은 판매 주문을 로드하고 조작하는 경우 각 주문을 한 곳에 로드하는 것이 좋습니다. 그런 다음 앨리스의 최신 주문이 다음 배송에 있는지 확인하려면, 앨리스 주문의 메모리 참조나 ID를 이용해 해당 참조가 배송 주문 목록에 있는지 확인할 수 있습니다. 이 테스트에서는 주문의 내용물을 신경 쓸 필요가 없습니다. 마찬가지로, 고유 주문 번호로 앨리스의 주문 번호가 배송 목록에 있는지 확인할 수 있습니다.

따라서 객체를 어떻게 구분하냐에 따라 값 객체참조 객체라는 두 가지 클래스로 생각하는 게 유용하다고 생각합니다. 각각의 객체가 동등성을 처리하는 방법을 알고 있어야 하고, 객체가 그 기대에 맞게 작동하도록 프로그래밍해야 합니다. 어떻게 하느냐는 사용 중인 프로그래밍 언어에 따라 다릅니다.

어떤 언어는 모든 복합 객체(compound)를 값으로 처리합니다. 예를 들어 Clojure에서 단순 복합 객체(compound)를 만들면 다음과 같습니다.

> (= {:x 2, :y 3} {:x 2, :y 3})
true

이것이 모든 것을 불변 값으로 취급하는 함수형 스타일입니다.

그러나 함수형 언어가 아닌 경우에도 값 객체를 만들 수 있습니다. 예를 들어 Java에서는 기본 Point 클래스가 내가 원하는 방식으로 동작합니다.

assertEquals(new Point(2, 3), new Point(2, 3)); // Java

이 방식은 Point 클래스가 기본 equals 메서드를 재정의하여 값 비교를 하기 때문에 작동합니다.

JavaScript에서도 비슷한 작업을 할 수 있습니다.

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  
  equals (other) {
    return this.x === other.x && this.y === other.y;
  }
}
 
const p1 = new Point(2,3);
const p2 = new Point(2,3);
assert(p1.equals(p2));

여기서 문제는 내가 정의한 equals 메서드가 다른 JavaScript 라이브러리에는 알려지지 않는다는 점입니다.

const somePoints = [new Point(2,3)];
const p = new Point(2,3);
assert.isFalse(somePoints.includes(p)); // 내가 원하지 않는 결과
 
// 그래서 이렇게 해야 합니다
assert(somePoints.some(i => i.equals(p)));

Java에서는 Object.equals가 핵심 라이브러리에 정의되어 있고 다른 모든 라이브러리가 이를 비교에 사용하기 때문에 이러한 문제가 없습니다 (==는 일반적으로 원시 타입에만 사용됩니다).

값 객체의 장점 중 하나는 메모리의 동일한 객체에 대한 참조인지, 값이 동일한 다른 참조인지 신경 쓸 필요가 없다는 것입니다. 그러나 신경 쓰지 않으면 예상치 못한 문제가 발생할 수 있습니다. 이를 Java 코드로 설명해보겠습니다.

Date retirementDate = new Date(Date.parse("Tue 1 Nov 2016"));
 
// 은퇴 파티를 준비해야 합니다
Date partyDate = retirementDate;
 
// 그러나 그 날짜는 화요일이므로 주말에 파티를 하기로 합니다
partyDate.setDate(5);
 
assertEquals(new Date(Date.parse("Sat 5 Nov 2016")), retirementDate);
// 어, 이제 3일 더 일해야 하네요 :-(

이것은 별칭 버그(Aliasing Bug)의 예입니다. 한 곳에서 날짜를 변경했더니 예상치 못한 결과를 초래한 경우입니다. 별칭 버그를 피하기 위해 간단하지만 중요한 규칙을 따릅니다. 값 객체불변해야 합니다. 파티 날짜를 변경하려면 새로운 객체를 만들어야 합니다.

Date retirementDate = new Date(Date.parse("Tue 1 Nov 2016"));
Date partyDate = retirementDate;
 
// 날짜를 불변으로 취급합니다
partyDate = new Date(Date.parse("Sat 5 Nov 2016"));
 
// 그리고 나는 여전히 화요일에 은퇴합니다
assertEquals(new Date(Date.parse("Tue 1 Nov 2016")), retirementDate);

물론, 값 객체불변으로 다루는 것이 더 쉬운 경우가 많습니다. 객체를 다룰 때는 setter 메서드를 제공하지 않으면 됩니다. 앞서 언급한 JavaScript클래스는 다음과 같이 작성할 수 있습니다:

class Point {
  constructor(x, y) {
    this._data = {x: x, y: y};
  }
  get x() {return this._data.x;}
  get y() {return this._data.y;}
  equals (other) {
    return this.x === other.x && this.y === other.y;
  }
}

불변성을 선호하는 이유는 별칭 버그를 피하기 위해서입니다. 그러나 복사본을 항상 만들어 할당하는 방식으로도 피할 수 있습니다. C#의 구조체 같은 언어는 이러한 기능을 제공합니다.

어떤 개념을 참조 객체로 취급할지 값 객체로 취급할지는 상황에 따라 다릅니다. 많은 경우 우편 주소를 간단한 텍스트 구조로 처리하고 값 동등성을 사용하는 것이 좋습니다. 그러나 더 정교한 매핑 시스템에서는 우편 주소를 복잡한 계층 모델에 연결하고 참조를 사용하는 것이 더 합리적일 수 있습니다. 대부분의 모델링 문제와 마찬가지로, 다른 맥락에서는 다른 해결책이 필요합니다.

일반적인 원시 타입(예: 문자열)을 적절한 값 객체로 대체하는 것이 좋습니다. 전화번호를 문자열로 표현할 수 있지만, 전화번호 객체로 변환하면 변수매개변수를 더 명확하게 만들 수 있고 (언어에서 지원하는 경우 타입 검사도 가능), 유효성 검사의 자연스러운 초점이 되며, (정수 ID 번호에 산술 연산을 하는 것과 같은) 부적절한 동작을 피할 수 있습니다.

작은 객체, 예를 들어 점, 금액, 범위는 값 객체의 좋은 예입니다. 그러나 개념적 정체성이 없거나 프로그램 전반에 참조를 공유할 필요가 없는 경우 더 큰 구조체도 값 객체로 프로그래밍할 수 있습니다. 이는 불변성을 기본값으로 하는 함수형 언어와 더 자연스럽게 맞아떨어집니다.

값 객체, 특히 작은 값 객체는 종종 간과되기 쉽지만, 일단 좋은 값 객체를 발견하면 그 위에 풍부한 동작을 생성할 수 있습니다. 예를 들어 Range 클래스를 사용해 보면 시작 및 종료 속성을 조작하는 중복을 방지할 수 있는 더 풍부한 동작을 제공할 수 있습니다. 도메인 특정 값 객체가 리팩토링의 초점이 되어 시스템을 극적으로 단순화하는 코드 베이스를 자주 접합니다. 이러한 단순화는 여러 번 경험해보기 전까지는 놀라울 수 있지만, 일단 경험하면 좋은 친구가 될 수 있습니다.

Original Source

martinFowler.com