본문 바로가기
도메인 주도 설계

반버논이 말하는 Value Object 란?

by simplify-len 2020. 8. 12.

반버논의 도메인 주도 개발 서적에서 발췌

값 객체(Value Object)

DDD의 필수적인 구성 요소.

왜?

값의 이점에 대해 이해하라.

측정하고 수량화하거나 설명해주는 값 타입은 생성,테스트, 사용, 최적화, 유지 관리가 더 쉽다.

가능한 위치에선 엔티티 대신 값 객체를 사용해 모델링하도록 노력해야 한다는 사실을 알게 되면 놀랄지도 모른다. 심지어 도메인 개념이 엔터티로 모델링돼야할 때도 엔티티의 설계는 자식 엔티티의 컨테이너보다는 값의 컨테이너로 동작하는 쪽으로 기울어야 한다.

배울 내용

  • 값으로 모델링하기 위해 도메인 개념의 특징을 이해하는 방법을 배우자
  • 통합의 복잡성을 최소화하기 위해 값 객체를 활용하는 방법을 살펴보자
  • 값으로 표현된 도메인 표준 타입의 사용을 확인하자.
  • 사스오베이션은 어떻게 값의 중요성을 배웠는지?
  • 사스오베이션 팀이 자신들의 값 타입을 테스트하고 구현하고 저장한 방법?

엔티티만을 생각해서는 개발에 투입되는 시간과 노력이 증가되는 결과를 초래한다

= 정신적인 수고를 덜어준다?

값이 제공하는 사용 용이성과 함께, 우리가 감당할 수 있는 수준에서 최대한 많은 유형의 값 객체를 사용하는 케이스를 찾아야 한다.

모델 요소의 특성에만 신경을 쓰고 있다면, 이를 값 객체로 분리하라. 값 객체가 담을 특성의 의미를 표현하고, 그에 관한 기능도 부여하자. 값 객체를 변경이 불가능한 것으로 취급하자. 식별자는 부여하지 말고, 엔티티를 유지할 때 필요한 설계 복잡성을 피하도록 하자.

설계를 진행하면서 특정 인스턴스를 엔티티로 모델링할 지, 값으로 모델링할지 혼란스러움.

값의 특징으로 이 혼란스러움을 잠재워보자.

가장 먼저 도메인 개념을 값 객체로 모델링할 땐 유비쿼터스 언어를 확실히 활용하자. 이를 반드시 달성해야 하는 가장 중요한 원칙이자 특징으로 여기자.

개념을 만약에 값으로 나타낼 것이라 결정한다면. 반드시 이런 특성을 가진다.

  • 도메인 내의 어떤 대상을 측정하고, 수량화하고, 설명한다.
    : 개념적 전체(Conceptual Whole) / 모델 내에 진정한 값 객체가 있다면, 이것은 도메인 안에 있지 않다.
  • 불변성이 유지될 수 있다.
    : 기호에 따라 엔티티의 참조를 갖고 있는 값 객체를 설계할 수도 있음. 그럴 때는 위배되는 행위이니, 참조하는 엔티티는 컴포지션의 불변성, 표현성,편리함 등을 위해 사용된다는 사고 방식을 갖는 편이 최선임.
  • 관련 특성을 모은 필수 단위로 개념적 전체를 모델링한다.
    : 값 객체는 하나 이상의 개별적 특성을 가질 수 있으며, 각 특성은 서로 연관돼 있다. 각 특성은 전체에 기여하는 중요한 한 부분으로서, 여러 특성이 설명하는 바를 모아 전체를 나타낸다.

개념적 전체설명
Value Object는 하나, 몇. 개 또는 다수의 개별 속성을 가질 수 있으며, 각각은. 다른 속성과 관련됩니다. 각 속성은 전체적으로 속성이 설명하는 전체에 중요한 부분을 제공합니다. 다른 속성과는 별도로, 각 속성은 응집력있는 의미를 제공하지 못합니다. 모든 속성이 함께 의도 된 완전한 측정 또는 설명을. 구성합니다. 이것은 단순히 속성 집합을 객체로 그룹화하는 것과 다릅니다. 전체가 모델의 다른 것을 적절하게 설명하지 못하면 그룹화 자체는 거의 수행하지 않습니다.

값 클래스의 생성자는 개념적 전체의 효과성에 영향을 미친다.

기본값 타입(long, string, int)의 과다한 사용은 도메인에 관해 빌드인된 내용이 없어 추가적인 내용을 만들어 줘야 한다. 가장 중요한 점은 클래스 Double이 도메인에 관해 무엇도 명시적으로 알려주지 못한다는 것. 유비쿼터스 언어를 적용하지 않았기 때문에 도메인적 고려사항이 무엇인지 알 수 없게 됨.

측정이나 설명이 변경될 땐 완벽히 대체 가능하다.
불변값의 변하지 않는 상태가 현재의 전체 값을 올바르게 나타내고 있는 이상, 엔티티는 반드시 해당 값의 참조를 갖고 있어야 한다. 만약 상태가 올바르지 않는 상황이 왔다면, 현재의 전체를 올바르게 나타내는 새로운 값으로 전체 값을 완전히 대체해야 한다.

  int total = 3;
  //나중에
  int total = 4;  
  • 다른 값과 등가성(value equality)을 사용해 비교할 수 있다.
    값 객체 인스턴스를 또 다른 인스턴스와 비교할 땐 객체 등가성 테스트가 사용? Equal 메소드와 같다.
  • 협력자(collaborator)에게 부작용이 없는 행동을 제공한다.
    함수란 고유의 상태를 변경하지 않고 출력을 만들어내는 객체의 오퍼레이션을 말한다. 특정 오퍼레이션을 수행할 때 어떤 수정도 발생하지 않는다면, 해당 오퍼레이션은 부작용이 없다고 말한다. 불변성 값 객체의 메소드는 반드시 부작용이 없는 함수여야 하는데, 불변성의 특성을 침해 해선 안되기 때문.

값이 엔티티를 참조할 때?

값 객체 메소드가 매개변수로 전달된 엔티티를 수정할 수 있도록 허용해야 할까? 책에서는 이렇게 할 경우, 테스트와 부작용에 취약해진다고 이야기한다. 따라서 엔티티를 매개변수를 받는 값의 메소드가 있다면, 엔티티가 자신의 규칙에 맞춰 스스로 변경하는 데 사용할 수 있도록 결과를 응답하는 편이 최선이다.

값 객체인 BusinessPriority 가 어떤 식으로든 엔티티인 스크럼 Product 를 사용해서 우선순위를 계산하는 예

float priority = businessPriority.priorityOf(product)

위 코드의 문제점은 무엇일까?

  • 값이 Product에 의존토록 할 뿐만 아니라 해당 엔티티의 형태를 이해하도록 강제하고 있다는 점을 살펴보면, 의존하는 값을 제한하고 스스로의 타입과 그 특성의 타입을 이해하자. 이는 항상 가능하진 않지만 바람직한 목표
  • 코드를 읽는 사람은 Product 의 어떤 부분이 사용될지 모른다. 표현이 명시적이지 않으며, 이는 모델의 명확성을 약화, Product의 실제 속성 일부나 파생된 속성을 전달했다면 휠씬 나았을 것
  • 엔티티의 매개변수로 갖는 모든 값 메소드가 엔티티의 수정을 유발하지 않는다는 점을 쉽게 증명할 수 없고, 그렇기 때문에 오퍼레이션을 테스트하기가 더욱 어려워진다는 점. 그러므로 값이 수정을 일으키지 않음을 약속하지만, 실제로 수정이 없다고 증명하긴 쉽지 않다.

그러나 만약 이렇게 바뀐다면,

float priority = businessPriority.priority(product.businessPriorityTotals());

값을 견고하게 만들기 위해, 값 메소드의 매개변수로 오직 값만을 전달하자. 단순히 Product의 도메인 서비스가 수행하게 되면서 계산하게 한다. 이렇게 한다면 가장 훌룡한 수준에서 부작용이 없는 행동을 만들 수 있다.

미니멀리즘으로 통합하기.

값 객체를 사용해 유입되는 업스트림 컨텍스트로부터 다운스트림 컨텍스트의 개념을 모델링하자. 이를 통해 우선순위를 미니멀리즘 에 따라 통합할 수 있으며, 이는 다운스트림 모델을 관리하는 책임이라 볼 수 있는 속성의 수를 최소화해준다. 불변 값을 결과로서 사용한다면 책임을 덜 수 있다.

무슨말일까 코드를 봐야 이해가 될까??

불변 값을 많이 사용해서 책임을 덜으라는 이야기.

업스트림 식별자와 액세스 컨텍스트의 애그리게잇이 다운스트림인 협업 컨텍스트에 영향을 미쳤던 상황을 생각해보자. 식별자와 액세스 컨텍스트의 두 애그리게잇은 User와 Role이다. 협업 컨텍스트는 User가 중개자 역할에 해당하는 Role을 수행하는지 여부에 관심이 있다.

image-20200812144524381

협업 컨텍스트는 자신의 부패 방지 계층을 사용해 식별자와 액세스 컨텍스트의 오픈 호스트 서비스를 쿼리한다?

통합에 기반한 쿼리가 특정 사용자가 중개자 역할을 수행하고 있음을 알려주면, 협업 컨텍스트는 이를 대표할 Moderator라는 객체를 생성

업스트림인 식별자와 액세스 컨테스트의 여러 애그리게잇이 협업 컨텍스트에 미치는 영향을 최소화한 부분.

Moderator는 자신의 많지 않은 특성을 통해 협업 컨텍스트에서 이야기되는 유비쿼터스 언어의 필수 개념을 모델링한다. 게다가 Moderator클래스는 Role 애그리게잇 특성을 단 하나도 갖고 있지만, 클래스 이름 자체에서 사용자가 수행하는 중개자 역할을 드러낸다.

@Override
public Moderator moderatorFrom(Tenant aTenant, String anIdentity) {
  Moderator moderator =
    this.userInRoleAdapter()
    .toCollaborator(
    aTenant,
    anIdentity,
    "Moderator",
    Moderator.class);

  return moderator;
}

public interface UserInRoleAdapter {

public <T extends Collaborator> T toCollaborator(
    Tenant aTenant,
    String anIdentity,
    String aRoleName,
    Class<T> aCollaboratorClass);
}
package com.saasovation.collaboration.domain.model.collaborator;

public final class Moderator extends Collaborator {

    private static final long serialVersionUID = 1L;

    public Moderator(String anIdentity, String aName, String anEmailAddress) {
        super(anIdentity, aName, anEmailAddress);
    }

    protected Moderator() {
        super();
    }

    @Override
    protected int hashPrimeValue() {
        return 59;
    }
}

image-20200812154022045

왜 자꾸 나의 가정을 의심하라고 하는가?

여러분의 가정을 의심하라.

지금 설계하고 있는 객체가 자신의 행동으로 인해 변경돼야 한다고 생각한다면, 그 필요성을 스스로 질문해보라. 값을 반드시 변경해야 한다면 대신할 대상을 활용하는 편이 어떻까?

가능한 상황에 이 접근법을 사용하면 설계는 단순화.

간혹 객체의 불변성이 아무의미가 없을 때도 있다.

이런 상황에는 엔티티로 모델링돼야 함을 의미.


구현

이 부분이 재미있는 부분이다.

코드를 통해 이해해보자

우선 기본값 상태를 초기화.

기본 특성 초기화는 프라이빗 세터를 호출해 수행하고, 자가 위임의 사용을 추천합니다.

두 번째 생성자는 복사 생성자로 불리는데, 이는 기존의 값을 복사해 새로운 값을 생성하는 데 쓰인다. 이 생성자는 복사할 값의 특성을 기본 생성자의 매개변수로 전달해, 얕은 복사로 알려진 방식에 따라 자가 위임을 수행한다.

이 두 번째 생성자는 단위 테스트 시에 중요하다. 우린 값 객체를 테스트할 땐 불변성의 검증을 추가하고 싶어한다.


public class BusinessPriority extends ValueObject {

    private BusinessPriorityRatings ratings;

    public BusinessPriority(BusinessPriorityRatings aBusinessPriorityRatings) {
        this();

        this.setRatings(aBusinessPriorityRatings);
    }

    public BusinessPriority(BusinessPriority aBusinessPriority) {
        this(new BusinessPriorityRatings(aBusinessPriority.ratings()));
    }
  ...
public float costPercentage(BusinessPriorityTotals aTotals) {
  return (float) 100 * this.ratings().cost() / aTotals.totalCost();
}

public float priority(BusinessPriorityTotals aTotals) {
  float costAndRisk = this.costPercentage(aTotals) + this.riskPercentage(aTotals);

  return this.valuePercentage(aTotals) / costAndRisk;
}

public float riskPercentage(BusinessPriorityTotals aTotals) {
  return (float) 100 * this.ratings().risk() / aTotals.totalRisk();
}

public float totalValue() {
  return this.ratings().benefit() + this.ratings().penalty();
}

public float valuePercentage(BusinessPriorityTotals aTotals) {
  return (float) 100 * this.totalValue() / aTotals.totalValue();
}

public BusinessPriorityRatings ratings() {
  return this.ratings;
}

부작용 없는 함수의 메소드 이름은 중요하다. 이런 메소드는 모드 값을 반환하지만, 의도적으로 get접두사의 자바빈 이름을 사용하지 않는다. 이런 객체 설계의 단순하지만 효과적인 접근법을 통해 값 객체가 유비쿼터스 언어에 충실하도록 유지할 수 있다.

getValuePercentage() 의 사용은 기술적 측면에서의 컴퓨터 명령이지만, valuePercentage() 는 사람이 유창하게(fluent) 읽을 수 있는 언어 표현이다.

자바 Bean API 는 언의 유창함을 방해한다. 한 예로 java.lang.String에서 get쓰는 것은 딱 한가지.

값 객체의 저장

값 객체 인스턴스를 영속성 저장소에 저장하는 방법에는 여러 가지있음.

값의 ORM 영속성 예제로 들어가기에 앞서, 잘 이애한 후 성실히 따라야 하는 모델링에서의 중요한 약속이 있다. 따라서 논의를 시작하기 위해(도메인 모델링과는 달리) 데이터 모델링이 여러분의 도메인 모델의 잘못된 영향을 미칠 때 어떤 일이 일어나는지, 이런 잘못되고 유해한 영향에 맞서기 위해 할 수 있는 일은 무엇인지 먼저 살펴보자.

데이터 모델 누수의 부정적 영향을 거부하라

값 객체를 데이터 저장소로 저장하는 대부분의 경우는 비정규화된 방식으로 저장. 즉, 해당 특성은 부모 엔티티 객체와 같은 데이터베이스 테이블 행에 저장된다. 이는 저장소와 값을 가져오는 과정을 깔끔하게 최적화하도록 해주고, 영속성 저장소의 누수를 막아준다.

그러나, 모델 내의 값 객체가 반드시 관계형 영속성 저장소의 엔티티로 저장돼야 할 때가 있다. 즉 저장 시에 특정 값 객체 타입의 인스턴스가 해당 타입을 위한 관계형 데이터 베이스 테이블에서 자신만의 행을 차지하고, 자신만의 데이터베이스 기본키 열을 갖게 된다. 이 말은 도메인 모델 객체가 데이터 모델의 설계를 반영해야 하고, 값보다는 엔티티가 돼야 한다는 의미일까? 그렇지 않다. 이런 임피던스 불일치에 따른 결과를 마주하면, 영속성의 관점보다는 도메인 모델의 관점을 유지하는 것이 중요.

ORM은 값을 엔티티로 만드는 저주를 내린다.

영속성의 관점보다는 도메인 모델의 관점을 유지하는 것이 중요.

  1. 내가 모델링하는 대상의 개념이 도메인 내에 있는가? 아니면 속성 중의 하나로서 대상을 측정하거나 수량화하거나 설명하는가?
  2. 도메인의 요소를 설명하도록 올바르게 모델링했을 때, 이 모델 개념은 앞서 강조했던 값 특성의 대부분을 포함하는가?
  3. 단순히 하위 데이터 모델이 도메인 모델 객체를 엔티티로서 저장해야 하기 때문에 모델 내에서 엔티티의 사용을 고려하고 있진 않는가?
  4. 도메인 모델이 고유 식별자를 요구하기 때문에, 내가 개별 인스턴스를 신경쓰기 때문에, 내가 객체의 수명주기에 걸친 변화의 연속성을 관리해야만 하기 때문에 엔티티를 사용하는가?

설명한다. 그렇다. 그렇다. 아니다. 라면 무조건 값 객체 써야 한다.

가능하다면, 여러분의 도메인 모델을 위해 데이터 모델을 설계해야지, 데이터 모델을 위해 도메인 모델을 설계해서는 안된다.

댓글