VO(Value Object)

디우·2022년 3월 4일
0

우테코에서 진행한 두번째 미션인 로또 구현..을 하면서 VO(Value Object)에 대한 개념을 피드백으로 받게 되었다.

이전에도 다른 크루들이 VO에 대한 글을 학습로그에 올리는 것을 보았지만 당시에는 내가 직접 적용하고 있던 개념도 아니었고 이름 그대로 값 객체 이구나..정도로만 생각하고 넘어갔었다.

다행히 바로 이렇게 VO에 대한 피드백이 있게 되었고, 관련해서 공부해 보게 되었다.

VO에 대한 피드백


VO(Value Object) 란??

위에 피드백에서 알 수 있다시피 로또 구입을 위한 최소한의 금액 을 포장하는 Money(혹은 LottoMoney)를 구현했다고 생각해보자.

(여기서 Integer 를 그냥 사용하면 안될까요? 라고 물을 수 있는데 위의 피드백에서 언급한 것과 같이 500원 이라는 의미로 단순히 Integer를 사용하게 된다면 생성 시점에 검증이 불가능하고 예상치 못한 버그를 마주하게 될 수 있다.)

그렇다면 이 Money(LottoMoney)는 무엇을 나타내는 객체일까? 이는 단순히 값 그 자체를 표현하는 객체이다. 그리고 우리는 이런 객체를 VO(Value Object) 라고 하기로 했다.

물론 현재 예시인 LottoNumber에는 한 개의 속성만을 표현하고 있지만 그 이상의 속성들을 묶어서 특정 값을 나타낼 수도 있다.

그리고 VO도 또한 도메인 객체의 일종이며 객체의 불변성을 보장한다.


나의 시선에서 본 VO(Vaue Object)의 특징

public class LottoNumber implements Comparable<LottoNumber> {

    private static final int MINIMUM_LOTTO_NUMBER = 1;
    private static final int MAXIMUM_LOTTO_NUMBER = 45;
    private static final Map<Integer, LottoNumber> LOTTO_TOTAL_NUMBERS = IntStream.rangeClosed(MINIMUM_LOTTO_NUMBER, MAXIMUM_LOTTO_NUMBER)
            .boxed()
            .collect(toMap(identity(), LottoNumber::new));

    private final int number;

    private LottoNumber(int number) {
        this.number = number;
    }

    public static LottoNumber from(int number) {
        validateNumberBoundary(number);

        LottoNumber lottoNumber = LOTTO_TOTAL_NUMBERS.get(number);

        return Objects.requireNonNullElseGet(lottoNumber, () -> new LottoNumber(number));
    }

    private static void validateNumberBoundary(int number) {
        if (number < MINIMUM_LOTTO_NUMBER || number > MAXIMUM_LOTTO_NUMBER) {
            throw new IllegalArgumentException("1~45의 숫자이어야 합니다.");
        }
    }

    public int getNumber() {
        return number;
    }

    public static List<LottoNumber> getLottoTotalNumbers() {
        return new ArrayList<>(LOTTO_TOTAL_NUMBERS.values());
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        LottoNumber that = (LottoNumber) o;
        return number == that.number;
    }

    @Override
    public int hashCode() {
        return Objects.hash(number);
    }

    @Override
    public int compareTo(LottoNumber o) {
        return number - o.getNumber();
    }
}

위 코드는 최종 완성한 VO에 해당하는 LottoNumber 클래스이다.

앞의 예시에서 언급한 LottoMoney와는 다른 VO 이다.

1. VO는 도메인의 일종으로 로직을 포함할 수 있다.

이 부분에서 상당히 고민을 했었다.

VO가 어떤 값을 표현하기 위한 객체라면 로직을 포함해도 되는 걸까?? 값 그 자체를 표현하는데만 집중해야하는 것 아닐까? 하는 생각이 들었기 때문이다.

그리고 위에서 언급한 것처럼 VO는 객체의 불변성을 보장해야한다는 부분도 로직을 포함해도 되나하는 생각이 들게 하였다.

하지만 여러 글들과 예시 코드들을 참조한 결과, 스스로 내린 결론은 객체의 불변성을 보장하는 한 로직을 포함해도 된다.이다.

위의 예시 코드를 보아도 from, validateNumberBoundary, getNumber, getLottoTotalNumbers 와 같은 메소드를 사용하고 있다. 하지만 이는 앞서 언급한 것처럼 객체 내부 속성 값을 변경하지 않는다. 즉 여전히 객체의 불변성을 유지한다.

결론적으로 수정자(Setter)를 두지 않는다. 만약 새로운 속성 값을 가지는 객체가 필요하다면 두려워하지 말고 새로 생성하자!

2. equals() 와 hashCode() 를 오버라이딩 해야 한다.

VO(Value Object)는 값 그 자체를 표현하는 객체라고 하였다. 따라서 이름이 다르다고 하더라도 모든 속성 값이 같다면 같은 인스턴스라고 이야기 할 수 있어야 한다. 따라서 VO 에서는 최상위 클래스인 Object 클래스의 equals()hashCode()를 오버라이딩해야한다.

위의 예시 코드에서도 equals()hashCode()를 오버라이딩 하여 속성 값(number 필드 속성 값)이 같으면 같은 객체라고 판단 하도록 재정의하였다.

3. 자가 유효성 검사 (생성자에서 검증)

위의 예시 코드에서는 private 생성자를 두고 정적 팩토리 메소드from()을 통해서만 객체 생성이 가능하도록 하고 있다. 그리고 그 생성 과정에서 validateNumberBoundary 호출을 통해서 유효한 값(1~45라고 하는 로또 번호)인지를 검증해주고 있다.
이는 앞에서 언급한 LottoMoney에서 단순 Integer를 사용했을 때 생성 시점에 검증이 불가능하다는 단점을 극복하게 해준다. 즉, 객체의 생성 시점에 유효성 검사가 이루어진다.

참고: https://velog.io/@livenow/Java-VOValue-Object%EB%9E%80

profile
꾸준함에서 의미를 찾자!

2개의 댓글

comment-user-thumbnail
2022년 3월 5일

디우 안녕하세요 리차드에요~
VO 에 대해 좀 희미하게 느껴지던 부분이 더 선명해진 것 같아요.
좋은 포스팅 감사합니다!! 🎶

1개의 답글