커맨드, 어댑터, 퍼사드 패턴
- 3.1 IoT 만능 리모컨
- 3.2 커맨드 패턴의 구조
- 3.3 작업 취소(Undo) 기능
- 3.4 매크로 커맨드
- 3.5 어댑터 패턴
- 3.6 퍼사드 패턴
- 3.7 최소 지식 원칙(데메테르 법칙)
요청을 객체로 만들고, 인터페이스를 변환하고, 복잡함을 감추는 것 -- 객체 사이의 소통 방식을 설계하는 세 가지 패턴이야.
IoT 만능 리모컨을 만든다고 해보자. 7개 슬롯에 조명, 선풍기, 스테레오, 차고 문을 연결해야 하는데, 각 장치의 API가 전부 달라. 조명은 on()/off(), 차고 문은 up()/down()/stop(), 스테레오는 on()/setCd()/setVolume(). 리모컨이 이 모든 API를 알아야 한다면, 장치가 바뀔 때마다 리모컨을 수정해야 하잖아. 요청하는 쪽과 수행하는 쪽을 분리해야 해.
**커맨드 패턴(Command Pattern)**은 요청을 execute() 메소드를 가진 객체로 캡슐화해. LightOnCommand는 내부에 Light를 들고 있고, execute()가 호출되면 light.on()을 위임하는 거지. 리모컨(Invoker)은 Command 인터페이스만 알고, 뒤에서 뭐가 실행되는지 전혀 몰라. 이 구조의 킬러 기능은 Undo야. Command에 undo() 메소드를 추가하면, LightOnCommand의 undo()는 light.off()를 호출하면 돼. 마지막 실행 커맨드를 저장해두면 한 단계 취소, 히스토리 스택을 쌓으면 여러 단계 취소가 가능해져. 더 나아가면 매크로 커맨드로 여러 커맨드를 배열에 담아 한 번에 실행할 수도 있고, 작업 큐에 넣어서 비동기로 처리할 수도 있어. 커맨드가 그냥 execute()를 가진 객체이기 때문에, 하나든 여러 개든 큐에 넣든 로그에 기록하든 똑같이 다룰 수 있는 거야.
커맨드 패턴이 요청을 캡슐화하는 거였다면, 이번엔 인터페이스 자체가 안 맞는 문제를 다뤄보자. 한국 플러그를 미국 콘센트에 꽂으려면 어댑터가 필요하듯, 소프트웨어에서도 기존 클래스의 인터페이스가 클라이언트 기대와 다를 때 변환기가 필요해. **어댑터 패턴(Adapter Pattern)**이 그거야. Turkey를 Duck 자리에 쓰고 싶으면, TurkeyAdapter가 Duck 인터페이스를 구현하면서 내부적으로 Turkey에 위임하는 거지. 클라이언트는 Duck이라고 생각하고 쓰지만, 실제로는 Turkey가 일하는 구조야.
반면 **퍼사드 패턴(Facade Pattern)**은 인터페이스를 변환하는 게 아니라 단순화하는 거야. 홈시어터에서 영화 한 편 보려고 팝콘 기계 켜고, 조명 어둡게 하고, 스크린 내리고, 프로젝터 켜고... 8단계를 거치는 대신, HomeTheaterFacade의 watchMovie() 하나만 호출하면 끝. 퍼사드는 서브시스템의 직접 사용을 막지 않아 — 편한 인터페이스를 제공할 뿐이지. 이 두 패턴 뒤에 깔린 원칙이 **최소 지식 원칙(데메테르 법칙)**이야. station.getThermometer().getTemperature() 이런 식으로 "친구의 친구"한테 말 거는 건 안 돼. station.getTemperature()로 바꾸면 중간 객체에 대한 의존이 사라지거든. 결국 어댑터든 퍼사드든, 클라이언트가 알아야 할 것을 최소한으로 줄이는 게 목적이야.
정리
3장 읽고 기억할 거 세 가지:
- 커맨드 패턴은 요청을 객체로 캡슐화해 — Undo, 매크로, 작업 큐 같은 확장이 자연스럽게 따라와. 요청하는 쪽과 수행하는 쪽을 완전히 분리하는 거야.
- 어댑터는 인터페이스를 변환하고, 퍼사드는 단순화해 — 둘 다 클라이언트가 알아야 할 것을 줄여주지만 목적이 달라.
- 최소 지식 원칙 — "친구의 친구"와 직접 대화하지 마. 상호작용을 가까운 친구로 제한하면 결합도가 낮아져.