참조 객체와 값 객체 - 작은 클래스 만들기
클래스를 작게 유지하는 두 기법 중 “값 객체 분리”로 넘어가기 위한 전 단계로, 그 대비 개념인 참조 객체(Reference Object) 와 별칭(alias) 문제를 게임 매출 예제로 설명한다.
클래스도 작게 만들어야 한다
- 메소드를 작게 만들면 수정·재사용이 쉬워지듯, 클래스도 작게 만드는 게 좋다.
- 문제는 적절한 크기를 정하기 어렵다는 것.
- 너무 작으면 → 클래스 수가 늘어 사용·관리가 어려움
- 너무 크면 → 수정·재사용이 어려움
💡 TIP 작은 클래스를 만드는 두 가지 방법
- 클래스의 일부 로직을 값 객체로 분리한다.
- 단일 책임 원칙(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) 이라고 부른다. 하나의 객체를 여러 이름으로 부르는 것.
- 예제에서
sales와anotherSales는 같은 Sales 객체에 붙은 서로 다른 이름. - 별칭은 객체를 새 변수에 할당하거나 다른 메소드의 파라미터로 전달할 때 생성된다.
- 한 별칭을 통해 변경된 상태는 다른 별칭으로 전파된다.
- 예:
sales.addSale()로 수량 5→6개, 금액 45,000→54,000원으로 바꾸면anotherSales도 동일한 상태를 본다. anotherSales입장에서는 아무것도 안 했는데 갑자기 상태가 바뀐 것처럼 보인다.
- 예:
📌 IMPORTANT 별칭은 장점이자 단점
- 장점 : 여러 클라이언트가 상태를 공유해야 할 때(예: 하나의 Sales가 모든 판매 내역 관리) 쉽게 해결된다.
- 단점 : 잘못 쓰면 원하지 않는 상황에서도 변경이 전파된다. 예상치 못한 상태 변경은 심각한 버그로 이어질 수 있다.
- 결론: 참조 객체는 별칭으로 상태를 쉽게 공유하지만, 별칭 문제 때문에 상태 관리가 복잡해진다.
해결의 방향: 값 객체
💡 TIP 별칭 문제의 좋은 해결책은, 상태가 바뀌는 참조 객체 대신 상태가 바뀌지 않는 불변(immutable) 객체를 쓰는 것이다. 이렇게 값이 바뀌지 않는 작은 객체를 값 객체(Value Object) 라고 부른다.
→ 다음 시간: 값 객체를 이용해 참조 객체의 복잡성을 낮추는 방법.
핵심 요약
- 클래스도 작게 유지하는 게 좋고, 그 방법 중 하나가 로직을 값 객체로 분리하는 것이다.
- 참조 객체는 여러 클라이언트가 하나의 객체를 공유해 오퍼레이션하는, 보통 가변인 도메인 객체다.
- 하나의 상태(예: 전체 매출)를 정확히 관리하려면 모든 변수가 같은 객체를 참조(별칭) 해야 한다.
- 별칭은 상태 공유라는 장점과, 의도치 않은 변경 전파라는 단점을 동시에 가진다.
- 이 복잡성을 낮추는 대안이 불변인 값 객체다.