본문 바로가기
JAVA/Spring & Java 학습 기록

원시값 포장과 VO

by 구본식 2023. 9. 23.

스타트업 인턴 도메인을 파악하면서 VO의 개념 정립할 수 있었고, 스스로 공부한 내용을 기록해보고자 한다. 

(참고로 해당 서비스는 도메인과 엔티티가 분리된 환경이였다.)


원시값 포장이란?

원시값 포장이란,

String, int 등 원시 타입(Primitive)의 값을 이용해 속성을 표현하지 않고, 의미있는 객체로 포장해서 사용한다는 개념이다.

 

간단히 아래와 같다고 볼 수 있다.

String name = "홍길동"; //안티 패턴
Name name = new Name("홍길동");

name 에는 다양한 유효성 검사 등이 존재할 수 있는데, 외부에서 검사하는 것 아니라, 이러한 검사를 Name 클래스에게 책임을 넘기게 된다.

 

그럼 원시값을 사용하는 이유가 뭘까?

  • VO 라는 객체로 포장했기 때문에, 클래스 내부에서 스스로 상태를 관리하고, 책임을 분리시킬 수 있다.
    즉, 유효성 검사 등 각 클래스는 하나의 역할에 대한 책임을 가진다.
  • 객체 내에서 유연한 타입 관리가 가능하다.(변경, 추가 등)
    int를 Money 클래스로 관리했을 때, int -> double로 변경 시 Money 필드의 타입만 변경해주면된다.

예시를 살펴보자.

@Getter
public class ItemVersion1 {
	private final String name;
	private final String description;
	
	private final int purchasePrice;
	private final int sellingPrice;
	
	public ItemVersion1(String name, String desc, int purchasePrice, int sellingPrice){
		this.name = name;
		this.description = desc;
		
		validatePrice(purchasePrice);
		validatePrice(sellingPrice);
		this.purchasePrice = purchasePrice;
		this.sellingPrice = sellingPrice;
	}
	
	private void validatePrice(int price){
		if(price < 0){
			throw new IllegalArgumentException("잘못된 금액을 입력했습니다.");
		}
	}
	
	public int calculatePrice(int quantity, PriceKind priceKind){
		if(priceKind.isPurchase()){
			return Math.multiplyExact(purchasePrice, quantity);
		}
		return Math.multiplyExact(sellingPrice, quantity);
	}
	
}

Item 이라는 도메인은 이름, 설명, 구매가격, 판매 가격 등 다양한 속성을 가질 수 있다.

위의 코드에서는 생성자에서 가격 필드에 대한 유효성 검사를 Item 클래스가 검증하게 된다.

 

과연, 가격이란 필드의 유효성 검사 등이 Item 클래스에 책임일까?

Item 클래스의 속성 중 하나이기 때문에, 책임은 있다고 생각한다.

하지만 가격 제약 조건 추가 등이 생겼을 때는 어떨까? 점점 하나의 도메인이 거대해지는 현상이 발생할 것이라고 생각한다.

즉, Item 클래스는 그 자체로 상태를 관리하는 역할만 수행해야되는데, 필드 하나하나의 검증 및 예외 처리 등을 모두 관리해야 되는 책임이 생기는 것이다.

 

그럼, 원시값 포장을 한 코드를 한번 보자.

@Getter
public class Money {
	private final int price;
	
	public Money(int price) {
		validatePrice(price);
		this.price = price;
	}
	
	private void validatePrice(int price){
		if(price < 0){
			throw new IllegalArgumentException("잘못된 금액을 입력했습니다.");
		}
	}
	
	public int calculateTotalPrice(int quantity){
		return Math.multiplyExact(price, quantity);
	}
	
	@Override
	public boolean equals(Object o) {
		if(this == o)
			return true;
		if(o == null || getClass() != o.getClass())
			return false;
		
		Money money = (Money)o;
		return price == money.price;
	}
	
	@Override
	public int hashCode() {
		return Objects.hash(price);
	}
	
}
@Getter
public class ItemVersion2 {
	private final String name;
	private final String description;
	
	private final Money purchasePrice;
	private final Money sellingPrice;
	
	public ItemVersion2(String name, String desc, int purchasePrice, int sellingPrice){
		this.name = name;
		this.description = desc;
		
		this.purchasePrice = new Money(purchasePrice);
		this.sellingPrice = new Money(sellingPrice);
	}
	
	
	public int calculatePrice(int quantity, PriceKind priceKind){
		if(priceKind.isPurchase()){
			return purchasePrice.calculateTotalPrice(quantity);
		}
		return sellingPrice.calculateTotalPrice(quantity);
	}
	
}

Item 클래스는 자기 자신의 상태만 관리하고, 가격에 관련된 모든 사항은 Money 클래스 전적으로 담당하게 된다.

(유효성 검사, 가격 계산 등)

 

또한, 가격의 자료형 타입이 변경되었을 때(int -> double), 기존의 상황이라면 가격과 관련된 모든 필드의 자료형을 하나하나 바꾸어야 하지만, 현재에서는 Money 클래스 내 필드 타입만 변경하면 된다. 필드 추가 상황 등도 마찬가지 일 것이다.

(제약 조건 추가 등이 발생했을 때도 Money 클래스에 단순히 추가하면 됨.)

 

만약, VO 용도의 객체로 생성했다면(원자값 포장이란 행위를 사용하는), 반드시 동등성 검사 및 불변 객체임을 보장해야된다.

이 부분은 아래 VO 부분에서 좀 더 설명하겠다.


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

 

간략히 VO란?

도메인에서 한 개 또는 그 이상의 속성들을 묶어서 특정 값을(의미가 있는) 나태는 객체를 의미한다.

 

VO 용도로 사용되려면 필수적으로 유효성 검사를 비롯해 아래 2가지 특징을 만족해야된다.!

 

1. equals & hashCode 메서드 재정의를 통해 동등성 비교가 가능해야 된다.

즉, 내부 속성 값들이 같으면, 같은 객체임을 보장해야된다. equals 메서드를 재정의 할때는 hashCode도 재정의 해야함을 잊지말자!
(equals, hashCode는 단순한 개념 뿐만 아니라 해시 충돌 등 다양한 지식이 파생되는 부분이 많아서 따로 정리해보겠다.)

 

또한, 원시값 포장만 했다고 동등성을 보장 받을 수 없기 때문에, VO 용도의 객체를 생성했을 때는 반드시 재정의를 통해 동등성을 보장해야된다.

 

2. 수정자가(Setter) 없는 불변 객체임을 보장해야된다.

VO는 도메인 객체의 일부이고, 기본 키로 구분되는 Entity(도메인)과 다르게, VO는 내부 속성값들이 식별자의 역할을 하므로, 항상 불변 객체임을 보장해야된다. (추적이 불가할 수도 있기 때문)

즉, 값의 수정이 생겼을 때는 VO 객체가 항상 새로 생성되도록 해야된다.

 

아래 코드는 앞선 코드를 조금더 VO 스럽게 바꾼 코드이다.(내 생각대로?..ㅎㅎ)

public class ItemPrice {
	private final Money purchasePrice;
	private final Money sellingPrice;
	
	public ItemPrice(int purchasePrice, int sellingPrice){
		this.purchasePrice = new Money(purchasePrice);
		this.sellingPrice = new Money(sellingPrice);
	}
	
	public int calculatePrice(int quantity, PriceKind priceKind){
		if(priceKind.isPurchase()){
			return purchasePrice.calculateTotalPrice(quantity);
		}
		return sellingPrice.calculateTotalPrice(quantity);
	}
	
	@Override
	public boolean equals(Object o) {
		if(this == o)
			return true;
		if(o == null || getClass() != o.getClass())
			return false;
		
		ItemPrice itemPrice = (ItemPrice)o;
		return Objects.equals(purchasePrice, itemPrice.purchasePrice) 
			&& Objects.equals(sellingPrice, itemPrice.sellingPrice);
	}
	
	@Override
	public int hashCode() {
		return Objects.hash(purchasePrice, sellingPrice);
	}
}
@Getter
public class ItemVersion3 {
	private final String name;
	private final String description;
	
	private final ItemPrice itemPrice;
	
	public ItemVersion3(String name, String desc, int purchasePrice, int sellingPrice){
		this.name = name;
		this.description = desc;
		this.itemPrice = new ItemPrice(purchasePrice, sellingPrice);
	}
	
	public int calculatePrice(int quantity, PriceKind priceKind){
		return itemPrice.calculatePrice(quantity, priceKind);
	}
	
}

기존 Item의 가격과 관련된  purchasePrice, sellingPrice 속성을 묶어 아이템 가격이란 VO 객체로 표현해보았다.

 

 

즉, 정리하자면 VO 객체를 사용하는 이유는 아래와 같다.

  • 도메인의 원시값을 포장하면, 도메인이 과도하게 커지는 것을 막을 수 있다.
  • 클래스 별 책임과 역할을 분리시 킬수 있고, 유지보수성에 좋다.
  • 데이터베이스 테이블 관점이 아닌, 객제 지향 관점에서 프로그래밍이 가능하다.

참고 자료