1. VO(Value Object)란?
VO의 의미를 보면 다음과 같다.
* VO란 도메인에서 한 개 또는 그 이상의 속성들을 묶어서 특정 값을 나타내는 객체를 의미한다.
* 해당 속성들을 primitive 타입이다! (int, boolean, ...)
여기서는 의미만 간단하게 살펴보고, 아래에서 VO의 예시를 살펴보면서 이해해보자.
2. VO, 그래서 왜 쓰는건데?
VO가 필요한 이유는, primitive 타입이 도메인 객체를 모델링하기 위해 충분하지 않기 때문이다.
primitive 타입으로는 도메인에서 의미 있는 값으로 인식하기 힘들다.
지금부터 VO가 필요한 이유를 살펴보자.
🎯 2-1. primitive 타입의 기능들을 객체가 전부 사용하지 않는다.
'사다리 타기 게임'을 예시로 들어보자.
사다리에서 사다리는 ‘너비’와 ‘높이’를 가지고 사다리를 생성한다.
VO를 사용하지 않고 primitive 타입만으로 설계한 Ladder는 다음과 같다.
public class Ladder {
private final int width;
private final int height;
public void Ladder(int width, int height) {
this.width = width;
this.height = height;
}
}
이 설계는 얼핏 보면 잘 설계된 것처럼 보인다!
‘너비’와 ‘높이’가 primitive 타입인 int 형으로 선언되어 있다.
int는 ‘너비’와 ‘높이’가 가지지 않는 속성과 연산을 가지고 있다.
위의 말이 잘 이해가 되지 않을 것이다.
높이를 예시로 해당 질문에 답해보자.
1. 사다리의 높이가 더해지는 기능이 필요한가?
2. 사다리의 높이를 곱하거나 나누는 것이 필요한가?
3. 사다리의 높이가 음수가 되는 것이 가능한가?
대답은 모두 ‘아니요’ 일 것이다.
int 타입은 위 질문들의 기능들을 다 가지고 있는데, 왜 사다리의 ‘너비’, ‘높이’를 int형으로 해야 할까?
위의 예시는 자동차 게임으로 비교해 보면
Car라는 객체에 전진 기능 move, 후진 기능 back, 멈춤 기능 stop을 넣어 놓고,
기능을 사용하기 위해 Car를 쓰는 것이 아니라,
단순히 '자동차'라는 것을 표현하기 위해 Car를 사용하는 것이다.
사다리의 높이도 '정수'이므로 int로 표현했지만,
결국 int의 기능들을 전부 사용하지는 않는다.
이러한 관점에서 도메인의 객체를 나타내기 위해 primitive 타입을 쓰는 나쁜 관습을
primitive obsession이라고 부르기도 한다.
🎯 2-2. 한 곳이 아니라 여러 곳에서 사용될 때 중복 코드 발생
‘너비’와 ‘높이’를 가지는 것이 사다리뿐만이 아니라 직사각형이라는 객체도 가질 수 있다.
이때, 사다리와 직사각형은 ‘너비’와 ‘높이’의 유효성 검사를 진행한 후에 생성되어야 한다.
(ex : 너비와 높이는 음수면 안된다.)
이러한 상황에서, 유효성 검사 코드를 사다리와 직사각형 객체에 중복되게 넣어야 한다.
이렇게 ‘너비’와 ‘높이’를 가지는 모든 객체에 중복되는 코드가 들어갈 것이다.
public class Ladder {
private final int width;
private final int height;
public void Ladder(int width, int height) {
validateWidth(width); // 중복 코드
validateHeight(height); // 중복 코드
this.width = width;
this.height = height;
}
private void validateWidth() {
...
}
private void validateHeight() {
...
}
}
public class Rectangle {
private final int width;
private final int height;
public void Ladder(int width, int height) {
validateWidth(width); // 중복 코드
validateHeight(height); // 중복 코드
this.width = width;
this.height = height;
}
private void validateWidth() {
...
}
private void validateHeight() {
...
}
}
🎯 2-3. 한 곳이 아니라 여러 곳에서 사용될 때 불변을 한 번에 보장할 수 없다.
2-2와 마찬가지로
‘너비’와 ‘높이’를 가지는 것이 사다리뿐만이 아니라 직사각형이라는 객체도 가질 수 있는데,
직사각형의 '너비'와 '높이', 사다리의 '너비'와 '높이'가 불변인 것을 호출하는 쪽에서 어떻게 알 수 있을까?
일일이 Rectangle, Ladder 클래스로 들어가서
int형 width, height에 final 키워드가 붙었는지 확인해야 할 것이다.
✅ VO가 필요한 이유 요약
1. primitive 타입의 기능들을 '값'이 다 사용하지 않는다.
2. '값'이 여러 곳에서 사용되면 정보가 모두 여러 곳에 퍼져 있기 때문에,
유효성 검사나 불변 체크 등을 해당 '값'이 있는 모든 객체에서 진행해야한다.
3. VO 생성 시 제약조건
VO를 사용하기 전에, VO의 생성 시 제약 조건을 알아보자.
VO를 만드려면 아래의 3가지 제약 조건을 지켜야한다.
(3가지 조건을 지킨 값 객체가 VO가 된다.)
1. 불변성 (Immutable)
2. 동등성 (Equality)
3. 자가 유효성 검사 (Self-Validation)
🎯 3-1. 불변성 (Immutable)
VO는 불변해야한다. (불변 객체여야한다.)
Setter와 가변 로직이 없는 불변상태여야한다.
이러한 특성 덕분에, VO를 호출하는 쪽에서는 언제 어디서 호출을 하든 값 변경에 대한 걱정을 할 필요가 없다.
🎯 3-2. 동등성 (Equality)
생성된 여러 VO가 실제 다른 객체이더라도 (객체 주소값이 다 다르더라도)
‘값’이 같다면 동등한 객체로 판단한다.
이러한 예시로, ‘지폐’를 많이 들곤 한다.
지폐에는 고유번호가 존재한다.
만원 지폐가 5장있다고 하면, 해당 5장은 모두 고유번호가 다를 것이다. (객체 주소가 다르다)
하지만, 우리는 5장을 모두 ‘만원’이라는 값으로 같다고 인식하는 것처럼
VO도 객체의 주소가 달라도 ‘값’이 같다면 동등한 객체로 판단해야한다.
👉 따라서 equals & hashCode를 재정의해야한다.
🎯 3-3. 자가 유효성 검사 (Self-Validation)
위에서 원시 타입을 사용했을 때, 사용하는 모든 곳에서 유효성 검사를 진행해야 한다는 문제점이 있었다.
VO를 사용하면, VO 안에서 생성 시에 유효성 검사를 진행한 후에 생성되어야 한다.
이러한 특성 덕분에 VO를 사용하는 쪽에서는 유효성 검사가 보장되어 있으므로 안전하게 사용할 수 있다.
4. VO, 직접 사용해보자
앞에서 ‘너비’와 ‘높이’를 원시 타입으로 사다리 예시를 들었었는데,
이를 VO의 특성을 지켜서 VO로 만들어보자.
public class ShapeProperty {
// 불변성 (Immutable)
private final int width;
private final int height;
public Shape(final int width, final int height) {
// 자가 유효성 검사 (Self-Validation)
validateWidth(width);
validateHeight(height);
this.width = width;
this.height = height;
}
// 동등성 (Equality)
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ShapeProperty that = (ShapeProperty) o;
return width == that.width && height == that.height;
}
@Override
public int hashCode() {
return Objects.hash(width, height);
}
}
원시 타입을 사용했던 사다리(Ladder) 입장에서 VO 사용 시 변화를 살펴보자.
// 원시 타입 사용
public class Ladder {
private final int width;
private final int height;
public void Ladder(int width, int height) {
validateWidth(width);
validateHeight(height);
this.width = width;
this.height = height;
}
}
// VO로 변경
public class Ladder {
private final ShapeProperty shapeProperty;
public void Ladder(final ShapeProperty shapeProperty) {
this.shapeProperty = shapeProperty;
}
}
1. 사다리가 원시 타입 '너비'와 '높이'를 묶어서
원시 타입이 아닌 ShapeProperty라는 값 객체를 가져서,
사다리가 '모양 속성'이라는 네이밍을 가진 객체를 가질 수 있게 됐다.
2. 유효성 검사와 불변 체크를 Ladder가 아닌, 값 객체 'ShapeProperty'에서 수행하기 때문에
Ladder에서는 핵심 비즈니스 로직만을 가질 수 있다.
3. 이후에 Ladder가 아닌, Rectangle이 추가되어 '너비'와 '높이'를 사용하더라도,
원시 타입이 아닌 'ShapeProperty'를 사용하기 때문에
중복으로 유효성 검사, 불변 체크를 하지 않아도 된다.
Reference
원시 타입 대신 VO(Value Object)를 사용하자
'우아한테크코스 5기 > 학습 로그' 카테고리의 다른 글
함수형 인터페이스의 정의 & 사용 예제 (0) | 2023.04.10 |
---|---|
instanceof 사용을 지양하기 - Why? Solution? (1) | 2023.03.19 |
도메인에 처음 접근하기 - Out -> In / In -> Out 접근 방식 (0) | 2023.02.27 |
포장 값을 View로 전달하는 방식으로 무엇을 사용할까? (2) | 2023.02.27 |
[Java] ArrayList, LinkedList를 직접 구현해보며 이해한 것 (0) | 2023.02.26 |