우테코 첫번째 미션을 진행하며 가장 감명깊게 배웠던 개념은 VO였다.
실제로 관련된 피드백 또한 받았다.
어떤 이점인지, 본인이 생각하는 VO의 성격은 무엇인지 리뷰어가 궁금할 만한 지점을 미리 드러내주면 더 좋을 것 같습니다.
마틴 파울러는 자신의 블로그에 다음과 같이 말하였다.
When programming, I often find
it's useful to represent things as a compound.
- 프로그래밍 할때 사물을 복합물로 표현하는 것이 유용할때가 종종 있다.
If I have two point objects that
both represent the Cartesian coordinates of (2,3),
it makes sense to treat them as equal.
- (2,3) 좌표를 나타내는 두 좌표는 같다고 여기는게 당연하다.
Objects that are equal due to the value of their properties,
in this case their x and y coordinates,
are called value objects.
- 속성 값(이 경우 x 및 y 좌표)으로 인해 동일한 개체를 값 개체라고 합니다.
즉, 값 개체를 복합적인 속성을 가진 객체라고 표현한다.
그럼 VO는 어떤 특성으로 일반 Class 들과 구분될까?
우리는 동일한 속성값을 가지는 두 객체를 같은 객체라고 하고 싶다.
하지만 실제 코드로 비교해보면 두 객체는 다르다 판단한다.
public class Person {
private final String name;
private final int age;
public Person(final String name, final int age) {
this.name = name;
this.age = age;
}
}
@Test
void equals() {
Person person1 = new Person("daon", 20);
Person person2 = new Person("daon", 20);
assertThat(person1 == person2).isFalse();
assertThat(person1.equals(person2)).isFalse();
}
아래 두 테스트는 실패해야하지만 모두 통과한다. 왜 그럴까?
그 전에 우리는 동일성 비교와 동등성 비교의 차이를 알아야 한다.
동일성(==) 비교는 객체가 참조하고 있는 주소값을 확인한다.
두 객체를 print 해보면 쉽게 확인해 볼 수 있다.
person1: $Person@365c30cc
person2: $Person@701fc37a
이를 보고 알 수 있듯이 두 객체가 참조하고 있는 메모리 주소값은 서로 다르다.
따라서 객체가 포함하고 있는 속성값들을 비교하려면 동등성 비교인 equals()
메서드를 재정의 해줘야한다.
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
Person person = (Person) obj;
return this.name.equals(person.name) && this.age == person.age;
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
hashCode() 또한 재정의하는 이유
간단히 얘기하면 hash를 사용하는 Collection(HashSet, HashMap, HashTable) 객체는 논리적으로 같은지 비교할 때
hashCode()
의 리턴값이 같은지 우선 비교하기 때문이다. 이로 인해 equals()와hashCode()
는 항상 같이 재정의 해주자.
속성값 자체가 식별 값인 VO는 값이 바뀌면 다른 값이 되어 추적이 불가능하고 복사될 때 의도치 않은 객체들이 함께 변경되는 문제가 발생할 수 있다.
따라서 VO는 불변 객체로 만들어야한다.
그렇다면 setter 없이 어떻게 설정해야할까?
바로 생성자를 이용하여 값이 한 번만 할당되고 이후로 변경되지 않도록 만들 수 있다.
public class Person {
private final String name;
private final int age;
public Person(final String name, final int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
Person person = (Person) obj;
return this.name.equals(person.name) && this.age == person.age;
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
public class Main {
public static void main(String[] args) {
Person daon = new Person("다온", 10);
Person yeji = new Person("예지", 10);
yeji = new Person("황예지", 20);
}
}
이렇게 하면 VO의 정체성을 지키면서, 의도치 않은 변경을 막을 수 있기 때문에 유지 보수에도 효과적이다.
이로 인해 우리가 외부에 객체를 사용할 때 의도 하지 않는 데이터를 걱정할 필요를 덜 수 있다.
public class Person {
private static final int MINIMUM_NAME_LENGTH = 1;
private final String name;
private final int age;
public Person(final String name, final int age) {
validateNameLength(name);
this.name = name;
this.age = age;
}
private void validateNameLength(String name) {
if (name.length() < MINIMUM_NAME_LENGTH) {
throw new IllegalArgumentException();
}
}
// equals() hashCode() 재정의
}
이는 1번과 연계된 이점이다. 아래 해시맵 구현을 보자.
Map<String, Integer> personInfo1 = new HashMap<>();
Map<Name, Age> personInfo2 = new HashMap<>();
아래 해시맵이 좀 더 검증된 데이터로 이루어 졌음을 보장함을 알 수 있다.
개인적으로 느낀 생각이 다소 담겨있으니 오류가 있다면 마음껏 지적해주기 바란다. VO에 대한 감이 잡혔다면 다음에는 이와 관련된 Record 또한 학습해보길 권장한다.
martinfowler.com-Value Object
Tecoble - VO(Value Ojbect)란 무엇일까?
Tecoble - equals와 hashCode는 왜 같이 재정의해야 할까?
VO랑 불변객체의 개념을 혼동하고 있었는데 잘 정리가 되었어요 👍 VO를 불변객체로 만들어야 하는 군요!
저는 불변객체가 equals와 hashCode를 재정의해야 되는 것으로 착각하다가 이번에 불변 객체를 공부하면서 오개념이었다는 걸 깨달았는데, VO의 개념과 혼동했네요.. 공부하고 갑니다!