값 객체(VO)란?
값 객체는 색상이나 숫자처럼 참조 객체의 속성을 표현하기 위해 사용하는 단순한 객체를 말한다.
참조 객체는 식별성을 이용하여 객체의 동등성을 표현한다.
값 객체는 값이 동일하면 동일한 객체로 간주한다.
값이 동일하면 동일한 객체로 취급한다.
값 비교를 통해 참조 객체의 복잡성을 감소 시킨다.
불변 객체(immutable Object) 로 구현한다.
예시 : 금액
금액은 대표적인 값 객체이다. Money 클래스를 예를 들어 값 객체를 살펴보자.
package value.example;
import java.math.BigDecimal;
import java.util.Objects;
public class Money {
private final BigDecimal amount;
Money(BigDecimal amount) {
this.amount = amount;
}
public static Money wons(long fee) {
return new Money(BigDecimal.valueOf(fee));
}
public Money plus(Money amount) {
return new Money(this.amount.add(amount.amount)); // 상대 변경이 필요하면 새로운 객체 생성 후 반환
}
// 동일성 - 객체의 메모리 주소가 같은 것
// 동등서 - 객체의 값이 같은 것
@Override
public boolean equals(Object o) { // 속성값이 같으면 동일한 객체로 취급
if (o == null || getClass() != o.getClass()) return false;
Money money = (Money) o;
return Objects.equals(amount, money.amount);
}
@Override
public int hashCode() {
return Objects.hashCode(amount);
}
}
값 객체를 사용하면 참조객체의 복잡성을 낮출 수 있다.
값 객체가 참조 객체보다 간단한 이유
Money 가 가변 객체라면?
가변 객체 Money
package value.mutable;
import java.math.BigDecimal;
public class Money {
private BigDecimal amount;
public void plus(Money other) {
this.amount = this.amount.add(other.amount); // 속성에 계산 결과를 다시 대입해서 상태를 변경
}
}
가변 객체 Money 를 사용해서 값을 계산해보자.
import value.mutable.Money;
public class Main {
public static void main(String[] args) {
Money amount1 = Money.wons(1000L);
Money amount2= Money.wons(1000L);
amount1.plus(amount2); // 1000 + 1000 = 2000
amount1.plus(amount2); // 2000 + 1000 = 3000
amount2.plus(amount1); // 1000 + 3000 = 4000
amount1.plus(amount2); // 3000 + 4000 = 7000
System.out.println("amount1 = " + amount1.getAmount());
System.out.println("amount2 = " + amount2.getAmount());
}
}
프로그래밍을 하다 보면 항상 함께 움직이는 변수들이 있다. 이런 경우는 단지 우연이 아니라, 코드 속에 숨겨진 개념이 있다는 강력한 신호이다. 이럴 때 우리가 할 수 있는 가장 좋은 선택 중 하나는 값 객체를 만들어 해당 개념을 명시적으로 드러내는 것이다.
이번 글에서는 자바로 작성된 Game 클래스를 예시로 들어, 어떻게 값 객체를 활용해 코드를 더 단순하고 명확하게 만들 수 있는지를 소개한다.
이렇게 가변 객체로 구현된 참조 객체는 상태변경을 이해하기 위해서 코드를 일일히 따라가봐야 한다.
여기에 별칭 문제(?)까지 겹치면 실행 결과를 예상하는게 더 복잡해진다.
불변 객체 money
package value.immutable;
import java.math.BigDecimal;
import java.util.Objects;
public class Money {
private final BigDecimal amount;
Money(BigDecimal amount) {
this.amount = amount;
}
public static Money wons(long fee) {
return new Money(BigDecimal.valueOf(fee));
}
public Money plus(Money amount) {
this.amount.add(amount.amount);
return new Money(this.amount.add(amount.amount)); // 상대 변경이 필요하면 새로운 객체 생성 후 반환
}
// 동일성 - 객체의 메모리 주소가 같은 것
// 동등서 - 객체의 값이 같은 것
@Override
public boolean equals(Object o) { // 속성값이 같으면 동일한 객체로 취급
if (o == null || getClass() != o.getClass()) return false;
Money money = (Money) o;
return Objects.equals(amount, money.amount);
}
@Override
public int hashCode() {
return Objects.hashCode(amount);
}
}
불변 객체는 상태를 변경할 수 없기 때문에 plus 메서드에서 새로운 객체를 생성해 반환한다.
불변 객체를 이용하여 플러스 연산을 해보자.
불변 객체는 상태를 변경할 수 없기 때문에 객체를 생성할 때 값이 만원과 만원이 그대로 유지된다.
중간에 상태변경은 무시하고 객체를 생성할 떄 상태만 알면된다.
이렇게 불변객체 사용 시 상태변경을 추적하고 이해하는데 드는 시간과 노력을 줄일 수 있다.
import value.immutable.Money;
public class Main2 {
public static void main(String[] args) {
Money amount1 = Money.wons(1000L);
Money amount2= Money.wons(1000L);
amount1.plus(amount2); // 10000 + 10000 = 20000
amount1.plus(amount2); // 10000 + 10000 = 20000
amount2.plus(amount1); // 10000 + 10000 = 20000
amount1.plus(amount2); // 10000 + 10000 = 20000
System.out.println("amount1 = " + amount1.getAmount()); // 값이 바뀌지 않기 때문에 10000 원 - 초기값 그대로
System.out.println("amount2 = " + amount2.getAmount()); // 값이 바뀌지 않기 때문에 10000 원 - 초기값 그대로
}
}
값 객체 사용시 이점
- 관련 있는 변수들을 하나의 단위로 묶어 클래스를 작고 단순하게 만들 수 있다.
- 인스턴스 변수와 파라미터의 개수를 줄여 가독성을 높일 수 있다.
- 도메인 개념을 명확하게 표현할 수 있어, 유지 보수가 쉬워진다.
값 객체는 언제 사용할까?
여러 장소에서 함께 사용되는 변수들이 있다면 값 객체로 추출할 수 있을지 고려하자
예제: Game 클래스 리팩터링
게임 클래스의 플레이어의 위치를 나타내는 x , y 가 함께 사용된다.
roomAt(int x, int y) , isBlocked(int x, int y) 인자로 x , y 를 함께 전달하고 있고
tryMove 메서드안에서도 x 와 y 를 함께 변경하고 있다.
this.x += incX;
this.y += incY;
public class Game {
private int width, height;
private Room[] rooms;
private int x, y;
private void showRoom() {
System.out.println("당신은 [ " + roomAt(x, y).name() + " ] 에 있습니다.");
System.out.println(roomAt(x, y).description());
}
private boolean isBlocked(int x, int y) {
return y < 0 || y >= height || x >= width || roomAt(x, y) == null;
}
private void tryMove(int incX, int incY) {
if (isBlocked(x + incX, y + incY)) {
showRoom();
} else {
this.x += incX;
this.y += incY;
showRoom();
}
}
private Room roomAt(int x, int y) {
return rooms[x + y * width];
}
}
이와 같이 여러 곳에서 변수가 함께 사용된다면 어떤 개념이 숨겨져 있다는 힌트이다.
이 개념을 명시적으로 드러내기 위해서 값 객체를 추가할 수 있다.
x, y 의 어떤 개념이 숨겨져 있는지 유추해보자.
x, y 는 player 의 위치를 나타내기 때문에 숨겨진 개념은 위치라고 할 수 있다.
즉 Position 이 x, y 의 값 객체가 된다.
위치를 명시적으로 드러내기 위해 값 객체 즉 position 을 추가해보자
Step 1 . 값 객체 Position 만들기
값 객체 추가시 주의점!
- 값 객체 추가 시 관련 된 변수 뿐만 아니라 그 변수를 이용하는 로직도 함께 수정해야 한다.
- position 의 값 객체인 경우 함께 사용하는 변수는 x, y 이고 로직은 x 와 y 의 좌표를 이동시키는 부분이다.
- 로직과 필드 모두 수정해주어야한다.
값객체는 한 번 생성되면 상태가 변경되지 않아야하기 때문에 변수들은 final 로 선언하여야 한다.
final 변수는 객체 생성 시 초기화 되어야 한다.
private final int x;
private final int y;
생성자를 추가하고 생성자 안에서 x, y 를 초기화 하자.
- 값 객체처럼 작은 클래스는 생성자 대신 정적 팩토리 메서드를 사용하여 가독성과 사용성을 개선
public static Position of(int x, int y) { // 값 객체처럼 작은 클래스는 생성자 대신 정적 팩토리 메서드를 사용하여 가독성과 사용성을 개선
return new Position(x, y);
}
private Position(int x, int y) { // 생성자
this.x = x;
this.y = y;
}
x 와 y 값을 증가시키는 로직를 Position 클래스에 추가하자.
값 객체는 인스턴스 변수를 수정할 수 없기 때문에 변경된 상태를 가지는 객체를 생성한 후에 반환하도록 구현한다.
public Position shift(int incX, int incY) {
return new Position(x + incX, y + incY); // 새로운 Positon 생성 후 반환
}
따라서 Position 클래스의 shift 메서드는 x, y 에 파라미터로 전달된 값을 더한 후에 새로운 Position 인스턴스를 생성해서 반환한다.
Step 2. Game 클래스 리팩터링
x, y -> position 이라는 값 객체를 사용하여 위치 개념을 명확하게 표현하였다.
public class Game {
private int width, height;
private Room[] rooms;
private Position position;
private void showRoom() {
System.out.println("당신은 [ " + roomAt(position).name() + " ] 에 있습니다.");
System.out.println(roomAt(position).description());
}
private boolean isBlocked(Position position) {
return position.x() < 0 || position.y() >= height || position.x() >= width || roomAt(position) == null;
}
private void tryMove(int incX, int incY) {
if (isBlocked(position.shift(incX, incY))) {
showRoom();
} else {
this.position = position.shift(incX, incY);
showRoom();
}
}
private Room roomAt(Position position) {
return rooms[position.x() + position.y() * width];
}
}
이렇게 함께 사용되는 변수들을 값 객체로 추출하면 인스턴스 변수와 파라미터 갯수를 줄일 수 있다.
x 와 y 의 숨겨져 있던 위치라는 개념을 값 객체 Position 으로 표현하여 게임클래스를 더 단순하고 이해하기 쉽게 수정했다,
리팩터링 결과의 이점
- x,y 라는 로우레벨 필드가 의미있는 도메인 객체로 바뀌었다.
- 메서드 시그니처와 내부로직이 더 명확하고 간결해졌다.
- Position 은 재사용 가능하며, 다른 클래스에서도 일관되게 사용할 수 있다.
참고 자료: 오브젝트 - 설계 원칙편