
최근 Java로 개발을 하는 사람이라면 value object에 대해서 한 번쯤은 들어봤을 것이라고 생각한다.
나 역시도 그랬지만 최근에 머리 속에 흩어져있던 개념을 정리해보았다.
이 글을 통해서 VO에 대해서 공부한 것 또 내가 느낀 것을 정리해보려고 한다!
먼저 VO(Value Object) 가 무엇인지 알아보자.
명칭에서 드러난 의미 그대로 하나의 값을 나타내기 위해서 사용되는 객체이다.
왜 VO?
도메인을 설계하다보면 연관된 데이터를 묶어 하나로 관리하고 싶은 생각이 들 때가 있다.
간단한 예로 year, month, day 를 별개의 데이터로 관리하는 것보다 세 데이터를 포함하는 Date 라는 타입을 정의하여 관리하는 것이 훨씬 편하고 객체지향에 더 적합한 설계라고 볼 수 있을 것이다.
이때 사용하는 것이 바로 VO이다.
(과거에 VO에 대한 개념이 없었더라도 이미 사용하고 있었을 확률이 높다.)
하지만, VO를 사용할 때는 몇가지 주의해야 할 점이 있다. (중요)
무심코 사용하다가는 정말 잡기 어려운 버그가 발생할 수 있다.
이는 VO의 가장 큰 특징이자 반드시 지켜져야 하는 부분인데 불변 객체로 사용되어야 하는 이유는 다음과 같다.
💡 primitive type은 모두 소문자로 이루어진 타입 (ex. int, double..)이라고 생각하면 쉽다.
예를 들어 다음과 같은 상황을 가정해보자.
(Date는 year, month, day를 Person은 birthday를 필드로 가지고 getter, setter가 열려있다고 가정)
Date date = new Date(2023, 7, 17); //year, month, day
Person person1 = new Person();
Person person2 = new Person();
person1.setBirthday(date);
person2.setBirthday(date);
public void setBirthday(Date date) {
this.birthday = date;
}
date라는 VO를 생성해서 setter를 통해 person1, person2의 birthday 필드에 대입했다.
사용자가 정의한 Date type은 당연히 reference type이므로 위와 같은 형태가 되는 것이다.
참고) call by value, call by reference에 대한 개념이 부족한 분들은 아래 링크를 참고하시면 좋을 것 같습니다.
https://bcp0109.tistory.com/360
여기서 문제는 VO의 값을 변경하려고 할 때 발생한다.
person1.getBirthday.setMonth(10);
위의 코드를 실행하여 앞서 대입한 person1의 birthday 필드 값을 변경하려고 할 때, 결과는 어떻게 될까?
person1, person2의 birthday 필드가 모두 하나의 인스턴스 값을 공유하므로 person1, person2의 birthday 필드가 모두 수정되는 일이 발생한다.
copy 메소드 등을 만들어 VO를 대입할 때마다 값을 복사 해서 사용하는 방법
VO를 불변객체(setter를 만들지 않으면 됨)로 만들어 변경이 일어나지 않게 하는 방법
1, 2 모두 위에서 설명한 버그는 예방할 수 있지만
VO를 불변객체로 만들어 같은 값을 가리킬 때는 인스턴스를 공유하고 어느 하나의 값을 수정하고 싶을 때 새로 객체를 생성하여 할당하는 방법이 메모리 측면에서 더 낫고 편리한 것 같다.
equals(), hashcode()를 재정의해야 함을 설명하기 위해서는 동등성, 동일성의 개념이 사용된다.
동일성은 두 객체가 완전히 같은지를 나타내는 것이고 동등성은 두 객체가 같은 정보를 갖고 있는지 를 나타내는 단어이다.
(동일하면 동등하지만 동등하다고 동일한 것은 아니다.)
결론부터 말하자면, VO는 단순히 값을 나타내기 위해서 사용되는 객체이다.
따라서, 동일성이 아닌 동등성을 비교해야 한다.
모든 객체가 상속받고 있는 Object 객체의 equals() 메소드를 살펴보자.
public boolean equals(Object obj) {
return (this == obj);
}
== 연산자를 통해 두 대상의 주소값을 비교하고 있다. 즉, 동일성 비교를 하고 있다.
하지만, 우리가 VO를 사용하는 이유는 값을 나타내기 위해서이므로 두 객체가 있을 때, 설령 다른 객체(메모리 주소값이 다른 객체)라고 하더라도 논리적으로 같은 성질을 나타낸다면 같은 객체라고 판단해도 무방하다.
(사실은 그렇게 해야합니다.)
따라서, 동일성 비교가 아닌 동등성 비교를 위해서 equals() 함수를 용도에 맞게 재정의하는 것이다.
친구와 포커게임을 하는 상상을 해봅시다. 서로 다른 포커 덱에서 각각 하나의 카드를 선택하고, 나와 친구가 같은 카드를 선택했는지 알아야 하는 상황입니다. 여러분은 어떻게 할건가요?
아마도 여러분은 친구의 카드를 가져와 자신의 카드와 같은 숫자인지, 같은 그림인지 확인할 것입니다. 즉, 동일한 속성을 가지고 있는지 여부를 확인할 것입니다.
다시, 지금 여러분이 앞서 확인 한 카드를 친구와 교환한다고 상상해봅시다. 친구 카드와 여러분의 카드가 달라진 것이 있나요?
달라진 것은 없습니다. 여러분은 여전히 같은 카드를 쥐고 있습니다. 두 카드가 같은 속성을 가지고 있기 때문에, 그 두 카드는 구별할 수 없게 되는 것입니다.
결국 앞의 두 카드는 Value Object라고 말할 수 있습니다.
[출처] https://velog.io/@livenow/Java-VOValue-Object%EB%9E%80
Object의 equals() 함수 위에 API Note에 주목해보자.
일반적으로 이 메서드가 재정의될 때마다 hashCode 메서드를 재정의해야 합니다. hashCode 메서드에 대한 일반적인 규약을 유지하기 위해서는 동일한 객체가 동일한 해시 코드를 가져야 합니다.
여기서 말하는 일반적인 규약은 두 객체가 equals() 메서드를 통해 동등하다면, hashCode() 메서드를 통해 얻은 해시코드도 동일해야한다는 것을 말합니다.
하지만, equals()를 재정의한 후 hashcode()를 재정의 하지 않으면 equals()에서 정의한 같은 객체라도 해시코드 값이 다를 수 있다.
이렇게 되면, HashMap이나 HashSet 같은 해시 기반의 컬렉션에서 예상치 못한 동작을 일으킬 수 있다고 한다.
저도 개발자인데 같이 교류 많이 해봐요 ㅎㅎ! 서로 화이팅합시다!