이 문서는 의문을 가지고 있던 지점을 AI 에이전트와 대화하며 정리한 메모입니다. 손님과 바리스타 예시를 통해 메시지 패싱, 역할과 책임, 메서드 이름과 결합도의 관계를 정리합니다.
객체지향 설계: 메시지 패싱과 역할·책임#
손님(Customer)과 바리스타(Barista)의 관계를 통해 객체지향 설계 원칙을 이해해 본다.
Q1. 바리스타가 손님에게 주문을 받는다는 형태로 표현(메시지)이 가능하지 않을까?#
우아한형제들 기술 블로그 객체지향과 메시지에서는
customer.orderCoffee(barista)형태로,
손님이 바리스타에게 메시지를 보내는 방향으로 설계되어 있다.
그런데 현실에서 바리스타가 손님의 주문을 받는 것도 사실인데,
이를 뒤집어서barista.takeOrderFrom(customer)처럼
바리스타가 손님으로부터 주문을 받아오는 방향으로 설계하면 안 되는 걸까?
두 방향 모두 같은 상황을 표현하는 것 같은데, 어떤 차이가 있는가?
핵심적인 질문이다. 이 질문은 단순히 코드 문법을 넘어, 객체의 역할, 책임, 협력을 어떻게 나눌 것인지와 연결된다.
결론부터 말하면, 이 예시에서는 ‘손님이 바리스타에게 커피를 주문한다’는 방향이 더 자연스럽다.barista.takeOrderFrom(customer) 같은 형태가 문법적으로 불가능한 것은 아니지만, 보통은 결합도와 책임 분리를 불리하게 만들기 쉽다.
왜 ‘손님’이 메시지를 보내는 주체인가?#
가장 중요한 질문은 ‘누가 요청하고, 누가 그 요청을 처리하는가’ 이다.
현실 세계의 카페를 떠올려보자.
- 요청의 시작: 커피가 필요한 쪽은 손님이다. 손님은 ‘커피를 만들어 달라’는 요청(Request) 을 보낸다.
- 요청의 처리: 바리스타는 요청을 받아 커피를 준비하는 역할을 맡는다.
GNU Smalltalk 문서는 객체를 바깥에서 보면 상태와 연산만 드러나는 블랙박스로 설명하고, 어떤 일을 시키려면 그 객체에 메시지를 보내야 한다고 설명한다. Java 문서도 객체는 메서드를 통해 외부와 상호작용한다고 설명한다.
이 기준으로 보면, 이 시나리오는 커피가 필요한 손님이 바리스타에게 요청을 보내는 협력으로 모델링하는 편이 읽기 쉽다.
- 손님 객체:
주문한다(order)라는 메시지를 보낼 책임을 가진다. - 바리스타 객체:
커피를 만든다(makeCoffee)라는 메시지를 받아 처리할 책임을 가진다.
코드로 표현하면, 손님 객체의 메서드 내부에서 barista.makeCoffee(...)를 호출하는 형태가 된다.
public class Customer {
public void orderCoffee(Barista barista, String coffeeName) {
barista.makeCoffee(coffeeName);
}
}이 구조가 자연스럽게 느껴지는 이유는 요청을 시작하는 배경이 손님의 필요에 있기 때문이다.
만약 ‘바리스타가 주문을 받는다면’ 무엇이 문제일까?#
barista.takeOrderFrom(customer) 와 같은 코드를 상상해볼 수 있다. 이 코드가 늘 틀렸다고 말할 수는 없지만, 보통은 다음과 같은 문제가 생기기 쉽다.
1. 구체 타입 의존과 높은 결합도#
- 바리스타는 원래 손님이 누구인지 몰라도 된다. 그저 ‘커피 주문’이라는 요청만 받아 처리하면 된다.
- 하지만
takeOrderFrom(customer)형태가 되면, 바리스타가 손님이라는 구체 타입을 알아야 한다. - 더 나아가, 손님이 무엇을 원하는지(
customer.getWants()) 직접 물어봐야 할 수도 있다. - 이렇게 되면 바리스타는 손님이라는 특정 타입에 의존하게 된다.
- 나중에 손님이 아니라 키오스크나 모바일 앱으로 주문을 받는다면
→takeOrderFrom(kiosk),takeOrderFrom(mobileApp)같은 형태로 퍼질 가능성이 커진다.
→ 변경에 취약한 구조가 되기 쉽다.
- 나중에 손님이 아니라 키오스크나 모바일 앱으로 주문을 받는다면
2. 불필요한 책임의 증가#
- 바리스타의 핵심 책임은 ‘커피를 준비하는 것’ 이다.
- 그런데 주문 정보를 능동적으로 수집하는 책임까지 함께 지면, 바리스타의 역할 경계가 흐려질 수 있다.
- 이런 구조는 SRP 관점에서도 책임 분리를 어렵게 만들 수 있다.
3. 협력 흐름이 덜 자연스럽다#
- 출발 요구는 손님 쪽에 있는데, 코드의 중심은 바리스타 객체가 된다.
- 그러면 요청자와 처리자의 구분이 흐려져, 이 예시의 협력 구조를 읽기 어렵게 만들 수 있다.
두 관점 비교#
| 항목 | 👍 손님이 주문한다 (Customer → Barista) | 👎 바리스타가 주문받는다 (Barista → Customer) |
|---|---|---|
| 메시지 주체 | 손님(Client): 요청하는 쪽 | 바리스타가 주문자 정보를 읽어 와야 함 |
| 결합도 | 낮음: 바리스타는 손님을 직접 몰라도 협력 가능 | 높아지기 쉬움: 바리스타가 손님 타입을 알아야 할 수 있음 |
| 유연성/확장성 | 높음: 키오스크가 주문해도 요청 형식을 유지하기 쉬움 | 낮아지기 쉬움: 주문 주체가 늘수록 바리스타 코드가 흔들릴 수 있음 |
| 책임 분리 | 명확함: 손님(주문), 바리스타(제조) | 모호해지기 쉬움: 바리스타가 주문과 제조 책임을 함께 가질 수 있음 |
| 자연스러움 | 현실의 요청 방향과 협력 구조가 잘 맞음 | 요청자와 처리자의 역할이 덜 선명함 |
정리하면, 객체지향 설계의 핵심은 현실 세계의 역할과 책임을 코드로 기계적으로 복사하는 것이 아니라,
누가 무엇을 요청하고 누가 그것을 처리하는가를 자연스럽게 표현하는 것이다.
이 예시에서는 서비스를 요청하는 손님이 메시지를 보내고,
서비스를 제공하는 바리스타가 그 메시지를 수신해 처리하는 구조가 더 읽기 쉽고 변경에도 유리하다.
Q2. 왜 손님이 바리스타에게 “커피를 제조하라"는 메시지를 보내는 것인가?#
앞서 손님이 바리스타에게 메시지를 보내는 주체라고 했다.
그런데 손님도 마찬가지로, 외부에서 누군가가 손님에게
“커피를 주문해라"라고 메시지를 보내서 시작되는 게 아니라main이나 다른 실행 주체가 직접 손님의 메서드를 호출해서 시작된다.
그렇다면 굳이 손님을 거칠 필요 없이,main이 바리스타에게barista.makeCoffee()를 직접 호출하면 안 되는 건가?
왜 손님 객체가 바리스타에게makeCoffee()를 보내는 흐름으로 보는 것이 자연스럽고,
또 그 메시지 이름이 왜 “제조하라(makeCoffee)“인가?
이 질문은 ‘객체에게 어떤 메시지를 보낼 것인가’ 를 고민하는 과정과 직결된다.
결론부터 말하면, 이 예시에서는 손님이 바리스타에게 makeCoffee()를 요청하는 흐름으로 보는 것이 자연스럽다.
다만 이것을 “손님이 명령한다"는 뜻으로 이해하면 어색하다.
객체지향에서의 메시지 전송은 보통 요청(Request) 으로 이해하는 편이 더 정확하다.
왜 main이 barista.makeCoffee()를 직접 호출하면 안 되는가?#
엄밀히 말하면, main이 barista.makeCoffee()를 직접 호출하는 코드 자체가 금지되는 것은 아니다.
다만 이 예시를 객체 협력 관점에서 모델링할 때는, main이 두 객체의 관계를 모두 알고 조율하는 것보다 손님 객체가 자신의 책임 안에서 바리스타에게 요청을 보내는 구조가 더 자연스럽다.
그 이유는 다음과 같다.
main이 직접 조율하면, “손님은 커피가 필요하고 그 요청은 바리스타에게 가야 한다"는 도메인 규칙을main이 대신 떠안게 된다.- 그러면 Customer가 가진 “주문한다” 는 책임이 약해질 수 있다.
- 협력이 복잡해질수록 외부 조율자가 모든 객체 관계를 알고 있어야 해서 변경 지점이 커지기 쉽다.
여기서 중요한 점은 실행의 첫 호출자와 도메인 모델에서 요청을 시작하는 객체를 구분하는 것이다.
- 실제 런타임에서는
main, 컨트롤러, 서비스, 테스트 코드가 첫 호출자일 수 있다. - 하지만 이 시나리오의 도메인 모델에서는 손님이 커피를 원해서 바리스타에게 요청한다고 해석하는 편이 자연스럽다.
핵심: 메시지의 이름은 메시지를 ‘받는’ 객체의 책임을 기준으로 결정된다#
손님이 바리스타에게 메시지를 보낼 때, 그 메시지의 이름(makeCoffee)은
손님이 마음대로 정하는 것이 아니다.
그것은 바리스타가 외부에 공개해 둔 연산 중 하나를 호출하는 것에 가깝다.
Oracle Java 문서는 객체의 메서드가 바깥 세계와 상호작용하는 주된 수단이라고 설명하고, 인터페이스는 클래스와 바깥 세계 사이의 계약이라고 설명한다.
즉, 메시지 이름은 보통 받는 객체가 어떤 책임을 외부에 약속했는가를 기준으로 잡는 편이 자연스럽다.
바리스타 객체의 입장에서 자신의 핵심 책임을 생각해보자#
- 바리스타의 가장 본질적인 책임은 ‘커피를 준비하는 것’ 이다.
- 따라서 바리스타는 그 책임을 잘 드러내는 이름으로 기능을 외부에 공개하는 편이 자연스럽다.
makeCoffee()createCoffee()provideCoffee()
반대로 takeOrder()는 바리스타가 처리 과정 중 일부로 가질 수는 있어도,
이 예시에서 바리스타의 핵심 결과 책임을 가장 잘 드러내는 이름이라고 보기는 어렵다.
손님은 ‘주문 접수’ 자체를 원하는 것이 아니라, 그 결과인 만들어진 커피를 원한다.
그래서 손님은 바리스타의 핵심 기능인 makeCoffee()를 요청(Request) 한다고 보는 편이 자연스럽다.
관점의 차이: ‘주문’과 ‘제조’#
| 관점 | 설명 | 코드 표현 |
|---|---|---|
| 손님의 관점 | 손님은 ‘주문’이라는 행동을 한다. 목적은 커피를 얻는 것이다. | customer.orderCoffee(barista, menuItem) |
| 바리스타의 관점 | 바리스타는 외부의 요청을 받아 ‘커피를 제조’하는 책임을 수행한다. | public Coffee makeCoffee(MenuItem item) |
즉, 손님의 orderCoffee(...) 메서드 내부에서 바리스타의 makeCoffee(...)를 호출하는 흐름이 된다.
public class Customer {
public void orderCoffee(Barista barista, String coffeeName) {
System.out.println("손님: '" + coffeeName + "' 한 잔 주세요.");
barista.makeCoffee(coffeeName);
}
}
public class Barista {
public Coffee makeCoffee(String coffeeName) {
System.out.println("바리스타: 네, '" + coffeeName + "' 만들겠습니다.");
return new Coffee(coffeeName);
}
}“묻지 말고 시켜라 (Tell, Don’t Ask)” 관점#
이 개념은 객체지향에서 자주 언급되는 “Tell, Don’t Ask” 가이드와도 연결해 볼 수 있다.
❌ 상태를 묻고 외부에서 판단하는 방식#
if (barista.isAvailable()) {
barista.takeThisOrder(order);
}- 호출자가 바리스타의 상태를 더 많이 알아야 한다.
- 다음 행동을 호출자 쪽에서 조합하게 되므로, 판단 책임이 바깥으로 새기 쉽다.
물론 isAvailable() 같은 메서드가 항상 잘못된 것은 아니다.
다만 호출자가 수신 객체의 내부 사정을 많이 알아야 다음 행동을 결정할 수 있다면, 그만큼 결합도는 커지기 쉽다.
✅ 원하는 바를 직접 요청하는 방식#
barista.makeCoffee(order);- 호출자는 원하는 결과를 요청한다.
- 구체적인 처리 과정과 내부 판단은 바리스타가 책임진다.
이 방식이 항상 정답이라고 할 수는 없지만, 이 예시에서는 캡슐화와 책임 분리에 더 잘 맞는다.
최종 결론 요약#
- 메시지(메서드)의 이름은 보통 ‘받는 객체’의 책임을 가장 잘 나타내는 쪽으로 정해진다.
- 이 예시에서 바리스타의 핵심 책임은 ‘제조’이므로, 공개 인터페이스로
makeCoffee()를 두는 해석이 자연스럽다. - 손님은 바리스타의 공개된 기능인
makeCoffee()를 요청하는 것이다.
이는 명령이라기보다, 객체 간 협력에서의 요청으로 이해하는 편이 정확하다. barista.takeOrderFrom(customer)같은 구조는 구체 타입 의존과 책임 혼합을 키우기 쉬워서, 보통은 손님이 요청을 보내는 방향이 더 읽기 쉽고 변경에도 유리하다.
이처럼 메시지 하나를 정할 때도 각 객체의 역할과 책임을 함께 고민하는 것이
좋은 객체지향 설계를 만드는 데 중요하다.
참고 자료#
- 우아한형제들 기술 블로그 — 객체지향과 메시지: https://techblog.woowahan.com/2502/
- GNU Smalltalk User’s Guide — Overview: https://www.gnu.org/software/smalltalk/manual/html_node/Overview.html
- Oracle Java Tutorials — What Is an Object?: https://docs.oracle.com/javase/tutorial/java/concepts/object.html
- Oracle Java Tutorials — What Is an Interface?: https://docs.oracle.com/javase/tutorial/java/concepts/interface.html
- Oracle Java Tutorials — Defining an Interface: https://docs.oracle.com/javase/tutorial/java/IandI/interfaceDef.html
