본문 바로가기
JAVA

[Java] VO(Value Object)란?

by solar_youn 2022. 3. 15.

VO(Value Object)란 영어 그대로 값 그 자체를 나타내는 객체를 의미합니다. 값 그 자체를 나타낸다는 것은 무슨 의미일까요? 이를 설명하기 위해 아래의 예시를 통해 먼저 알아보도록 하겠습니다.

Value Objects Like a Pro를 작성한 Nicolo는 자신의 블로그에서 어떤 웹사이트에서 아래와 같은 질문을 보고 웃으며 창을 껐다고 합니다.

사람의 나이를 표현하기 위해 어떤 변수 타입을 사용해야 할까요?
[ ] Integer
[ ] Boolean
[ ] String

 

여러분들은 어떤 선택을 하셨나요? 해당 블로그의 저자는 셋 다 정답이 아니다 라고 합니다. 사람의 나이를 표현하는데 Integer를 사용하는 것은 적합하지 않으며 Age 타입을 사용해야 합니다. 그 이유는 다음과 같습니다.

  • 나이를 더하거나 뺄 수 있을까요? - 어쩌면 가능할 수도 있습니다.
  • 나이를 곱하거나 나눌 수 있을까요? - 그럴 수 없습니다.
  • 나이가 음수일 수 있을까요? - 아마도 불가능합니다.

즉, 나이는 Integer 일 수 없고 그렇게 표현하면 안됩니다. 객체를 primitive(원시값)로 표현하는 것은 원시값에 대한 집착이며 이를 객체에 사용해서는 안됩니다. Integer 로 나이를 표현한다면 객체를 생성한 시점에 해당 나이로 표현이 가능한 값인지 어떻게 보장할까요? 이는 할당이 일어나기 전에 모든 곳에서 명시적으로 확인해야 합니다. 그렇다면 한번 할당이 된 Integer 값이 이후에 변경되지 않을 것임을 어떻게 나타낼까요? 언어가 불변을 허용하는게 아니라면 선언 시에 처리해야 합니다.

이렇게 원시 타입을 이용해 도메인 객체를 모델링하는 것이 좋은 방법이 아닙니다. 그렇다면 저희는 나이라는 값을 표현하기 위해 무엇을 사용해야 할까요? 이때 사용하는 것이 값 객체, Value Object입니다.

 

지금부터는 VO의 특성에 대해 알아보도록 하겠습니다.

Value Object의 특성

1. Immutability (불변한)

Value Object는 불변성을 지녀야 합니다. 생성자를 통해 한 번 생성되면 이후에는 내부의 값을 변경할 수 없어야 합니다. 즉, VO 객체는 사용 도중에 값이 변경될 수 없으며 이를 보장하기 위해서는 다음과 같은 작업이 필요합니다.

  • Setter를 허용하지 않는다.
  • GC에 의해 폐기될 때까지 불변해야 한다.

불변으로 얻는 장점은 다음과 같습니다.

  • Hassle-free Sharing(번거롭지 않은 공유) - VO는 사용 도중에 값이 변경되지 않으므로 참조로 공유가 가능하며 side effect를 피하고, 동시에 코드의 복장성과 부하를 감소시킵니다. 이는 멀티쓰레드 환경에서 더욱 빛을 바랍니다.
  • Improved Semantics(향상된 의미) - 명확한 이름과 동작을 가질 수 있게 됩니다. 이를 위해서는 VO의 초기 클래스에는 생성자와 private 인스턴스 변수만 있어야 합니다. 이렇게 VO의 정확한 사용 사례에 따라서 메서드를 추가하게 되어 무의미한 인터페이스 생성을 피하고 의미 있는 이름과 동작을 가지게 됩니다.
    1. 새 인스턴스를 만들 때는 생성자 또는 static 메서드만을 사용한다.
    2. 현재 VO를 통해 새로운 VO를 생성한다.
    3. 내부의 데이터를 추출해 다른 타입으로 변환한다.
  • 아래는 VO를 조작하는 방법입니다.
public final class Money {

    private final double amount;

    // 정적 메소드를 활용해 새로운 인스턴스 생성
    public static Money zero() {
        return new Money(0);
    }

    // 생성자를 활용해 새로운 인스턴스 생성
    public Money(double amount) {
        this.amount = amount;
    }

    // 두 Money를 더해서 새로운 인스턴스 생성
    public Money add(Money anotherMoney) {
        return new Money(this.amount + anotherMoney.amount);
    }

    // 내부의 데이터를 추출해 String으로 변환
    public String toString() {
        return String.format("amount = %f", amount);
    }
}

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는 다른 객체이지만 결국 동일한 내부 값을 가지고 있으므로 동등하다고 판단하는 것이 Value Object의 값 동등성입니다. 이때, 같은 값을 가지고 있는 지를 판단하는 메서드를 만들거나 equals()와 hashCode() 함수를 재정의해야 합니다.

동일성(Identity) vs 동등성(Equality)

동일성 비교는 참조값을 비교하기 때문에 속성과 타입이 같아도 참조값이 다르면 다른 객체로 구분한다. 반면 동등성 비교는 equals() 함수를 통해 객체의 속성을 비교한다. 동일성 비교는 객체의 주소값이 같은지를 비교하는 == 연산자로 가능하며 동등성 비교는 Object 의 equals() 메서드로 비교가 가능하다.

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

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

 

다음은 로또 번호에 대한 자가 유효성 검사입니다.

public class LottoNumber {
    
    private final int number;
    
    public LottoNumber(int number) {
        this.number = number;
        validateNumberRange(number);
    }
	
    private validateNumberRange(int number) {
        if (number < 1 || number > 45) {
            throw new IllegalArgumentException("번호는 1부터 45 사이여야 합니다.");
        }
    }
    
    ...
}

결론

지금까지 Value Object란 무엇이고 어떻게 만드는 지에 대해 이야기 해봤습니다.

이제부터는 도메인을 설계할때, 원시 타입 값을 사용하지 않고 Value Object로 포장하여 객체 간의 역할을 나눕시다!

댓글