[Java] VO(Value Object)란? 왜 사용하는데?

nana·2024년 10월 19일
0

JAVA

목록 보기
9/11

0. 이 글을 찌게 된 이유

내가 하고있는 이커머스 프로젝트에서는 최근 3일간의 판매 순위 Top5를 뽑아오는 Rank API가 존재한다.


RankingRepository.java

RankingService.java

JPQL로 3일전에 판매된 데이터를 모두 가져와서 같은 productId로 그룹핑 한 후
판매 개수를 SUM해서 List<Object[]> 형태로 반환해 주는데 이 코드에서

findByNowdateForRanking 에서 object로 리턴을 하는 것보단 해당 값을 표현하기 위한 별도의 vo를 구성하는편이 좋을것 같아요.

이런 피드백을 받았다.

VO? VO라는 말을 처음 들어봐서 어떻게 변경해야하는지 감이오지 않아서 이 글을 찌게되었다 :D

1. VO(Value Object)

VO란 도메인에서 한 개 또는 그 이상의 속성들을 묶어서 특정값을 나타내는 객체를 의미한다.

  • 해당 속성들은 primitive타입이다.
    • 변경 불가능하며 오직 읽기만 가능(Read-Only)
      : DTO는 setter를 가지고 있어 값을 변경할 수 있지만, VO는 getter만을 가지기 때문에 읽기만 가능하고 수정은 불가능하다.

    1-1. DTO vs VO

    • DTO : 데이터를 담아 전달하는 역할을 하는 인스턴스 개념
    • VO : 값들에 대해 읽기만 가능한 Read-Only속성을 가져 객체로서 데이터 그 자체에 의미를 갖는다. 리터럴값(변하지 않는 데이터)

2. Value Object의 특성

2-1. Immutability(불변한)

  • 생성자를 통해 한 번 생성되면 이후에는 내부의 값을 변경할 수 없어야 한다.

  • VO객체는 이를 보장하기 위해서 다음과 같은 작업이 필요하다.
    Setter를 허용하지 않는다.
    GC에 의해 폐기 될 때 까지 불변해야 한다.

    2-1-1. 불변으로 얻는 장점

  • Hassle-free Sharing(번거롭지 않은 공유)
    : 사용도중에 값이 변경되지 않으므로 참조로 공유가 가능하며 side effect를 피하고, 동시에 코드의 복잡성과 부하를 감소시킨다. > 멀티쓰레드 환경에서 유용함!

  • Improved Semantics(향상된 의미)
    : 명확한 이름과 동작을 가질 수 있게 된다.
    이를 위해서 VO의 초기 클래스에는 생성자와 private 인스턴스 변수만 있어야 한다.
    무의미한 인터페이스 생성을 피하고 의미 있는 이름과 동작을 가지게 된다.

    * 새 인스턴스를 만들 때는 생성자 또는 static메서드 만을 사용한다.
    * 현재 VO를 통해 새로운 VO를 생성한다. 
    * 내부의 데이터를 추출해 다른 타입으로 변환한다. 

2-2. Value Equality(값 동등성)

VO는 동등성 검사를 해야하고 각각의 값이 같다면 두 VO객체는 동일하다고 판단한다.

public class Car {

  private final String name;
  private final int position;
  
  ...
}
public class Main {

  public static void main(String[] args) {
      Car carOne = new Car("sun", 1);
      Car carTwo = new Car("sun", 1);
  }
}

두 자동차 객체 carOne과 carTwo를 생성했을 때, carOne과 carTwo는 다른 객체이지만 결국 동일한 내부 값을 가지고 있으므로 동등하다고 판단하는 것이 VO의 값 동등성이다.
✨ 이때 같은 값을 가지고 있는지 판단하는 메서드를 만들거나 equalshashCode()함수를 재정의 해야한다.

2-3.Self Validation(자가 유효성 검사)

VO는 유효하지 않은 값으로 값 객체를 만들 수 없도록 유효성 검사를 시행해야한다.
필드 중 하나라도 유효하지 않다면 적절한 예외를 던져야 하는데 이러한 강제 검증은 도메인 제약조건을 의미 있고 명시적인 방식으로 표현하는데 유용하다.

  • VO를 사용하는 쪽에서 유효성 검사가 보장되어 있으므로 안전하게 사용할 수 있다.

3. VO를 쓰는 이유

VO가 필요한 이유는 primitive 타입이 도메인 객체를 모델링 하기 위해 충분하지 않기 때문.

4. 그렇다면, 코드를 리팩토링 해 보자.

//RankingRepository.java
List<Ranking> findByNowdateForRanking(@Param("threeDaysAgo") LocalDateTime threeDaysAgo, Pageable pageable);
//Ranking.class 
import lombok.Getter;

@Getter
public final class Ranking {
  private final Long productId;
  private final Long orderCount;

  public Ranking(Long productId, Long orderCount) {
      this.productId = productId;
      this.orderCount = orderCount;
  }
}
//RankingService.java
// VO로 변경 시작
 List<Ranking> orderedList = rankingRepository.findByNowdateForRanking(threeDaysAgo, pageable);

      return orderedList.stream()
              .map(r -> {
                  Long productId = r.getProductId();
                  Long orderCount = r.getOrderCount();
                  //VO로 변경 완료
                  Product product = productRepository.findByProductId(productId);

                  return new RankingResponse(
                          product.getProductId(),
                          product.getProductName(),
                          orderCount,
                          product.getPrice(),
                          product.getCategory()
                  );
              }).collect(Collectors.toList());

5. 결론

참고할 만한 글을 찾아보다가 DTO와 DAO, VO 를 비교해 놓은 글을 많이 보게되었는데 이후에 DTO공부를 하면서 비교해 봐야겠다.
단순히 값만 받아와서 반환해 주는 역할을 하는거라면 VO를 자주 사용해야겠다.


참고

profile
BackEnd Developer, 기록의 힘을 믿습니다.

0개의 댓글