DDD(Domain Driven Design) - Value Object (값 객체)
이번에 다룰 DDD 의 내용은 바로 값 객체이다. 반 버논에 도메인 주도 설계 구현에서는 되도록이면 엔터티가 아니라 값 객체를 사용해 모델링하도록 노력해야 한다고 말한다.
값 객체의 이점으로 책에서는 아래와 같이 언급한다.
"측정하고 수량화하거나 설명해주는 값 타입은 생성, 테스트, 사용, 최적화, 유지 관리가 더 쉽다."
그렇다면 도메인 개념을 값으로 모델링해야 할지 알 수 있는 방법은 무엇인가?
아래와 같은 특징을 포함하는지 고려해야 한다.
기존 name 변수에서 메서드를 통해서 스스로 변경하지 않고, 완전하게 새로운 인스턴스를 참조하도록 name 객체를 할당하여, 전체 값을 대체했다.
값 객체의 이점으로 책에서는 아래와 같이 언급한다.
"측정하고 수량화하거나 설명해주는 값 타입은 생성, 테스트, 사용, 최적화, 유지 관리가 더 쉽다."
그렇다면 도메인 개념을 값으로 모델링해야 할지 알 수 있는 방법은 무엇인가?
아래와 같은 특징을 포함하는지 고려해야 한다.
- 도메인 내의 어떤 대상을 측정하고, 수량화하고, 설명한다.
- 불변성이 유지될 수 있다.
- 관련 특성을 모은 필수 단위로 개념적 전체를 모델링한다.
- 측정이나 설명이 변경될 땐 완벽히 대체 가능하다.
- 다른 값과 등가성(value equality) 을 사용해 비교할 수 있다.
- 협력자(collaborator) 에게 부작요이 없는 행동 (Side-Effect-Free Behavior) 을 제공한다.
측정, 수량화, 설명
-> 도메인 내에 있는 어떤 대상을 측정하고 수량화하고 설명하는 개념.
ex) 사람에겐 나이가 있다. 나이는 실재하는 어떤 대상은 아니지만, 사람이 살아온 햇수를 측정하거나 수량화 한다.
불변성
-> 값 객체는 한 번 생성되면 변경할 수 없다. 보통 java 에서 객체를 생성할 때 특정 생성자 혹은 팩토리 패턴을 이용해서 생성후 객체의 변경을 할 수 있는 메서드를 제공하지 않는 방법으로 불변성을 유지할 수 있다.
만약 지금 설계하고 있는 객체가 자신의 행동으로 인해 변경돼야 한다고 생각한다면, 이는 값 객체가 아니라 엔터티로 설계해야 함을 의미한다.
개념적 전체 (Conceptual Whole)
-> 값 객체는 하나 이상의 개별적 특성을 가질 수 있으며, 각 특성은 서로 연관되어 있다. 여러 특성이 설명하는 바를 모아 전체를 나타낸다.
ex) {50,000,000달러} 는 50,000,000 이라는 특성과 달러라는 특성, 두 가지를 모두 갖고 있다. 이런 특성을 따로 분리해보면 다른 의미 혹은 별 의미가 없다. 두 특성을 모아서 개념적 전체로 봐야 금전적 수량을 나타내게 된다.
예제에서 확인할 수 있듯이, 값 객체의 불변성과 함께 전체 값이 한 번의 오퍼레이션으로 생성됨을 보장해줄 값 클래스의 생성자가 필요하다. 마치 전체 값을 조각조각 붙여서 만들어가듯, 생성된 후에 값 인스턴스의 특성을 채워가려 해선 안 된다. 최종 상태가 한 번에 원자적으로 초기화되도록 보장해야 한다.
대체성(replaceability)
-> 도메인이 정수인 total 의 개념을 포함하고 있다고 가정한다. total 의 현재 값이 3이지만 반드시 숫자 4로 수정해야만 하는 상황에서, 당연히 숫자 3을 4로 바로 수정하지 않는다. 대신 단순히 total 을 정수 4로 다시 설정한다.
당연한 듯 보이지만 아래 예제에서 보다 명확하게 이해할 수 있다.기존 name 변수에서 메서드를 통해서 스스로 변경하지 않고, 완전하게 새로운 인스턴스를 참조하도록 name 객체를 할당하여, 전체 값을 대체했다.
값 등가성
-> 값 객체 인스턴스를 또 다른 인스턴스와 비교할 땐 객체 등가성 테스트가 사용된다. 등가성은 두 객체의 타입과 특성을 비교해서 결정된다.
부작용이 없는 행동
-> 특정 오퍼레이션을 수행할 때 어떤 수정도 발생하지 않는다면, 해당 오퍼레이션은 부작용이 없다고 말한다. 값 객체의 메서드는 반드시 부작요이 없는 함수여야 한다. 불변서의 특성을 침해해선 안되기 때문이다.
아래 예제는 부작용이 없는 함수와 앞서 언급 했던 대체성과 맞물린 예제이다.
메서드 withMiddleInitial() 은 자신의 값을 수정하지 않는다. 따라서 부작용이 없다.
자신의 일부와 주어진 가운데 이니셜을 합쳐서 새로운 값을 인스턴스화한다. 이를 통해 모델의 중요한 도메인 비즈니스 로직을 잡아 내며, 대체성과 관련하여 앞선 예제에서 일어났던, 해당 로직(3개의 파라미터를 가진 생성자)이 클라이언트의 코드로 새나가는 상황을 방지한다.
마무리로, 어떤 특정 값 객체를 설계하는 대신에 기본 언어 값 타입(원시 타입 혹은 래퍼 타입) 을 사용하기로 결정한다면, 모델을 속이는 결과가 된다. 기본 값 타입(int, long, double, boolean) 등은 도메인에 맞춘 부작용이 없는 함수를 할당할 수 없게 된다. 이는 도메인에 관한 깊은 통찰을 얻기 힘들다.
이와 관련되어서 아마도 대부분의 설계를 값 객체로 해야 될것 같다고 느낄 수 있다. 나 또한 마찬가지다.
책에서는 단일 특성을 특별한 기능 없이 값 타입 안에 불필요하게 래핑하는 '실수' 를 하더라도, 아예 갑 설계에 동의하지 않는 사람보다는 나을 수 있다고 한다. 조금 지나치게 사용하더라도 언제든 리팩토링이 가능하다고 한다.
값으로 표현되는 표준 타입
-> 유비쿼터스 언어가 PhoneNumber(값) 를 정의했다면, 각 타입에 관한 설명도 필요하다.
1. 집 전화번호
2. 휴대폰 번호
3. 회사 번호
4. 그 외
그렇다면, 다양한 타입의 전화번호는 클래스 계층구조로 모델링돼야 하는가? 각 타입별로 별도의 클래스를 사용하면 클라이언트가 이를 구분하기 어려워진다. 표준 타입을 사용해 Home 이나 Mobile, Work, Other 등으로 전화의 타입을 나타내는 편이 좋다.
앞서, 언급했던 Money 에서는 Currency 라는 표준 타입을 사용한다.
ex) AUD, CAD, CNY, EUR, GBP, JPY, USD, KRW 등등
Money 에 amount 를 잘못 할당할 순 있지만, currency 는 타입으로써 제한하기에 잘못 할당할 수 있는 가능성을 배제시킬 수 있다.
만약 Currency 라는 타입을 사용하지 않고, String 으로 통화를 나타냈다면, 모델은 잘못된 상태에 빠지게된다.
Java 에서는 enum 을 활용하여 표준 타입을 정의할 수 있다.
댓글
댓글 쓰기