Post

VO에 대해

DTO, VO, Entitiy에 대해 공부하다보니 뭔가 VO에 대한 개념이 잘 잡히지 않았다.

그래서 추가로 VO에 대해 찾아봤고 어느정도 개념이 잡힌 것 같다.

Intro

VO는 많은 사람들이 DTO와 혼용하는 경우가 많다.

하지만 DTO와 VO는 각기 다른 역할을 가지고 있다.

  • DTO : 객체 간의 데이터 교환을 위한 객체
  • VO : 객체를 값처럼 쓰기 위한 객체

위의 내용만 본다면 무슨 소리인지 알기 힘들기 때문에 예제를 통해 알아보자.

VO에 관련된 질문

만약 아래와 같은 질문을 받았다고 생각해보자.

1
2
3
4
사람의 나이를 나태내기 위한 변수에 어떤 타입을 지정해야될까?
1. Integer
2. Boolean
3. String

대부분의 사람들이 나이는 숫자로 나타낼 수 있으니 Integer가 아닐까? 라는 생각으로 Integer을 고를 것이라고 생각한다.

하지만 답은 3개 중에 없다이다. 답은 사람의 나이를 표현하기 위한 Age 타입을 직접 만들어 지정해줘야한다.

그 이유는 Integer 타입의 경우 age가 가지지 않은 속성과 연산들을 가지고 있기 때문이다.

  • 나이를 가지고 더하기나 빼기 등의 연산이 필요한가?
  • 음수의 나이가 존재할 수 있는가?

위와 같이 생각해보면 Integer 타입이 뭔가 부적절하다고 느낄 수 있을 것이다.

그래서 Age는 정수가 아니라 Integer 타입으로 표현할 수 없다는 것이다.

도메인 객체를 나타내기 위해 primitive type으로 쓰는 나쁜 습관을 primitive obsession이라고 부른다.

또 만약 나이를 나타내는데 Integer 타입을 사용했다면 그 변수가 알맞은 값으로 초기화 하거나 한번 할당된 나이가 변경되지 않는 것을 보장하기 위해선 할당 전에 해야될 것이다.

이런 이유 때문에 우리는 VO의 필요성에 대해 느낄 수 있다.

VO

  • VO는 불변하다 - 수정자(setter)가 없다.
  • 같은 값을 가지는 VO는 같다고 취급한다.
  • 자가 유효성 검사 - 생성자에서 유효성 검사를 진행한다.

1. Immutability

VO는 불변이다. 즉, 한번 생성한 후에는 내부 값을 바꿀 수 없게 setter를 허용하지 않는다.

이것을 얻을 수 있는 이점은 다음과 같다.

  • Hassle-free Sharing

    VO는 다른 곳에서 수정할 수 없기 때문에 참조값으로 공유가 가능하다. side effect를 피하고 Multi-thread에서 이점이 있다.

  • Improved Semantics

    무의미한 getter를 VO에 추가하지 말자. 그리고 다음과 같이 VO를 조작함으로써 VO가 더 명확하게 해줄 수 있다.

    • 생성자 또는 정적 메소드를 통해 새 인스턴스 만들기
    • 현재 객체에서 다른 객체를 생성하기
    • 내부 데이터를 추출하여 다른 유형으로 변환하기

    이 부분은 조금 어려워 다음으로 넘어가자

2. value equality

만약 친구와 카드 게임을 하고 있다고 상상해보자.

서로 카드를 뽑아들었는데 만약 친구와 내가 같은 카드를 뽑았다. 그럼 두 카드가 정말로 같은지 서로 확인할 것이다.

또 만약 두 카드를 서로 바꾼다고 어떤 결과가 달라지지 않는다. 즉, 두 카드가 같은 값을 가지고 있기 때문에, 두 카드를 구별할 수 없다.

결국 카드 객체는 VO라고 말할 수 있는 것이다.

내부의 값이 동일한 두 객체는 동일한 객체라고 판단할 수 있다.

예제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
final class Card {
    private Rank rank;
    private Suit suit;

    public Card(Rank rank, Suit suit) {
        this.rank = rank;
        this.suit = suit;
    }

    public boolean sameAs(Card anotherCard) {
        return rank.sameAs(anotherCard.rank) &&
                suit.sameAs(anotherCard.suit);
    }
}

3. self validation

VO는 유효한 값만 허용해야한다. 유효하지 않다면 객체를 만들 수 없다.

따라서 생성자가 주입될 때 값의 유효성 검사를 진행해야한다.

예제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Numbers {

    private List<Integer> numbers;

    private Numbers(List<Integer> numbers) {
        this.numbers = numbers;
        validateNumbers();
    }

    public static Numbers of(List<Integer> numbers) {
        return new Number(numbers);
    }

    private void validateNumbers() {
        if (this.numbers.contains(0)) {
            throw new IllegalArgumentException("0 이외의 값만 허용합니다");
        }
    }
}

정리

어떤 객체를 VO로 만들 때 그 해당 객체의 특성을 잘 이해하고 그에 맞는 VO를 만들어야한다.

그러기 위한 체크 리스트는 다음과 같다.

  • 불변하며 수정자가 없음
  • 도메인의 의미론을 반영
  • 런타임 동안 정보의 흐름과 변환 방법을 보여줌.
  • default나 쓸모없는 getter 메소드가 없다
  • private 속성을 가지며 다른 Value Object와 비교할 수 있다.

출처)

VO(Value Object)란? (velog.io)

[Value Objects Like a Pro. How to build the perfect class for your…by Nicolò PignatelliMedium](https://medium.com/@nicolopigna/value-objects-like-a-pro-f1bfc1548c72)
This post is licensed under CC BY 4.0 by the author.