PPAK

[Java/Spring] VO(Value Object) 란? 본문

jvm/java

[Java/Spring] VO(Value Object) 란?

PPakSang 2022. 6. 26. 16:50

도메인을 설계하다보면 Value Object 를 자주 접하게 된다.

 

값 객체? 값 타입?? 알쏭달쏭한 VO 에 대해서 알아보자

 

우선 VO 는 아래의 두 가지 아이디어에 기반한다고 생각한다.

 

1. 연관있는 데이터의 집합이 분명 존재한다.

2. 그 데이터의 집합은 그 자체로만 식별이 가능하고, 어느 한 속성값이 수정된다면 더 이상 이전과 같은 데이터라고 할 수 없다.

 

아래는 직접 작성한 VO 이다

class Reference {
    int age;
    String course;
    String job;

    public Reference(int age, String course, String job) {
        this.age = age;
        this.course = course;
        this.job = job;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || this.getClass() != obj.getClass()) return false;
        final Reference r = (Reference) obj;
        return (this.age == r.age &&
                this.course.equals(r.course) &&
                this.job.equals(r.job));
    }

    @Override
    public int hashCode() {
        return Objects.hash(this.age, this.course, this.job);
    }
}

위의 작성한 코드에서 VO 의 첫 번째 특징을 확인할 수 있다.

1. VO 는 equals(), hashCode() 를 의미에 맞게 재작성 해야한다.

잠깐 Database Entity 에 대해서 살펴본다면, Entity 라고 하는 것은 pk(primary key) 를 통해 테이블 상에서 유일하게 식별이 되는 데이터 집합이다.

 

이와 다르게 VO 는 따로 pk 를 두지 않고, 속성값을 통해서만 데이터를 식별할 수 있는 데이터 집합을 의미한다고 볼 수 있다.

 

우리는 이미 자바에서 VO 를 사용한 적이 있는데, 대표적으로 Integer, String 을 예로 들 수 있다.

String s1 = "sample";
String s2 = "sample";

Integer i1 = 10;
Integer i2 = 10;

System.out.println(s1.equals(s2));
System.out.println(i1.equals(i2));

위 코드의 결과는 true, true 로 나오는데, 이를 통해 String 과 Integer 는 값을 통해서 동등성을 비교한다는 사실을 확인할 수 있다

class Sample {
    String s;

    Sample(String s) {
        this.s = s;
    }
}

Sample s1 = new Sample("sample");
Sample s2 = new Sample("sample");

System.out.println(s1.equals(s2));

위의 코드의 결과는 false 이다.

VO 가 아닌 객체는 위와 같이 내부에 포함한 속성값이 같더라도 동등하지 않다라고 판단한다.

 

또한 VO 는 equals() 외에도 hashCode() 에 대해서도 동등성을 보장한다

hashCode() 의 의미가 메모리상에서 객체를 식별할 값을 의미하기 때문에(실제로 컬렉션 객체를 사용할 때, 이 hashCode() 값을 통해 동등성을 비교한다) 당연히 동등한 속성값을 가지는 VO 에 대해서는 같은 값을 반환하는 것이 옳은 설계일 것이다.

 

다시 위의 작성한 Reference 코드를 살펴보면, equals(), hashCode() 에 대해서 동등성을 부여하기 위해서, VO 와 동등성을 비교할 VO 객체의 모든 속성값을 비교한다는 사실을 확인할 수 있다.

 

여기까지 VO 는 속성값이 같은 객체에 대해서 동등하다고 판단한다는 것을 확인했다.

즉, VO 의 속성값은 Entity 의 pk 와 같이 데이터 집합을 식별할 수 있는 데이터라는 것을 알 수 있다.

 

그렇다면, VO 의 속성값이 변경된다면 여전히 이전이랑 동일한 객체라고 말할 수 있는가?

다시 말해 Entity 의 pk 값이 임의로 변경되었을 때, 그 Entity 를 같은 데이터로 보는가?

 

여기서 VO 의 두 번째 특징을 확인할 수 있다.

2. VO 의 속성값은 불변(Immutable) 하다 (= setter 가 존재하지 않는다)

VO 의 속성값이 변한다는 것은 Entity 의 pk 값이 임의로 변경되는 것과 같은 의미다 (이런일이 있으면 안된다)

VO 는 속성값 그 자체가 식별값으로 동작하기 때문에 한번 생성되고 나서 변경되면 안된다.

따라서 생성 이후에 값을 수정하는 setter 와 같은 메소드가 존재해서는 안된다.

 

그렇다면 VO 를 변경하고 싶을 때는 어떻게 하면 좋을까?

예를들어, 위의 Reference Class 에서 (30살, "상급자코스", "요리사") 에서 (30살, "중급자코스", "요리사") 로 나의 Reference 를 변경하고 싶을 때 이야기이다.

 

정답은 새로운 VO 인스턴스를 생성하는 것이다.

 

의미상으로도 두 가지 Reference 는 속성값이 다르기 때문에 서로 다른 객체가 맞고, VO 는 불변성을 토대로 하기 때문에 새로운 객체를 생성하는 방법이 가장 적절하다.

 

3. 의문

언뜻 보기에 VO 는 생성 후 변경 불가능한 데이터의 집합으로 보인다.

그러면 이런 의문이 들 수 있다. (물론 너무 넘겨짚는 이야기 일 수도 있다)

 

VO 로 묶지 않고, 각각의 속성값을 따로 선언하면 되지않나? (해당 속성값에 대한 setter 는 없애고, equals() 와 hashCode() 는 해당 속성값을 비교할 때 적절히 구현하면 되고... 등등)

 

우선 답을 해보자면

 

1. VO 로 연관있는 데이터의 집합을 만드는 것에서 부터 의미를 가진다.

VO 를 생성함으로써 각각의 속성값과, VO 와 연관된 class 간의 결합도를 낮추고, Entity 의 속성값이 불필요하게 커지는 것을 방지할 수 있다.

 

2. VO 속성값에 대한 제약조건을 만들 수 있다

가령 Reference Class 같은 경우에는 나이가 20세 미만인 정보는 담을 수 없는다던지 등의 의도치 않은 값이 생성되는 것을 사전에 방지할 수 있다. 즉 새로운 값 타입의 데이터 집합을 정의할 수 있다는 의미이다.

 

이는 일급 컬렉션을 생성하는 이유 중 하나와 같은데, 속성값에 제한을 둔 Class 를 정의함으로써 완전 새로운 타입의 객체를 생성하거나 기존의 검증과 제약이 필요했던 클래스의 내부에서 해당 로직을 수행할 수 있다는 장점과 맥락을 같이한다.

 

VO 는 불변한 데이터 집합으로 속성값을 통한 동등성을 비교하고, 속성값에 대한 검증과 같은 제약조건을 걸 수 있다.

 

잘못된 정보가 있다면 댓글로 알려주시면 감사하겠습니다!!

Comments