객체지향

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

경딩 2025. 5. 21. 22:47
작은 클래스, 단순한 구조, 더 명확한 의도 표현

들어가며

작고 단순한 클래스를 만들기 위해 값 객체(Value Object) 를 활용할 수 있다.
이번 글에서는 "게임 매출 관리 애플리케이션" 을 예시로,
값 객체를 언제, 왜 사용하는지 그리고 참조 객체(Reference Object)와의 차이점을 소개한다


게임 매출 관리 애플리케이션

요구사항: 게임의 판매현황과 매출을 관리한다.

  • 게임은 판매 가능한 상품이다.
  • 판매자로 등록한 사용자는 시스템에 게임을 등록하고  판매할 수 있다.
  • 애플리케이션은 등록된 게임의 매출 정보를 관리해야한다.
  • 이때 중요한 점은 매출이 각각의 게임별로 관리된다는 점이다.
  • 게임을 하나당 하나의 매출(Sales) 객체가 생성되어야 한다.
package reference;

public class Game {
    private String title;
    private long price;
    private double discountRate;

    public Game(String title, long price, double discountRate) {
        this.title = title;
        this.price = price;
        this.discountRate = discountRate;
    }

    /**
     * 게임 하나당 가격 계산
     * @return
     */
    public long calculateSalePrice() {
        return price - (long)Math.ceil(price* discountRate);
    }
}
package reference;

/**
 * 게임별로 매출을 관리할 Sales 클래스
 */
public class Sales {
    private Game game;
    private int totalQuantity; // 게임의 누적 수량
    private long totalPrice; // 게임의 누적 금액

    public Sales(Game game) {
        this.game = game;
        this.totalQuantity = 0;
        this.totalPrice = 0;
    }

    /**
     * 판매 금액, 판매 수량 업데이트
     * @param qunatity
     */
    public void addSale(int qunatity) {
        totalQuantity+= qunatity;
        totalPrice += game.calculateSalePrice()* qunatity;
    }

    /**
     * 게임의 영업 이익 계산
     * @return
     */
    public long profit() {
        return (long) Math.ceil(totalPrice* 0.2);
    }

    public long totalAmount() {
        return totalPrice;
    }
}

 

테스트 케이스 예시

정상적인 누적 금액과 이익 계산

    @Test
    public void profit() {
        Game game = new Game("Object", 10000, 0.1);

        Sales sales = new Sales(game);
        sales.addSale(3);// 27000
        sales.addSale(2);// 18000

        assertEquals( 45000, sales.totalAmount()); // 45000
        assertEquals(9000, sales.profit()); // 9000

    }

 

  • 누적금액이 45000 원이고, 이윤이 9000 인것을 검증해보자.

 

문제 상황 : 여러 Sales 인스턴스가 존재할 때

    @Test
    public void invalid_profit() {
        Game game = new Game("Object", 10000, 0.1);

        Sales sales = new Sales(game);
        sales.addSale(3); // 27000

        Sales anotherSales = new Sales(game);
        anotherSales.addSale(2); // 18000

        assertEquals(27000, sales.totalAmount());
        assertEquals(5400, sales.profit());

        assertEquals(18000, anotherSales.totalAmount());
        assertEquals(3600, anotherSales.profit());

    }

 

  • 위 예시는 총 5개의 게임이 팔렸지만, Sales 객체가 둘로 나뉘어 이익계산이 부분적이다.
  • 하나의 전체 매출 객체가 아닌, 각기 다른 부분 매출을 관리하게 된다.

 

두 개의 객체에 나눠서 저장된 판매내역이다.

각 Sales 는 전체 매출의 일부만 포함된다.

따라서 둘중에 어떤 경우도 전체 매출이 아니다.

어떤 Sale 객체를 이용하건 영업이익의 일부만 구할 수 있다.

 

영업 이익을 정상적으로 계산하기 위해서는 오직 하나의 Sales 인스턴스만 유지해야한다.

그러면 이 Sales 객체는 모든 판매수량과 판매총액이 저장되어 있어야한다. 

모든 클라이언트가 하나의 Sales 인스턴스를 참조하여야한다,

 

여러 클라이언트가 게임의 매출을 관리해야하는 경우 영업이익을 정상적으로 계산하려면  모든 변수들이 같은 sales 객체를 참조해야한다.


하나의 인스턴스만 참조하면 

sales 객체를 통해 게임을 3개 anotherSales 객체를 통해 2개를 판매할 경우  이 판매내역이 모두 하나의 Sales 객체에 모이게 된다. 따라서 어떤 변수를 이용하여 영업이익을 계산하여도 정확한 영업이익을 계산할 수 있다.

이렇게 클라이언트들이  동일한 객체를 공유해서 오퍼레이션을 실행해야하는 객체를 참조 객체 또는 레퍼런스 오브젝트라고 부른다.

참조 객체(Reference Object)의 특징

  • 상태를 공유하는 객체
  • 가변(mutable) 객체
  • 식별자(ID)를 통해 객체를 구분
  • 예: Sales 객체
Sales sales = new Sales(game);
Sales alias = sales;

alias.addSale(1);
System.out.println(sales.totalAmount()); // alias에서 수정해도 sales에 반영됨
이처럼 하나의 객체에 붙은 여러 이름(alias)을 통해 상태가 공유되고 변경된다.

 

참조 객체는 상품이나 고객처럼 상대적으로 크고 복잡한 도메인 객체를 구현하기 위해 사용된다.

참조 객체는 일반적으로 상태를 변경할 수 있는 가변 객체를 구현한다.

예제에서 Sales 는 누적 수량과 판매총액이 계속해서 변경되기 때문에 가변 객체를 구현한 참조객체라고 할 수 있다.

참고로 참조 객체는 모든 변수가 동일한 객체를 참조하기 때문에 자바의 동등 연산자를 이용해서 비교하면 항상 true 가 반환된다.

 

두개의 변수가 동일한 객체를 가리킬 때 두 변수를 별칭이라 부른다.

별칭을 사용하면 하나의 객체를 여러 개의 다른 이름으로 부를 수 있다.

여기서 Sales 와 AnotherSales 는 같은 Sales 객체에 붙여진 두 개의 서로 다른 이름이다.

별칭은 객체를 새로운 변수에 할당하거나 다른 객체의 메소드의 파라미터로 전달할 때 생성된다.

별칭이 발생하는 상황

다른 변수에 객체를 할당할 때

A b = a;  // b는 a와 같은 객체를 참조

 

메서드 파라미터로 전달할 때

public void update(Sales s) {
    s.addSale(100);
}

update(original);  // original이 변경될 수 있음

 

모든 참조 변수는 동일한 참조 객체를 가리키기 때문하나의 별칭을 통해 변경된 상태는 다른 별칭으로 전파된다.

 

현재 Sales 에 저장된 수량은 5개이고, 금액은 45,000 원이다.

 

이때 sales 변수를 통해 addSale 메소드를 호출해서 수량을 6개로 금액은 54000 원으로 바꾸면 AnotherSales 도 동일한 상태를 보게 된다. 즉 Sales 변수를 이용해서 변경한 상태가 AnotherSales 로 전파된다.

AnotherSales 의 입장에서 생각해보면 아무것도 하지 않았는데 갑자기 상태가  변경된것 처럼 보일 수 있다.

즉 별칭은 장점이자 단점이 될 수 있다.

여러 클라이언트들이 상태를 공유할 수 있다는 관점에서는 장점일 수 있다.

Sales 처럼 하나의 객체가 모든 판매내역을 관리해야한다면 별칭을 이용해서 이 문제를 쉽게 해결 할 수 있다.

하지만 별칭을 잘못 사용하면 원하지 않는 상황에서도 변경이 전파될 수 있다.

예상하지 못한 상황에서 참조하는 객체가 다른 객체에 의해 상태가 변경되면 심각한 버그로 이어질 수 있다.

별칭 문제로 인해 상태를 관리하는 문제가 복잡해진다.

 


참조 객체의 단점

  • 의도치 않은 상태 변경 발생
  • 디버깅이 어려워짐
  • 상태 일관성 유지가 어려워짐

 

이 문제를 해결하기 위해서 상태가 바뀌는 참조 객체 대신 상태가 바뀌지 않는 불변 객체를 사용해야하낟.

이렇게 값이 바뀌지 않는 객체를 값 객체라 부른다.

값 객체를 이용해 참조객체의 복잡성을 낮춰보자


해결 방법: 값 객체(Value Object)

참조 객체의 복잡성을 줄이고자 한다면 값 객체를 사용하자!

 

객체지향 프로그래밍을 하다 보면, 항상 같이 움직이는 변수들을 마주할 때가 있다.

이건 단순히 우연이 아니라, 코드 속에 숨어 있는 개념 일 수 있다.

이럴 때 우리가 할 수 있는 가장 좋은 선택 중 하나는 

-> 값 객체(Value Object) 로 그 개념을 명확하게 드러내는 것이다.


 

값 객체(VO)란? 

값 객체는 색상이나 숫자처럼 참조 객체의 속성을 표현하기 위해 사용하는 단순한 객체를 말한다.

참조 객체는 식별성을 이용하여 객체의 동등성을 표현한다.

값 객체는  동일한 속성을 가지면 동일한 객체로 간주한다. (식별자 없음)

 

값 비교를 통해 참조 객체의 복잡성을 감소 시킨다.

불변 객체(immutable Object) 로 구현한다. ( 불변성(Immutable) )

 

특성 설명
식별자 없음 동일한 속성을 가지면 동일한 객체로 간주
불변성(Immutable) 생성 이후 상태가 바뀌지 않음
값 비교 중심 equals() 와 hashCode() 를 통해 값으로 동일성 판단

 

값 객체는 대표적으로 Money, Address, Color 같은 도메인 속성에 사용된다.

 


금액을 표현하는 값 객체 Money 클래스 예시

불변 객체로 구현한 Money.java

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는 어떨까? (Bad Example)

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());

    }
}

 

함께 움직이는 변수들, 우연이 아니다

프로그래밍을 하다 보면 항상 함께 움직이는 변수들이 있다. 이런 경우는 단지 우연이 아니라, 코드 속에 숨겨진 개념이 있다는 강력한 신호이다. 이럴 때 우리가 할 수 있는 가장 좋은 선택 중 하나는 값 객체(Value Object) 를 만들어 해당 개념을 명시적으로 드러내는 것이다.

 

이렇게 가변 객체로 구현된 참조 객체는 상태변경을 이해하기 위해서 코드를 일일히 따라가봐야 한다.

여기에 별칭 문제까지 겹치면 실행 결과를 예상하는게 더 복잡해진다.

 

불변 객체 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);
    }
}

 

불변 객체는 한번 생성되면 그 상태가 절대 바뀌지 않는 객체이다.

상태를 바꿀 수 없기 때문에, 연산을 수행할 때 기존 객체를 변경하는 대신 새로운 객체를 생성해 반환해야 한다.

 

예를 들어, 금액을 표현하는 불변 객체 Money가 있다고 가정해보자. 두 금액을 더하는 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 원 - 초기값 그대로

    }
}

불변 객체의 장점: 추적이 쉬운 상태

  • 불변 객체는 상태를 변경할 수 없기 때문에 객체를 생성할 때 값이 만원과 만원이 그대로 유지된다.
  • 중간에 상태변경은 무시하고 객체를 생성할 때 상태만 알면된다.
  • 디버깅이나 코드 분석 시, 객체의 상태가 어떻게 바뀌었는지를 일일이 추적할 필요가 없다.

즉, 상태추적과 이해에 드는 시간과 노력을 줄 일 수 있다.

요약

  • 불변 객체는 값을 변경하지 않고 새 객체를 반환한다.
  • 상태가 고정되어 있어 예측 가능성이 높고, 코드 이해와 유지보수가 쉬워진다.
  • 동시성 문제에도 강하다, 여러 스레드에서 같은 불변 객체를 안전하게 공유할 수 있다.

두개의 별칭이  client1 과 client2 가 동일한 Money 객체를 참조하고 있을 때 client1 이 money 를 2000원으로 변경한다고 가정해보자.

 

money 가변 객체일 경우

client1 이 변경한 money 를 client2 도 동일하게 참조하게 된다.

즉 client2 도  동일하게 2000원인 금액을 보게된다.

client2 가 부수효과를 예상하지 못했다면 이렇게 갑작스러운 상태변경은 버그로 이어질 수 있다.

 

 

money 불변 객체일 경우

 

 

 

머니는 불변객체이기 때문에 내부 상태를 직접 변경하는 대신 새로운 객체를 생성한다.

따라서 client1 는 새로운 객체를 참조하기 때문에 클라이언트2로 변경이 전파되지 않는다.

클라이언트 2는 여전히 1000원인 머니 객체를 바라보고 있지때문에 변경으로 인한 부수효과가 발생하지 않는다.

 

즉 불변 객체로 구현한 값 객체는 상태를 추적할 필요가 없고, 부수 효과가 발생하지 않기  때문에  안심하고 사용할 수 있다.

따라서 값 객체를 참조 객체와  섞어서 사용하면 참조 객체가 가지는 복잡성을 완화시킬 수 있다.

 

  • 참조 객체는 두 객체의 동등성을 확인하고 객체 식별자를 비교한다.
  • 참조 객체는 상태 변경이 가능한 가변 객체로 구현한다.
  • 참조 객체는 부수효과로 인해 별칭 문제가 발생한다.

불변 객체의 장점

  • 값 객체는 두 객체의 동일성을 확인하고 속성을 비교한다.
  • 값 객체는 상태를 변경할 수 없는 불변 객체로 구현한다.
  • 값 객체는 별칭 문제를 방지할 수 있다.
  • 상태 변경이 없으므로 부수효과(side effect)가 없다.

값 객체의 가치

참조 객체 안에 숨겨진 모호한  개념을 명시적으로 드러내서 복잡성을 감소하기 위해 사용

참조 객체 안의 복잡한 로직이나 중복 코드를 값 객체로 이동

 

1.암시적인 개념을 명시적으로 표현하기 위해 사용하는 경우

long 타입 안에 숨겨져 있는 금액 개념

Sales 클래스에는 누적 금액을 저장하는 totalPrice 변수와 와 Game의 클래스에는 정가를 저장하는 price 변수가 있다.

두 변수는  Long 타입으로 선언되었지만 실제로는 금액이라는 개념을 나타낸다.

  

이 코드를 읽는 개발자는 두 변수를 볼 때 마다 머릿속에서 Long 타입을 금액이라는 개념으로 변환해야한다.

덧샘 뺄셈 연산도 마찬가지로 단순히 Long 타입 값을 계산하는 것이 아니라 금액을  계산한다는 것을 의미한다.

 

 

 

코드에 표현된 어떤 개념을 머릿속에서 다른 개념으로 해석해야 한다면 그 개념을 코드로 명확하게 표현하는것이 낫다.

금액이라는 개념을 Money  값객체로 구현하면 개념을 명확하게 표현할 수 있다.

 

금액이라는 Money 값 객체로 구현하면 개념을 명확하게 표현할 수 있다.

 

amount 를  Money 클래스에 선언 후 plus, minus, times 메서드를 추가한다.

이제 Long 타입을  Money 타입으로 변경 후  연산자를 Money 의 메서드로 변환한다.

 

이제 금액이라는 개념이 코드안에 명확히 표현되었다.

더 이상 코드를 읽는 개발자가 머릿속에서 long 타입을 금액이라는 개념으로 변환할 필요가 없다.

금액이라는 개념을 명확히 표현하기 때문에 이해하기 쉬워졌다.

 

2. 값객체는 중복을 제거하기 위해서도 사용된다.

여러 참조 객체 사이에 값을 처리하는 간단한 로직이 정리되어 있다면, 이 로직을 값 객체를 통해서 중복을 제거할 수 있다.

 

Sales 와 Game 에는 금액 올림 로직이 중복되어 있다.

만약 금액 올림 규칙이 바뀐다면 여러군데에서 수정을 해야한다.

이 로직은 값 객체에 Money 에 옮겨서 중복을 제거할 수 있다.

 

 

중복을 피하자: DRY 원칙

프로그래밍에서 중복된 코드가 있어서는 안 된다는 원칙을 DRY(Don’t Repeat Yourself) 원칙이라고 부른다.

 

여기서 중복코드는 단순히 모양이 같은 코드가 아닌 요구사항 변경시 함께 수정되는 코드를 의미한다.

반대로, 모양이 동일하더라도 함께 수정되지 않는다면 중복코드로 간주하지 않는다.

즉 DRY 원칙은 "유지보수 시 함께 수정되는 코드" 를 하나로 통합하자는 철학에 가깝다.

 


값 객체는 합성(Composition) 관계로 연결한다

도메인을 설계할 때, 값 객체(Value Object)는 일반적으로 합성(Composition) 관계를 통해 다른 객체에 포함된다.

합성 관계란, 포함된 객체의 생명 주기(Lifecycle)가 포함하는 객체에 종속된다는 것을 의미합니다.
즉, 값 객체는 자신을 포함하고 있는 객체가 소멸되면 함께 소멸됩니다.

 

예를 들어, Order라는 엔티티 안에 ShippingAddress라는 값 객체가 있다면, 주문이 삭제되면 배송지도 함께 삭제되는 식이다,

예제에서 Sales 객체에 포함된 Money 객체는 Sales  객체가 제거될 때 함께 제거된다.

 

예를 들어 Sales라는 객체 안에 Money라는 값 객체가 포함되어 있다고 가정해보자.

이 관계는 UML(클래스 다이어그램)에서 채워진 마름모(◆)로 표현된다.

이 경우, Money는 Sales에 포함된 값 객체이며, Sales 객체가 삭제되면 Money 객체도 함께 사라진다.
즉, 포함된 객체의 생명주기가 포함하는 객체에 종속되는 구조이다.

  • Sales가 주체이고,
  • Money는 그 내부에서 의미를 갖는 부속 값 객체이다.

UML 다이어그램에서 이 관계는 Sales 쪽에 채워진 마름모(◆)로 표시되며,
Money 쪽은 일반 선으로 연결되어 합성(Composition) 관계임을 나타낸다.

 

 

값 객체는 단순한 값을 표현하기 때문에 독립적인 클래스로 표현하지 않고, 다른 객체의 속성으로 표현하기도 한다.

객체의 속성으로 표현하면 생명주기가 종속된다는 사실을 직관적으로 표현할 수 있는 장점이 있다.


값 객체 설계

일반적인 하향식 설계 방식이 아닌 리팩터링을 통해 식별

코드 리팩터링 중에 복잡하거나 중복된 로직을 간단한 개념을 값 객체로 추출

 

 

값 객체 가이드

여러 장소에서 함께 사용되는 변수들이 있다면 값 객체로 추출 할 수 있을지 고민하라.

 


 

 

값 객체 사용시 이점

  • 관련 있는 변수들을 하나의 단위로 묶어 클래스를 작고 단순하게 만들 수 있다. 
  • 인스턴스 변수와 파라미터의 개수를 줄여 가독성을 높일 수 있다.
  • 도메인 개념을 명확하게 표현할 수 있어, 유지 보수가 쉬워진다.

값 객체는 언제 사용할까?

 

여러 장소에서 함께 사용되는 변수들이 있다면 값 객체로 추출할 수 있을지 고려하자


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

 

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