객체지향

값 객체(Value Object)를 사용해야 할 때 – Game 클래스 리팩터링 예시

경딩 2025. 5. 21. 22:47

값 객체(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 은 재사용 가능하며, 다른 클래스에서도 일관되게 사용할 수 있다.

 

참고 자료: 오브젝트 - 설계 원칙편