← ALL ENTRIES

참조 객체와 값 객체 - 작은 클래스 만들기


클래스를 작게 유지하는 두 기법 중 “값 객체 분리”로 넘어가기 위한 전 단계로, 그 대비 개념인 참조 객체(Reference Object)별칭(alias) 문제를 게임 매출 예제로 설명한다.

클래스도 작게 만들어야 한다

  • 메소드를 작게 만들면 수정·재사용이 쉬워지듯, 클래스도 작게 만드는 게 좋다.
  • 문제는 적절한 크기를 정하기 어렵다는 것.
    • 너무 작으면 → 클래스 수가 늘어 사용·관리가 어려움
    • 너무 크면 → 수정·재사용이 어려움

💡 TIP 작은 클래스를 만드는 두 가지 방법

  1. 클래스의 일부 로직을 값 객체로 분리한다.
  2. 단일 책임 원칙(SRP) 을 적용해 클래스의 책임을 작게 유지한다.

(단일 책임 원칙은 이후 강의에서 다루고, 이번에는 값 객체를 향한 기초로 참조 객체부터 본다.)

예제: 게임 매출 관리 애플리케이션

요구사항

  • 게임 = 판매 가능한 상품. 판매자가 시스템에 등록·판매한다.
  • 등록된 게임은 정가 10,000원 / 할인율 10%.
  • 매출은 게임별로 하나씩 관리된다 (게임 1개 등록 → 관련 매출 1개 생성).
  • 매출은 누적 수량 + 누적 금액을 관리하고, 게임별 영업이익도 계산해야 한다.
  • 영업이익 = 누적 금액의 20%.

계산 흐름 예시

판매개당 판매가이번 판매액누적 수량누적 금액영업이익(20%)
3개 판매9,000원27,000원3개27,000원5,400원
+2개 판매9,000원18,000원5개45,000원9,000원
  • 개당 판매가 = 정가 10,000 × (1 − 0.1) = 9,000원
  • 영업이익은 계산 후 올림 처리한다.

코드 구현

Game 클래스

  • 인스턴스 변수: title, price(정가), discountRate(할인율)
  • calculateSalePrice() : 정가에 할인율을 반영해 게임 1개의 판매가를 계산

Sales 클래스

  • 인스턴스 변수: game, totalQuantity(누적 수량), totalPrice(누적 금액)
  • addSale(quantity) : 판매될 때마다 호출
    • 판매 수량을 totalQuantity에 더함
    • 판매 금액(game.calculateSalePrice() × quantity)을 totalPrice에 더함
  • profit() : 영업이익 = totalPrice × 20%올림
// 개념 스케치
class Game {
    String title;
    int price;         // 정가
    double discountRate; // 할인율

    int calculateSalePrice() {
        return (int)(price * (1 - discountRate)); // 10000 * 0.9 = 9000
    }
}

class Sales {
    Game game;
    int totalQuantity;
    int totalPrice;

    void addSale(int quantity) {
        totalQuantity += quantity;
        totalPrice += game.calculateSalePrice() * quantity;
    }

    int profit() {
        return (int)Math.ceil(totalPrice * 0.2); // 누적 금액의 20% 올림
    }
}

문제 발견: 여러 Sales 객체로 나누면?

  • 테스트 1 (Sales 하나) : 5개 판매 → 누적 45,000원, 영업이익 9,000원 ✅
  • 테스트 2 (Sales 둘) :
    • sales 로 3개 판매 → 27,000원, 영업이익 5,400원
    • anotherSales 로 2개 판매 → 18,000원, 영업이익 3,600원

📌 IMPORTANT 두 개의 Sales 객체가 각각 전체 판매의 일부만 관리하게 되어, 어느 쪽으로 profit()을 호출해도 영업이익의 일부만 나온다. 둘 다 정답이 아니다.

  • 영업이익을 정상 계산하려면 Sales 인스턴스는 오직 하나만 존재해야 하고, 그 하나에 모든 수량·총액이 모여야 한다.
  • 여러 클라이언트가 매출을 관리하더라도 모든 변수가 같은 Sales 객체를 참조해야 한다.
  • 그러면 sales(3개) + anotherSales(2개)가 하나의 객체에 모여, 어떤 변수로 계산해도 정확한 영업이익이 나온다.

참조 객체(Reference Object)

📝 NOTE 정의 여러 클라이언트가 동일한 객체를 공유해서 오퍼레이션을 실행해야 하는 객체를 참조 객체(Reference Object) 라고 부른다.

  • 상품·고객처럼 상대적으로 크고 복잡한 도메인 객체를 구현하는 데 사용된다.
  • 일반적으로 상태를 변경할 수 있는 가변(mutable) 객체로 구현된다.
  • 예제의 Sales는 누적 수량·총액이 계속 바뀌므로 가변 객체로 구현된 참조 객체.
  • 모든 변수가 동일한 객체를 참조하므로, Java의 동등 연산자(==) 비교 시 항상 true 가 반환된다.

별칭(Alias)

📝 NOTE 정의 두 개의 변수가 동일한 객체를 가리킬 때, 그 두 변수를 별칭(alias) 이라고 부른다. 하나의 객체를 여러 이름으로 부르는 것.

  • 예제에서 salesanotherSales는 같은 Sales 객체에 붙은 서로 다른 이름.
  • 별칭은 객체를 새 변수에 할당하거나 다른 메소드의 파라미터로 전달할 때 생성된다.
  • 한 별칭을 통해 변경된 상태는 다른 별칭으로 전파된다.
    • 예: sales.addSale()로 수량 5→6개, 금액 45,000→54,000원으로 바꾸면 anotherSales도 동일한 상태를 본다.
    • anotherSales 입장에서는 아무것도 안 했는데 갑자기 상태가 바뀐 것처럼 보인다.

📌 IMPORTANT 별칭은 장점이자 단점

  • 장점 : 여러 클라이언트가 상태를 공유해야 할 때(예: 하나의 Sales가 모든 판매 내역 관리) 쉽게 해결된다.
  • 단점 : 잘못 쓰면 원하지 않는 상황에서도 변경이 전파된다. 예상치 못한 상태 변경은 심각한 버그로 이어질 수 있다.
  • 결론: 참조 객체는 별칭으로 상태를 쉽게 공유하지만, 별칭 문제 때문에 상태 관리가 복잡해진다.

해결의 방향: 값 객체

💡 TIP 별칭 문제의 좋은 해결책은, 상태가 바뀌는 참조 객체 대신 상태가 바뀌지 않는 불변(immutable) 객체를 쓰는 것이다. 이렇게 값이 바뀌지 않는 작은 객체값 객체(Value Object) 라고 부른다.

→ 다음 시간: 값 객체를 이용해 참조 객체의 복잡성을 낮추는 방법.

핵심 요약

  • 클래스도 작게 유지하는 게 좋고, 그 방법 중 하나가 로직을 값 객체로 분리하는 것이다.
  • 참조 객체는 여러 클라이언트가 하나의 객체를 공유해 오퍼레이션하는, 보통 가변인 도메인 객체다.
  • 하나의 상태(예: 전체 매출)를 정확히 관리하려면 모든 변수가 같은 객체를 참조(별칭) 해야 한다.
  • 별칭은 상태 공유라는 장점과, 의도치 않은 변경 전파라는 단점을 동시에 가진다.
  • 이 복잡성을 낮추는 대안이 불변인 값 객체다.