오브젝트 실전 강의 교재¶
6장 — 메시지와 인터페이스¶
원서: 조영호 『오브젝트』 대상: Java/Spring 백엔드 입문~중급 수강생 형식: 개념 → 비유 → 예시 → 핵심 교훈 → 현업 예제 → 함정 → 체크리스트 → 퀴즈(정답 분리)
0. 이 장을 시작하기 전에¶
0.1 학습 목표¶
- 객체 협력의 언어 — 메시지·인터페이스 의 의미.
- 묻지 말고 시켜라 (Tell, Don't Ask) 가 왜 핵심인가.
- 의도를 드러내는 인터페이스 의 설계 기준.
- 디미터 법칙 — 한 점만 허용 도그마 vs 본질.
- 명령-쿼리 분리 (CQS) — 부작용과 조회의 분리.
0.2 큰 그림 — 메시지가 첫 시민¶
[ 클래스 ] [ 메시지 ]
객체의 정적 정의 (필드·메서드) 객체 사이 요청·응답
구현 디테일 협력의 단위 ★
→ OO 의 본질은 클래스가 아니라 메시지
("메시지가 객체를 결정한다" — 3장)
비유 — "회의 vs 사람"
회사에 큰 프로젝트가 잡혔다고 해 봅시다. 가장 먼저 정해지는 것은 "어떤 회의를 열 것인가" 입니다. 회의가 잡히면 그 자리에는 "PM 자리·개발자 자리·디자이너 자리" 가 정해지고, 마지막에야 각 자리에 앉을 사람이 결정됩니다. 같은 'PM 자리' 라도 김 PM 이 앉을 수도, 이 PM 이 앉을 수도 있고, 일정만 맞으면 누가 앉아도 회의는 진행됩니다.
객체지향도 같은 순서입니다. 객체들 사이에 오갈 메시지가 가장 먼저 정해지고, 그 메시지를 받을 수 있는 역할이 그 다음에 정의되며, 그 역할을 수행하는 객체는 마지막에 결정됩니다. 같은 역할이라도 다양한 객체가 그 자리에 들어올 수 있고, 호출자는 누가 들어왔는지 알 필요가 없습니다.
0.3 현업에서 왜 중요한가¶
- API 설계의 모든 결정 (시그니처·반환 타입·예외) 이 이 장에 모임.
- Spring Controller·Repository·Service 의 메서드 시그니처 결정.
- Effective Java Item 51 (시그니처)·56 (Javadoc) 과 같은 결.
1. 협력과 메시지¶
1.1 클라이언트-서버 모델¶
객체 협력 = 요청 (메시지) 보내는 클라이언트 + 응답 하는 서버.
- 누가 클라이언트·누가 서버는 상대적 (한 객체가 한 협력에선 서버, 다른 협력에선 클라이언트).
- 모든 객체가 양쪽 역할 모두 가능.
1.2 메시지와 메시지 전송¶
- 메시지: 의도 표현 (예:
screening.reserve(customer, 2)). - 메시지 전송: 호출 행위.
- 수신자가 메서드를 결정 (다형성).
1.3 메시지와 메서드의 차이¶
- 메시지: 무엇을 요청 (의도).
- 메서드: 어떻게 처리 (구현).
같은 메시지에 객체마다 다른 메서드 → 다형성.
DiscountPolicy policy = ...; // 컴파일타임: 추상
policy.calculateDiscountAmount(screening); // 메시지 전송
// 런타임: AmountDiscountPolicy.calculateDiscountAmount() 또는 PercentDiscountPolicy.calculateDiscountAmount() (다형성)
1.4 퍼블릭 인터페이스와 오퍼레이션¶
- 퍼블릭 인터페이스: 객체가 외부에 노출한 메시지의 집합.
- 오퍼레이션: 인터페이스에 선언된 메시지 (추상).
- 메서드: 그 오퍼레이션의 구체 구현.
1.5 시그니처¶
오퍼레이션의 이름 + 매개변수 목록 + 반환 타입 + 예외. 객체 협력의 계약.
2. 인터페이스와 설계 품질¶
2.1 묻지 말고 시켜라 (Tell, Don't Ask)¶
데이터를 꺼내 와 호출자가 처리하지 말고, 데이터를 가진 객체에게 일을 시켜라.
// ❌ Ask — 데이터 꺼내서 외부가 결정
if (account.getBalance() >= amount) {
account.setBalance(account.getBalance() - amount);
}
// ✅ Tell — 객체에게 시킴
account.withdraw(amount); // 내부 검증·차감 모두 캡슐화
→ Account 가 자기 데이터로 자기 결정. 정보 전문가 (5장) 의 실천.
2.2 의도를 드러내는 인터페이스¶
메서드 이름·시그니처가 무엇을 하는지 (의도) 드러내야지, 어떻게 하는지 (구현) 노출 X.
// ❌ 구현 노출
boolean hasMoreThanFiveLateDeliveries(Customer c);
// ✅ 의도
boolean isPriorityCustomer(Customer c);
2.3 함께 모으기¶
관련 메시지를 한 인터페이스에. 응집도 ↑.
3. 원칙의 함정¶
3.1 디미터 법칙은 점 하나 강제 X¶
디미터 법칙: "객체는 자신이 직접 알고 있는 객체에게만 메시지를 보낸다".
// ❌ 위반 — 친구의 친구
order.getCustomer().getAddress().getStreet();
// ✅ — 친구에게만
order.getCustomerStreet();
→ 도그마 "점은 1개만" 이 아니라 객체 내부 구조 노출 회피 가 본질.
3.2 결합도와 응집도의 충돌¶
- 결합도 낮추려고 위임을 너무 많이 = 중개자 (악취).
- 응집도 높이려고 너무 묶으면 = 거대 클래스.
→ 균형. 한 원칙을 절대값으로 끌고 가지 마라.
4. 명령-쿼리 분리 원칙 (CQS)¶
4.1 핵심¶
한 메서드는 상태를 변경하거나 (명령) 값을 반환하거나 (조회) — 둘 중 하나만.
// ❌ — 둘 다 하는 함수
public Money withdraw(int amount) {
balance -= amount;
return new Money(balance);
}
// ✅ — 분리
public void withdraw(int amount) { // 명령 — void
balance -= amount;
}
public Money balance() { return balance; } // 조회 — 부작용 X
4.2 명령-쿼리 분리와 참조 투명성¶
조회 메서드는 여러 번 호출해도 같은 결과 (참조 투명). 부작용 있는 함수는 호출 순서·횟수가 결과 영향 → 추론 어려움.
4.3 책임에 초점을 맞춰라¶
CQS 가 본질적으로 가리키는 것: 한 메서드 = 한 책임. Clean Code 3장 "함수는 한 가지만" 과 같은 결.
5. 인터페이스 설계 절차¶
1. 메시지를 먼저 결정¶
협력에 필요한 요청·응답을 시나리오로 적는다.
2. 메시지가 객체를 결정¶
그 메시지를 받을 수 있는 객체 (정보 전문가) 가 인터페이스의 주인.
3. 의도가 드러나는 이름¶
표준 명명 규칙 (findBy*, is*, to*).
4. 매개변수·반환 정련¶
- 매개변수 4개 이하 (객체화 검토).
- 반환은 컬렉션/Optional (null 회피).
- 예외는 호출자 관점에서 분류.
핵심 교훈¶
- 메시지가 객체보다 먼저 — OO 의 본질.
- Tell, Don't Ask — 데이터 꺼내지 말고 객체에게 시켜라.
- 의도 드러내는 인터페이스 — 무엇을 (의도) > 어떻게 (구현).
- 디미터 법칙은 도그마 X — 내부 구조 노출 회피가 본질.
- CQS — 명령과 조회 분리, 추론·테스트 자유.
- 시그니처가 계약 — 한 번 공개되면 영원.
현업 예제 — Spring 컨트롤러의 시그니처¶
안티패턴¶
// ❌ — Ask + boolean 플래그 + 다중 책임
@PostMapping
public Map<String, Object> handle(@RequestBody Map<String, Object> raw, boolean isVip) {
// 파싱·검증·저장·이메일 발송 다
}
권장¶
@PostMapping("/orders")
public OrderResponse create(@Valid @RequestBody CreateOrderCommand cmd) { // 의도 + 검증
Order order = orderService.create(cmd);
return OrderResponse.from(order);
}
@GetMapping("/orders/{id}")
public OrderResponse get(@PathVariable Long id) { // 명령-쿼리 분리
return orderService.findById(id)
.map(OrderResponse::from)
.orElseThrow(() -> new OrderNotFoundException(id));
}
→ 의도가 메서드 이름·시그니처에 명시. CQS (create 명령, get 조회).
함정 / 주의¶
- Tell, Don't Ask 도그마 — getter 가 무조건 나쁜 건 아님. 디스플레이용·DTO 등 조회 자체가 책임인 경우는 OK.
- 디미터 "점 1개" 강제 = 함수 폭증. 본질은 내부 구조 노출 회피.
- CQS 가 깨지는 경우 —
Iterator.next()처럼 표준 라이브러리가 이미 깸. 도메인 코드만 엄격히. - 공개 API 는 한 번 결정되면 영원 — Effective Java Item 56·Clean Code 11장과 같은 결.
체크리스트 (인터페이스 리뷰용)¶
- 메서드 이름이 의도 (무엇을) 를 드러내는가 — 구현 (어떻게) 노출 X
-
a.getB().getC().d()같은 체인이 없는가 - 한 메서드가 명령 + 조회를 동시에 하지 않는가 (CQS)
- boolean 플래그 매개변수 대신 enum 또는 분리된 메서드인가
- 매개변수 4개 이상 → 객체화 검토
퀴즈¶
- 메시지 와 메서드 의 차이는?
- Tell, Don't Ask 의 본질을 한 문장으로?
- 디미터 법칙 을 "한 점만 허용" 으로 해석하면 안 되는 이유?
- CQS 의 실용적 효과 두 가지?
- Effective Java Item 51 (시그니처 신중) 과 6장의 연결은?
정답·해설¶
- 메시지 = 무엇을 요청 (의도). 메서드 = 어떻게 처리 (구현). 같은 메시지에 객체마다 다른 메서드 = 다형성. OO 의 본질은 메시지 (요청) 이고 메서드는 그 응답 방식.
- 데이터를 꺼내지 말고 객체에게 일을 시켜라 — 데이터 가진 객체가 자기 결정. 외부가 데이터로 분기·계산하면 캡슐화 위반·결합도 폭증·빈혈 도메인.
- 본질이 점 개수가 아니라 객체 내부 구조 노출 회피. 도그마로 점 1개 강제하면 의미 없는 위임 메서드 폭증 (중개자 악취).
a.b().c()가 안전한 경우 (예:Optional.of(x).map(...).get()) 도 많음. - (1) 추론 쉬움 — 조회는 부작용 없으므로 여러 번 호출해도 같은 결과 (참조 투명). (2) 테스트 쉬움 — 명령과 조회 분리하면 mock·verify 가 명확.
- Item 51 = "한 번 공개된 시그니처는 영원" + "매개변수 신중" + "boolean 플래그 회피". 6장의 "의도 드러내는 인터페이스" + "CQS" + "매개변수 객체화" 와 같은 결. Effective Java 가 메서드 단위, 오브젝트 가 객체 협력 관점.
다음 장 예고 — 7장: 객체 분해¶
객체로 분해하기 전에는 프로시저 추상화 와 데이터 추상화 가 있었다. 7장은 두 추상화의 역사·차이·한계를 다루고, 추상 데이터 타입 → 클래스 로 발전한 흐름. 객체지향이 단순한 데이터 묶음을 넘어서는 이유.