JAVA/JAVA | Spring 학습기록

[JAVA] VO(Value Object)로 원시값을 포장해보자

kth990303 2022. 3. 16. 13:57
반응형

VO(Value Object)란, 도메인에서 속성들을 묶어서 값을 나타내는 객체이다.

사실 나는 VO에 대해서 단순하게 "도메인 중에서 가장 기본이 되는 객체" 정도로만 알고 있었는데, 이번에 우아한테크코스 미션을 진행하면서 VO 개념을 정립할 수 있었다.


VO란 무엇일까?

사실 VO의 개념에 대해선 이 글이 정말 잘 정리가 돼있다.

https://tecoble.techcourse.co.kr/post/2020-06-11-value-object/

 

VO(Value Ojbect)란 무엇일까?

프로그래밍을 하다 보면 VO라는 이야기를 종종 듣게 된다. VO와 함께 언급되는 개념으로는 Entity, DTO등이 있다. 그리고 더 나아가서는 도메인 주도 설계까지도 함께 언급된다. 이 글에서는 우선 다

tecoble.techcourse.co.kr

(tecoble 홍보 아님. 진짜 잘 설명돼있다.)

 

어떤 원시값을 포장하기 위해 VO를 만들었다면,

속성(원시값들)이 동일할 때, 같은 객체로 만들어야 할 것이다.

따라서 equals, hashcode 재정의가 필수적이다..

 

또한, 이 속성들은 상태를 나타내는 것이므로 불변(final)으로 만들어주어야 한다.

위 글에서 이 내용을 그대로 설명해주고 있다.


VO에 대해 잘 몰랐을 때

나는 이번에 우아한테크코스 블랙잭 미션을 하면서 아래와 같은 피드백을 받았다.

Cards 클래스 코드에서 피드백을 받았는데,

Cards 클래스에선, 현재 참가자가 보유중인 카드들에서 얻을 수 있는 정보 (점수계산, 블랙잭 여부, 버스트 여부 등)를 도메인로직으로 처리하는 코드들을 가지고 있었다.

 

처음에는 VO에 대한 개념이 잘 없었어서,

Score 클래스를 새로 만들어서 Cards 클래스의 책임을 위임하라는 것인줄 알았다.

Score 클래스가 Cards의 책임을 위임하기 위해선, 가지고 있는 카드들의 정보를 의존해야 되는데, 이게 맞나? 싶었다.

 

그래서 아래와 같이 클래스를 만들기도 했었다.

public int calculateFinalScore(Set<Card> cards) {
    final int score = calculateCardsSum(cards);
    if (hasAce(cards) && checkValidationAceBonusScore(score)) {
        return score + ACE_BONUS_SCORE;
    }
    return score;
}

private boolean hasAce(Set<Card> cards) {
    return cards.stream()
            .anyMatch(Card::isAce);
}

private int calculateCardsSum(Set<Card> cards) {
    return cards.stream()
            .mapToInt(Card::getScore)
            .sum();
}

cards에 의존하면서 점수계산 로직을 처리하는, VO가 아닌 이상한 객체를 만들어버렸다.

심지어 위 객체는 상태도 전혀 없이, 위에 언급한 cards에 의존하는 메서드들만 가지고 있었는데,

Score 객체를 만든 이유가 전혀 없는, 단순 메서드 덩어리에 불과한 객체가 만들어졌다.

VO에 대해 전혀 이해하지 못한 객체가 생성된 것이다.


VO를 공부하고 난 후

그런데, VO에 대해서 공부하고 나니 

Score을 VO로 만들라는 리뷰어 핀의 피드백이 이해가 가기 시작했다.

 

블랙잭 게임에서는 점수에 따라 게임 결과에 크게 영향을 미친다!

21점일 경우 블랙잭이 가능한 점, 21점 초과일 경우 버스트인 점, 특정 점수일 때 ACE가 1점이 아닌 11점으로 진행하는 것이 유리한 점 등, 특정 점수일 때 Cards에서 진행가능한 로직이 굉장히 많았다.

따라서 Score를 VO로 만들어주어 특정 결과가 가능한 여부를 판단하도록 책임을 Cards에서 Score로 위임해줄 수 있다!

 

따라서 아래와 같이 int score 원시값을 포장해주는 Score 클래스를 생성해주었다.

package blackJack.domain.card;

import java.util.Objects;

public class Score {
    // ...
    private final int score;

    public Score(int cardPoint) {
        score = cardPoint;
    }

    public boolean isBlackJack(int cardCount) {
        return cardCount == BLACK_JACK_CARD_COUNT && score == BLACK_JACK;
    }

    public boolean isBust() {
        return score > BLACK_JACK;
    }
    // ...
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Score score1 = (Score) o;
        return score == score1.score;
    }

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

어떠한 점수 자체를 상태로 가지며,

이 점수가 특정 조건일 때 isBust, isBlackJack인지 파악하는 도메인 로직을 가지는 VO가 생성되었다 :)

 

이렇게 해주면 Cards 클래스 내에 존재하는 메서드들은 Score 클래스에 메시지를 던짐으로써 블랙잭, 버스트 등 특정 조건을 만족하는지 확인해줄 수 있다!

각 객체의 책임도 세분화된 것을 확인할 수 있다.

리뷰어님의 따봉을 받았다


처음엔 Score는 단순히 int값을 나타내는 것이며, domain (VO)로 작용할 줄은 상상도 못했었는데,

Score를 VO로 만들어주니까 특정 점수일 때 여러 특수 상황들을 만족하는지 체크할 수 있도록 책임을 위임해줄 수 있었다.

 

이번 리뷰어 핀이 내 PR을 merge 권장기간이 끝날 때까지 쭉 피드백해주시면서 관심있게 봐주셨는데, 덕분에 역할을 더 세분화하여 분리할 수 있었고, VO의 장점에 대해서도 실감할 수 있었다 :)

 

그 외에도 추가적으로 배운 점들이 많은데, 나중에 블랙잭 미션이 끝나면 우아한테크코스 카테고리에 추가로 글을 작성해보도록 하겠다.

 

내 블랙잭 1단계 미션 PR은 아래 링크에서 확인가능하다.

https://github.com/woowacourse/java-blackjack/pull/253

 

[1단계 - 블랙잭] 케이(김태현) 미션 제출합니다. by kth990303 · Pull Request #253 · woowacourse/java-blackjack

안녕하세요 핀! 케이입니다. 이번 블랙잭 미션 리뷰요청드립니다! 미션을 진행하면서 궁금한 점이 하나 생겨 질문드립니다 😄 Service Class가 하는 역할 이전 페어미션 때엔 service 클래스를 생성

github.com

 

반응형