[Java] VO(Value Object)에 대해 알아보자

Daon (HyeongIk Jo)·2024년 3월 3일
9

개글스

목록 보기
4/8
post-thumbnail

우테코 첫번째 미션을 진행하며 가장 감명깊게 배웠던 개념은 VO였다.
실제로 관련된 피드백 또한 받았다.

어떤 이점인지, 본인이 생각하는 VO의 성격은 무엇인지 리뷰어가 궁금할 만한 지점을 미리 드러내주면 더 좋을 것 같습니다.

VO(Value Object)란?

마틴 파울러는 자신의 블로그에 다음과 같이 말하였다.

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 들과 구분될까?

특징

1. equals & hashCode 메서드를 재정의 해야한다. (동등성)

우리는 동일한 속성값을 가지는 두 객체를 같은 객체라고 하고 싶다.
하지만 실제 코드로 비교해보면 두 객체는 다르다 판단한다.

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()는 항상 같이 재정의 해주자.

2. 수정자(Setter)가 없는 불변 객체여야 한다. (불변성)

속성값 자체가 식별 값인 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의 정체성을 지키면서, 의도치 않은 변경을 막을 수 있기 때문에 유지 보수에도 효과적이다.

어떤 이점이 있을까?

1. 우선 객체를 생성할 때 객체 안에서 검증을 추가할 수 있다.

이로 인해 우리가 외부에 객체를 사용할 때 의도 하지 않는 데이터를 걱정할 필요를 덜 수 있다.

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() 재정의
}

2. Collection 사용시 제네릭에 VO를 명시하여 의도한 데이터만 접근할 수 있다.

이는 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는 왜 같이 재정의해야 할까?

profile
To be a Backend Developer

6개의 댓글

comment-user-thumbnail
2024년 3월 4일

VO랑 불변객체의 개념을 혼동하고 있었는데 잘 정리가 되었어요 👍 VO를 불변객체로 만들어야 하는 군요!

저는 불변객체가 equals와 hashCode를 재정의해야 되는 것으로 착각하다가 이번에 불변 객체를 공부하면서 오개념이었다는 걸 깨달았는데, VO의 개념과 혼동했네요.. 공부하고 갑니다!

답글 달기
comment-user-thumbnail
2024년 3월 4일

글 잘 읽었습니다. 만약 validate 로직이 없다면 오히려 record 객체로 제작하는게 더 효율적일 수도 있겠네요.

답글 달기
comment-user-thumbnail
2024년 3월 4일

이점 2번에서 장점이 확 드러나는 것 같아요 ㅎㅎ 가독성도 좋고 검증된 데이터라는게 잘 보이네요!

답글 달기
comment-user-thumbnail
2024년 3월 4일

VO를 통해 원시값을 의미있는 단어로 바꿔서 말하고자하는걸 명확히 할 수있겠네요!ㅎㅎ VO에 대한 내용 잘보고갑니다~

답글 달기
comment-user-thumbnail
2024년 3월 4일

마지막 예시 코드에서 가독성이 높아진다는 장점도 보이는 거 같네요 VO에 대해 잘 이해됐어요 😊

답글 달기
comment-user-thumbnail
2024년 3월 6일

글 잘 봤습니다👍 Record도 언급을 해주셨는데 hashCode와 equals를 항상 정의해준다는 측면에서, VO를 record로 만드는 데 이점이 있을 것 같네요! 단점은 없을까요?🤔 VO와 DTO는 또 어떤 공통점과 차이점이 있을지도 궁금해지구요! 다음 포스팅도 기대하겠습니다~

답글 달기

관련 채용 정보