본문으로 바로가기
dobidugi
목록으로
강의

값 객체(Value Object)

8분
목차

참조 객체의 속성을 표현하는 단순·불변 객체인 값 객체의 개념과, 이를 활용해 클래스의 복잡성을 낮추고 코드를 명확하게 만드는 리팩토링 방법.

값 객체란

📝 NOTE 정의 값 객체(Value Object) 는 색상·숫자·금액처럼 참조 객체의 속성을 표현하기 위해 사용하는 단순한 객체다. 값이 동일하면 동일한 객체로 간주한다.

값 객체의 3가지 구현 규칙

  1. 불변 객체로 구현 — 인스턴스 변수를 final로 선언, 생성자에서만 초기화
  2. 상태 변경 시 새 객체 반환 — 직접 수정하지 않고 새 상태의 객체를 생성해 반환
  3. equals 오버라이딩 — 모든 속성을 비교해 동등성 판단
class Money {
    private final long amount;   // final → 생성 후 변경 불가

    Money(long amount) { this.amount = amount; }

    // plus는 amount를 변경하지 않고 새 Money를 생성해 반환
    Money plus(Money other) {
        return new Money(this.amount + other.amount);
    }

    @Override
    public boolean equals(Object o) { /* 모든 속성 비교 */ }
}

값 객체가 참조 객체보다 단순한 이유

같은 연산을 가변 객체와 불변 객체로 각각 구현해 비교한다.

1) 상태 추적 문제

2) 별칭(Aliasing) 문제

📌 IMPORTANT 두 별칭(Client1, Client2)이 동일한 Money 객체를 참조하는 상황:

  • 가변 객체: Client1이 값을 변경하면 Client2도 같은 변경을 봄 → 예상 못 한 부수 효과 → 버그
  • 불변 객체: 변경 시 새 객체를 생성하므로 Client1만 새 객체를 참조, Client2로 변경이 전파되지 않음 → 안전

💡 TIP 정리

구분참조 객체값 객체
판단 기준동일성(식별자 비교)동등성(속성 비교)
가변성가변 객체불변 객체
별칭 문제발생함방지됨

→ 값 객체를 참조 객체와 섞어 쓰면 참조 객체의 복잡성을 완화할 수 있다.

값 객체의 2가지 용도

강사는 값 객체를 두 목적으로 사용한다:

  1. 암시적 개념을 명시적으로 표현
  2. 복잡한 로직·중복 코드를 이동시키는 장소

용도 1: 암시적 개념의 명시적 표현

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

용도 2: 중복 제거 (DRY)

📝 NOTE DRY 원칙 모든 지식 조각은 시스템 안에서 하나의 모호하지 않고 의미 있는 표현을 가져야 한다. (중복 개념·중복 코드가 존재해서는 안 된다)

📌 IMPORTANT 중복 코드 = 요구사항이 변경될 때 함께 수정되는 코드. 모양이 같아도 함께 수정되지 않으면 중복 코드가 아니다.

값 객체와 합성 관계

📝 NOTE 합성(Composition) 관계 포함되는 객체의 생명 주기가 포함하는 객체에 종속된다 = 함께 생성되고 함께 삭제된다.

값 객체를 식별하는 방법

💡 TIP 미리 고민하지 말 것 값 객체 타입은 미리 설계하지 않는다. 복잡한 참조 객체를 리팩토링하는 과정에서 발견한다.

  1. 리팩토링 중 숨겨진 간단한 개념 발견 → 개념을 표현하는 값 객체 추가
  2. 참조 객체들 사이 중복 코드 발견 → 중복을 이동시킬 값 객체 추가
  3. 함께 사용되는 변수를 찾기 — 다음 변수들은 값 객체 후보:
    • 함께 초기화 / 함께 변경 / 여러 메서드에서 함께 참조
    • 다른 메서드의 파라미터로 함께 전달 / 유효성을 함께 검증

실전 적용: Game 클래스 리팩토링

📌 IMPORTANT 값 객체를 추가할 때는 관련 변수뿐 아니라 그 변수를 이용하는 로직도 함께 옮기는 것이 핵심이다.

1) x, y → Position (위치)

class Position {
    private final int x, y;
    private Position(int x, int y) { this.x = x; this.y = y; }
    static Position of(int x, int y) { return new Position(x, y); }
    Position shift(...) { return new Position(x + dx, y + dy); }
}

2) width, height → Size (크기)

💡 TIP 함께 사용되는 변수들을 값 객체로 추출하면 인스턴스 변수와 파라미터 개수가 줄고, 값 객체는 작아서 여러 곳(예: Room의 x·y)에서 재사용할 수 있다.

3) 이동 숫자 → Direction (방향)

마무리: 작은 리팩토링의 힘

📌 IMPORTANT 클래스 사이의 책임을 조정하기 전에, 먼저 개별 클래스 단위로 작은 리팩토링을 수행한다. (긴 메서드를 조합 메서드로 분리 / 모호한 개념을 값 객체로 추출) 이렇게 메서드·클래스 수준을 정리하면 책임 조정이 훨씬 수월해진다.

핵심 요약



이전 글
참조 객체와 값 객체 - 작은 클래스 만들기
다음 글
Spring SseEmitter로 알림을 보내고 연결 관리하기