책임과 메시지
- 자율적인 책임
- 메시지와 메서드의 구분
- 다형성
- What/Who 사이클
- 묻지 말고 시켜라
- 인터페이스
- 인터페이스와 구현의 분리
메시지를 중심으로 객체지향 설계의 핵심 원칙들을 쏟아내보자. 4장이 "협력 -> 책임 -> 역할"이라는 큰 흐름을 잡았다면, 이번엔 그 협력이 실제로 어떻게 이뤄지는지 — 메시지를 통해 — 깊게 들어가는 거야. 가장 실전적인 원칙들이 많이 나오는 장이지.
4장에서 책임이 "하는 것"과 "아는 것"으로 나뉜다고 했는데, 여기서는 책임의 수준을 다뤄. 같은 책임이라도 어떻게 표현하느냐에 따라 객체의 자율성이 달라지거든.
예를 들어 재판 장면을 생각해봐. 왕이 모자 장수에게 증언을 요청하지.
- 어제 오후 네 시에서 다섯 시 사이에 일어난 일을 시간순으로 말하라 — 이건 너무 구체적이야. 어떻게 증언해야 하는지까지 지정하고 있잖아
- 증언하라 — 이건 적절한 수준이지. 무엇을 해야 하는지만 말하고, 어떻게 할지는 모자 장수가 자율적으로 결정해
책임이 너무 구체적이면 객체의 자율성이 사라져. 구현 방식까지 강제하는 셈이니까. 반대로 너무 추상적이면 객체가 뭘 해야 할지 모호해지고.
좋은 책임은 **무엇(what)**을 말하지, **어떻게(how)**를 말하지 않아. 이 원칙이 이 장 전체를 관통하거든.
1장에서도 잠깐 나왔지만, 여기서 더 깊게 파고들어보자.
- 메시지(message) — 객체가 다른 객체에게 보내는 요청. "무엇을 해달라"는 거야
- 메서드(method) — 메시지를 받은 객체가 실제로 실행하는 코드. "어떻게 처리할지"지
메시지를 보내는 쪽(송신자)은 수신자가 어떤 메서드를 실행할지 몰라. 알 필요도 없어. 송신자는 그냥 "증언하라"는 메시지를 보내고, 수신자가 어떤 방식으로 증언하든 상관하지 않거든.
이 분리가 왜 중요하냐면, 유연성 때문이야. 메시지는 고정하되 메서드를 바꿀 수 있으면, 시스템의 행동을 수정할 때 송신자를 건드릴 필요가 없지. 새로운 종류의 증인을 추가해도, "증언하라"는 메시지를 보내는 쪽의 코드는 그대로야.
메시지와 메서드의 분리가 만들어내는 가장 강력한 결과가 **다형성(polymorphism)**이야.
같은 메시지를 받아도 수신자의 타입에 따라 다르게 처리하는 거지. "증언하라"는 같은 메시지를 모자 장수, 요리사, 앨리스가 각각 다른 방식으로 수행할 수 있어.
다형성의 실전적 의미:
- 송신자는 수신자의 구체적인 타입을 몰라도 돼. "증언할 수 있는 무언가"라는 역할만 알면 되지
- 새로운 타입의 수신자를 추가해도 송신자의 코드를 수정할 필요가 없어. 확장에는 열려 있고 수정에는 닫혀 있지 — OCP(개방-폐쇄 원칙)
- 수신자를 교체할 수 있어. 런타임에 다른 객체로 바꿔치기해도 협력이 깨지지 않거든
다형성은 역할(role)의 프로그래밍 레벨 구현이야. 1장에서 "바리스타가 누구든 커피를 만들 수 있으면 된다"고 했던 그 대체 가능성이 다형성을 통해 코드에서 실현되는 거지.
저자가 제시하는 설계 사고방식인 What/Who 사이클은 두 단계로 이뤄져:
- What — 어떤 행동이 필요한가? (어떤 메시지가 필요한가?)
- Who — 누가 그 행동을 수행할 것인가? (누가 그 메시지를 받을 것인가?)
What을 먼저 결정하고, Who를 나중에 결정해. 순서가 핵심이야.
많은 개발자가 반대로 하거든. "이 클래스가 있으니까 이 메서드를 넣자" — 이건 Who를 먼저 정하고 What을 끼워맞추는 거야. 이렇게 하면 불필요한 메서드가 생기고, 책임이 이상한 곳에 배치되고, 결합도가 높아지지.
올바른 접근: "주문의 총액을 계산해야 한다" (What) -> "그럼 누가 이 메시지를 받아야 하지?" (Who) -> "주문 항목 정보를 가진 주문 객체가 적절하겠다"
What/Who 사이클을 따르면 메시지가 인터페이스를 결정하고, 인터페이스가 객체를 결정해. 이게 메시지 중심 설계의 핵심이야.
Tell, Don't Ask — 객체지향 설계에서 가장 유명한 격언 중 하나지.
나쁜 예: 객체의 상태를 물어보고(get), 그 값을 기반으로 외부에서 판단하고, 다시 객체에게 지시하는 것. getter로 데이터를 꺼내서 밖에서 로직을 돌리는 패턴이 이거야.
좋은 예: 객체에게 그냥 시키는 것. 판단도 객체가 하고, 실행도 객체가 하게. 상태를 꺼내지 않고 메시지만 보내는 거지.
구체적으로 보면:
// 묻고 있다 (나쁨)
if (account.getBalance() >= amount) {
account.setBalance(account.getBalance() - amount);
}
// 시키고 있다 (좋음)
account.withdraw(amount);
첫 번째 코드는 계좌의 상태를 꺼내서 외부에서 판단하고 다시 집어넣잖아. 계좌의 자율성이 없지. 두 번째 코드는 "출금해라"고 메시지만 보내. 잔액이 충분한지 확인하는 것도, 어떻게 출금하는지도 계좌 객체가 알아서 하거든.
"묻지 말고 시켜라" 원칙을 지키면:
- 객체의 자율성이 보장돼
- 캡슐화가 자연스럽게 지켜지고 (상태를 꺼내지 않으니까)
- 객체 간 결합도가 낮아지지 (내부 상태 구조를 몰라도 되니까)
저자는 인터페이스를 객체가 외부에 공개하는 메시지의 목록으로 정의해.
인터페이스의 특징:
- 사용법을 익히면 내부 구조를 몰라도 돼. 자판기의 버튼을 누를 줄 알면 내부 메커니즘을 몰라도 음료를 뽑을 수 있잖아
- 인터페이스 뒤의 구현을 자유롭게 바꿀 수 있어. 자판기 내부를 개조해도 버튼 배치가 같으면 사용법은 안 변하지
- 인터페이스만 동일하면 서로 대체 가능해. 다형성의 기반이야
좋은 인터페이스의 조건:
- 최소 인터페이스 — 꼭 필요한 메시지만 공개해. 많이 공개할수록 변경의 영향이 커지거든
- 추상적인 인터페이스 — "어떻게"가 아니라 "무엇"을 표현해.
insertCoinAndSelectBeverageAndDispense()보다purchase()가 낫지
마지막으로 저자는 인터페이스와 구현의 분리 원칙을 정리해.
모든 객체는 두 부분으로 나뉘어:
- 퍼블릭 인터페이스 — 외부에 공개되는 메시지. 다른 객체가 알아야 하는 부분
- 구현(implementation) — 외부에 감춰지는 내부 로직과 상태. 다른 객체가 몰라야 하는 부분
이 분리의 이유는 단순해 — 변경의 파급 효과를 최소화하기 위해서야. 구현이 바뀌어도 인터페이스가 그대로면 다른 객체는 영향을 받지 않지. 반대로 인터페이스가 바뀌면 그 인터페이스를 사용하는 모든 객체가 영향을 받아. 그래서 인터페이스는 신중하게, 구현은 자유롭게.
이 원칙이 이 장에서 나온 모든 것을 하나로 엮어:
- 자율적인 책임 -> 구현이 아닌 인터페이스 수준에서 책임을 정의해라
- 메시지와 메서드의 분리 -> 메시지는 인터페이스, 메서드는 구현이야
- 다형성 -> 인터페이스가 같으면 구현이 달라도 대체 가능하지
- Tell, Don't Ask -> 인터페이스(메시지)만 사용하고, 구현(상태)에 접근하지 마
정리
5장 읽고 기억할 거 세 가지:
- What을 먼저, Who를 나중에. 어떤 메시지가 필요한지를 먼저 결정하고, 누가 처리할지는 나중에 결정해. 이 순서가 좋은 인터페이스를 만들어
- 묻지 말고 시켜라. 상태를 꺼내서 밖에서 판단하지 말고, 객체에게 그냥 요청해. 캡슐화와 자율성은 이 원칙에서 시작하지
- 인터페이스와 구현을 분리해라. 외부에는 메시지(무엇)만 공개하고, 메서드(어떻게)는 감춰. 변경의 파급 효과를 최소화하는 핵심 전략이야