자바를 처음 배울 때, 우리는 클래스를 만들고 습관적으로 모든 필드에 대해
Getter와 Setter를 생성하곤 합니다. IDE가 단축키 한 번이면 만들어주니 너무나 자연스럽죠.
하지만 "이게 정말 객체지향적인가?"라는 의문을 가져본 적 있으신가요?
단순히 값을 넣고 빼기만 한다면 그건 객체가 아니라 데이터 주머니에 불과할지도 모릅니다.
오늘은 무분별한 Getter/Setter 사용이 왜 문제인지, 그리고 이를 해결하기 위한 진짜 객체지향 설계 방법(Rich Domain Model, VO 등)에 대해 정리해보겠습니다.
1. Getter/Setter의 이중성과 문제점
빈약한 도메인 모델 (Anemic Domain Model)
우리는 '정보 은닉'을 위해 필드를 private으로 막습니다. 그런데 바로 public Getter/Setter를 열어버린다면? 사실상 정보 은닉은 깨진 것과 다름없습니다.
// ❌ 상태와 행위가 분리된 절차지향적 코드
public class Order {
private int totalAmount;
// 단순히 값만 꺼내주고 넣어주는 역할
public int getTotalAmount() { return totalAmount; }
public void setTotalAmount(int amount) { this.totalAmount = amount; }
}
// 서비스 로직 (객체의 데이터를 꺼내서 외부에서 처리함)
public void discount(Order order) {
int amount = order.getTotalAmount();
if (amount > 10000) {
order.setTotalAmount(amount - 1000);
}
}
이렇게 되면 객체는 자신의 데이터에 대한 통제권을 잃고, 단순히 데이터 보관소(주머니) 역할만 하게 됩니다.
이것을 빈약한 도메인 모델이라고 합니다.
2. 해결책 - 묻지 말고 시켜라 (Tell, Don't Ask)
객체지향의 핵심은 데이터와 그 데이터를 조작하는 행위(로직)를 한곳에 묶는 것입니다.
데이터를 달라고(Ask) 하지 말고, 원하는 작업을 하라고 시켜야(Tell) 합니다.
풍부한 도메인 모델 (Rich Domain Model)
// ✅ 스스로의 상태를 관리하는 객체
public class Order {
private int totalAmount;
// 객체가 스스로 판단하고 상태를 변경함
public void applyDiscount() {
if (this.totalAmount > 10000) {
this.totalAmount -= 1000;
}
}
}
// 서비스 로직
public void process(Order order) {
order.applyDiscount(); // "할인해!" 라고 명령만 하면 됨
}
이제 외부에서는 Order의 내부 구현을 몰라도 됩니다. 변경 사항이 생겨도 Order 객체 내부만 수정하면 되므로 유지보수성이 대폭 향상됩니다.
3. 더 나은 설계 1: Value Object (VO)
int, long 같은 원시 타입은 값이 무엇을 의미하는지, 유효한 값인지 스스로 증명하지 못합니다.
이를 해결하기 위해 값을 감싸는 객체(VO)를 사용합니다.
Value Object의 특징
- 불변성 (Immutable): 생성 후 값이 변하지 않음 (Setter 없음).
- 유효성 검증: 생성자에서 값의 유효성을 체크함 (예: 금액은 음수 불가).
- 동등성: 값이 같으면 같은 객체로 취급 (equals & hashCode 재정의).
public class Money {
private final int amount; // 불변
public Money(int amount) {
if (amount < 0) {
throw new IllegalArgumentException("돈은 음수가 될 수 없습니다.");
}
this.amount = amount;
}
public Money plus(Money other) {
return new Money(this.amount + other.amount); // 새로운 객체 반환
}
}
이제 int price = -1000; 같은 끔찍한 실수를 원천 차단할 수 있습니다.
4. 더 나은 설계 2: Aggregate Root와 캡슐화
객체들이 복잡하게 얽혀 있을 때, 외부에서 내부의 작은 객체들을 마음대로 조작하게 두면 안 됩니다.
Aggregate Root(대표 객체)를 통해서만 내부에 접근하도록 제약해야 합니다.
잘못된 설계 vs 올바른 설계
| 구분 | 방식 | 문제점/장점 |
|---|---|---|
| Bad | order.getItems().add(item); |
주문(Order) 몰래 아이템을 추가함. 총액 계산 누락 등 버그 발생 위험. |
| Good | order.addItem(item); |
주문 객체가 아이템 추가와 동시에 총액 갱신 등 규칙을 수행함. |
5. 결론: 객체를 객체답게 쓰는 법
- Getter/Setter는 최소한으로: 무조건 만들지 말고, 꼭 필요한 경우(DTO, 조회용)에만 만들자.
- 상태 변경은 명확하게:
setStatus("CANCEL")대신cancelOrder()처럼 의도가 드러나는 메서드를 사용하자. - 검증은 생성 시점에: 생성자에서 유효성을 검사하여, 태어날 때부터 온전한 객체(VO)만 존재하게 하자.
"값을 꺼내서(Get) 내가 계산하고 다시 넣지(Set) 말자."
"객체에게 계산하라고 시키고(Tell), 결과만 받아오자."
이것이 객체지향 프로그래밍의 시작입니다.
'Language > Java' 카테고리의 다른 글
| [JAVA] Stack 구현 & Stack Class의 문제점 (feat. Deque) (0) | 2025.12.19 |
|---|---|
| [JAVA] Collection Framework & Collections Class _ Part 1 (0) | 2025.12.16 |
| [JAVA] 접근제한자 & 캡슐화 (0) | 2025.12.12 |
| [JAVA] 객체 지향 프로그래밍 4대 원칙 (0) | 2025.12.12 |
| [JAVA] String vs StringBuilder vs StringBuffer 비교 정리 (왜 문자열을 다루는 클래스가 3개나 있을까?) (1) | 2025.12.04 |
