컴파일러를 하나 만든다고 하자. 소스 코드를 실행 가능한 형태로 바꾸려면 스캐너가 문자열을 토큰으로 쪼개고, 파서가 토큰을 구문 트리로 엮고, 의미 분석기가 타입을 확인하고, 코드 생성기가 바이트코드를 뽑고, 최적화기가 그것을 다듬는다. 이 단계들은 저마다 독립된 클래스로 잘 나뉘어 있다 — Scanner, Parser, SemanticAnalyzer, CodeGenerator, Optimizer. 좋은 분할이다. 각 단계를 따로 테스트하고 따로 교체할 수 있다.

문제는 이 컴파일러를 그냥 쓰고 싶은 사람에게서 터진다. 대부분의 사용자는 “이 파일 하나 컴파일해 줘”가 전부다. 그런데 그러려면 Scanner를 만들고, 그 결과를 Parser에 넘기고, 다시 SemanticAnalyzer를 거쳐, CodeGenerator를 호출하고, 마지막에 Optimizer를 붙이는 순서를 클라이언트가 직접 알아야 한다. 잘 나눈 대가로, 흔한 일 하나가 다섯 부품의 조립 절차가 되어 버렸다. 퍼사드(Facade)는 이 서브시스템 앞에 문 하나를 세운다 — Compiler.compile(source) 한 줄이면 안에서 다섯 단계가 알아서 돈다.

무엇이 문제인가 — 동기

같은 증상을 홈시어터로 옮기면 더 생생하다. 영화 한 편을 보려고 소파에 앉았다. 그런데 이 시스템은 앰프, 튜너, DVD 플레이어, 프로젝터, 스크린, 조명, 팝콘 기계가 제각각 객체로 나뉘어 있다. “영화 봐”라는 한마디를 코드로 옮기면 이렇게 된다.

popper.on();
popper.pop();
lights.dim(10);
screen.down();
projector.on();
projector.setInput(dvd);
projector.wideScreenMode();
amp.on();
amp.setDvd(dvd);
amp.setSurroundSound();
amp.setVolume(5);
dvd.on();
dvd.play(movie);

여기서 세 가지가 동시에 아프다.

첫째, 클라이언트가 서브시스템의 부품을 낱낱이 안다. 영화 한 편 보겠다는 코드가 앰프의 볼륨 규약, 프로젝터의 입력 전환, 조명의 밝기 수치까지 전부 알고 있다. 이 앎은 곧 의존성이다. 일곱 개 클래스 중 어느 하나의 API가 바뀌면 — 앰프에 setStereoSound()가 추가되고 setSurroundSound()가 사라지면 — 이 코드가 함께 깨진다.

둘째, 호출 순서라는 암묵적 지식이 클라이언트로 샌다. 스크린을 내리기 전에 프로젝터를 켜면 안 되고, DVD를 재생하기 전에 앰프 입력을 맞춰야 한다. 이 순서는 서브시스템의 내부 사정인데, 그것을 아는 책임이 클라이언트에게 떠넘겨져 있다. 영화를 트는 자리가 열 군데면 이 순서가 열 번 복제되고, 순서가 하나 바뀌면 열 곳을 다 고쳐야 한다.

셋째, 끝내는 절차는 더 지저분하다. 영화가 끝나면 위 열네 줄을 대략 역순으로 되짚어 꺼야 한다. 켜는 코드와 끄는 코드가 서로 짝이 맞는지를 클라이언트가 손으로 지켜야 한다. 서브시스템이 잘 나뉜 것과, 그것을 쓰기 쉬운 것은 전혀 다른 문제다 — 분할은 부품을 만들 뿐, 흔한 사용법을 만들어 주지 않는다.

판단 기준: 서브시스템을 쓰는 클라이언트 코드에서 “부품 여럿을 정해진 순서로 엮는 똑같은 절차”가 반복되면 퍼사드를 의심한다. 함정: 부품이 여러 개라는 사실 자체는 문제가 아니다 — 그 부품들을 클라이언트가 직접 조립하는 절차가 여기저기 복제될 때만 퍼사드가 값을 한다.

패턴의 구조 — 누가 무엇을 하나

퍼사드의 등장인물은 놀랄 만큼 단순하다. 새 계층 하나를 얇게 끼우는 게 전부다.

Facade(퍼사드). 서브시스템 앞에 세우는 통합 창구다. 클라이언트가 원하는 흔한 작업을 굵은 단위의 메서드로 제공한다 — watchMovie(), endMovie(). 이 메서드 안에서 서브시스템의 여러 부품을 올바른 순서로 호출한다. 퍼사드는 새 기능을 만들지 않는다. 이미 있는 부품들에게 일을 떠넘길 뿐이다. 그래서 퍼사드 자신은 얇다 — 로직이 아니라 조율(orchestration)만 담는다.

Subsystem classes(서브시스템 클래스들). 실제 일을 하는 부품들이다(Amplifier, Projector, Dvd…). 이들은 퍼사드의 존재를 모른다. 자기 일만 할 뿐, 누가 자기를 조율하는지 알지 못한다. 이 무지가 중요하다 — 서브시스템이 퍼사드를 참조하기 시작하면 의존이 거꾸로 흘러 계층이 엉킨다.

Client(클라이언트). 이제 퍼사드하고만 대화한다. homeTheater.watchMovie(movie) 한 줄. 부품이 몇 개인지, 순서가 어떤지 알 필요가 없다.

여기서 퍼사드의 성격을 규정하는 결정적 원칙이 하나 있다 — 퍼사드는 문을 열어 줄 뿐, 다른 문을 잠그지 않는다. 대부분의 클라이언트는 watchMovie() 한 줄로 족하다. 그러나 어떤 클라이언트는 “볼륨만 3으로 하고 싶다”처럼 세밀한 제어가 필요하다. 이때 퍼사드는 그 클라이언트가 amp.setVolume(3)으로 하위 부품에 직접 접근하는 것을 막지 않는다. 퍼사드는 흔한 길을 넓게 포장한 대로(大路)이지, 유일한 통로가 아니다. 이 점이 퍼사드와 감싸서 가려 버리는 패턴들을 가르는 핵심이다.

판단 기준: 퍼사드를 설계할 때 “이 창구로 90%의 사용을 덮되, 나머지 10%는 하위로 직접 내려가게 열어 둔다”가 되면 옳다. 함정: 서브시스템 접근을 퍼사드로만 강제하고 하위 부품을 전부 숨겨 버리면, 세밀한 제어가 필요한 클라이언트가 막혀 퍼사드에 온갖 메서드를 덧붙이기 시작한다 — 그 순간 퍼사드는 만능 신(God) 객체로 비대해진다.

코드로 — 패턴이 자라나는 과정

클라이언트가 홈시어터 부품을 직접 조립하던 데서 출발해, 통합 창구를 세우고, 그 창구가 다른 문을 막지 않음을 확인하는 순서로 따라간다.

Refactoring Step 클라이언트가 부품을 직접 조립한다 — 순서 지식이 밖으로 샌다
1/4
public class MovieNight {    public void start(Amplifier amp, Tuner tuner, Dvd dvd,                      Projector projector, Screen screen,                      TheaterLights lights, PopcornPopper popper, Movie movie) {        // "영화 봐" 한마디가 열네 줄의 조립 절차가 된다.        popper.on();        popper.pop();        lights.dim(10);        screen.down();        projector.on();        projector.setInput(dvd);        projector.wideScreenMode();        amp.on();        amp.setDvd(dvd);        amp.setSurroundSound();        amp.setVolume(5);        dvd.on();        dvd.play(movie);    }}// 클라이언트가 일곱 부품의 API와 켜는 순서를 전부 안다. 이 코드는// 영화를 트는 자리마다 복제되고, 부품 하나가 바뀌면 그 모든 자리가 깨진다.

세 번째 스텝에서 클라이언트의 의존이 일곱에서 하나로 줄어든 것이 퍼사드가 사는 값이다. 하지만 마지막 스텝을 빼먹으면 퍼사드를 오해한다. 퍼사드는 서브시스템을 감춰 없애는 것이 아니라, 흔한 사용에 지름길을 낸 것이다. Audiophile처럼 세밀한 제어가 필요한 클라이언트는 여전히 부품에 직접 손댈 수 있다. 만약 이 길을 막았다면, 퍼사드는 setStereoSound까지 대신 노출하려고 메서드를 계속 늘려야 했을 것이다 — 그 길이 곧 비대해지는 퍼사드로 가는 함정이다.

판단 기준: 퍼사드를 세운 뒤 “흔한 작업은 창구로, 특수한 작업은 하위로”라는 두 갈래가 자연스럽게 공존하면 제대로 앉은 것이다. 함정: 퍼사드가 서브시스템의 거의 모든 메서드를 1:1로 다시 노출하고 있다면, 그건 통합 창구가 아니라 얇은 껍데기 위임층이다 — 아무것도 단순화하지 못하면서 계층만 하나 늘렸다.

결과 — 무엇을 얻고 무엇을 내주나

퍼사드가 사는 값은 결합의 감소와 사용의 단순화다. 클라이언트는 서브시스템의 부품 개수도, 이름도, 호출 순서도 몰라도 된다. 알아야 할 것이 일곱에서 하나로 줄었으니, 서브시스템 내부가 리팩터링되어도 — 부품이 쪼개지거나 합쳐지거나 API가 바뀌어도 — 그 파급이 퍼사드 안에 갇힌다. 클라이언트 코드는 흔들리지 않는다. 서브시스템과 클라이언트가 퍼사드를 사이에 두고 느슨하게 결합되는 것이다. 게다가 순서·정합 같은 암묵적 지식이 퍼사드 한 곳에 모여 이름을 얻는다(watchMovie가 곧 “영화를 트는 올바른 절차”의 문서다).

대가는 세 가지다. 첫째, 계층이 하나 는다. 클라이언트와 서브시스템 사이에 간접 호출 한 겹이 끼므로, 아주 단순한 서브시스템이라면 이 한 겹은 이득 없는 껍데기다. 둘째, 퍼사드가 비대해질 위험이 상존한다. 클라이언트들이 저마다 조금씩 다른 편의 메서드를 요구하기 시작하면 — watchMovieInFrench(), watchMovieWithSubtitles(), watchMovieButKeepLightsOn() — 퍼사드에 메서드가 우수수 붙는다. 얇아야 할 조율층이 온갖 변형을 다 아는 만능 객체로 부풀고, 이 하나가 바뀌면 모두가 흔들리는 새 병목이 된다. 셋째, 잘못 만들면 서브시스템을 가둔다. 퍼사드를 유일한 통로로 강제하고 하위 접근을 막으면, 세밀한 제어가 필요한 클라이언트가 갈 곳을 잃고 퍼사드를 더 비대하게 만든다.

운영비용 렌즈로 보면 순이득의 조건이 분명하다. 서브시스템이 여러 부품으로 복잡하고, 그것을 쓰는 클라이언트가 여럿이며, 흔한 사용 패턴이 있을 때 퍼사드는 크게 남는다 — 조립 절차의 복제를 없애고 결합을 한 점으로 모으기 때문이다. 반대로 부품이 한둘뿐이거나 사용처가 하나뿐이라면, 퍼사드는 간접성만 더하고 이득은 쓰이지 않는다.

판단 기준: 얻는 것(결합 감소·순서 지식의 캡슐화·리팩터링 격리)과 내주는 것(계층 추가·비대화 위험)을 세어 순이득이 양수일 때만 퍼사드를 앉힌다. 함정: 편의 메서드 요청이 올 때마다 퍼사드에 그대로 추가하면, 통합 창구가 요구사항의 쓰레기통이 된다 — 변형이 많아지면 그건 퍼사드가 아니라 새 서브시스템이 필요하다는 신호다.

언제 쓰고, 언제 과용인가

퍼사드가 정확히 맞는 자리는 이렇다. 복잡한 서브시스템에 간단한 기본 진입점이 필요할 때(라이브러리의 facade 패키지, javax.faces의 진입 API가 그 이름을 그대로 쓴다). 서브시스템과 클라이언트 사이의 결합을 낮춰 둘을 독립적으로 진화시키고 싶을 때. 계층화된 시스템에서 각 계층의 진입점을 하나로 정해 계층 간 대화를 그 문으로만 흐르게 하고 싶을 때. 자바 표준 라이브러리에서 java.net.URLopenStream()이 좋은 예다 — 그 뒤에서 프로토콜 핸들러, 커넥션, 스트림이 조립되지만 사용자는 문 하나만 두드린다.

과용의 냄새는 반대편에서 난다. 서브시스템이 단순한데 “혹시 복잡해질까 봐” 미리 퍼사드를 두면, 그건 오지 않을 복잡성에 거는 보험이다. 클라이언트가 어차피 하나뿐이라면 그 클라이언트가 곧 창구이므로 별도 퍼사드는 계층만 는다. 가장 흔한 과용은 앞서 본 비대화 — 퍼사드가 서브시스템의 거의 모든 메서드를 위임하며 두꺼워지면, 통합의 이득은 사라지고 병목만 남는다.

여기서 이웃 패턴과의 경계를 그어 둘 필요가 있다. [중재자]는 퍼사드처럼 여럿 사이의 소통을 하나로 모으는 사촌이다. 갈라지는 것은 방향과 앎이다. 퍼사드는 서브시스템 부품들이 자기를 모르는 채, 단지 그들 위에 편의 창구를 얹어 클라이언트→서브시스템의 단방향 진입을 단순화한다. 중재자는 동료 객체들이 서로 직접 말하지 못하게 하고 모든 상호작용을 중재자를 거치도록 강제한다 — 동료들이 중재자를 알고, 소통이 중재자를 통해서만 오간다. 퍼사드는 열어 주는 문이고, 중재자는 반드시 거쳐야 하는 교환대다. [어댑터]와도 헷갈리기 쉽다. 어댑터는 하나의 기존 인터페이스를 클라이언트가 원하는 다른 인터페이스로 바꾼다 — 목적은 인터페이스 변환이다. 퍼사드는 여러 부품 앞에 새로운 간결한 인터페이스를 얹는다 — 목적은 단순화다. 하나를 다른 모양으로 맞추면 어댑터, 여럿을 한 문으로 모으면 퍼사드다. 그리고 추상팩토리(Abstract Factory)는 서브시스템의 객체들을 어떻게 생성하는지를 감추는 데 자주 퍼사드와 짝을 이룬다 — 퍼사드가 서브시스템 사용을 숨긴다면, 추상 팩토리는 그 부품들을 만드는 방법을 숨겨 클라이언트가 구체 클래스를 몰라도 되게 한다.

판단 기준: 여러 객체 사이의 소통을 모으되 그들이 서로를 직접 몰라야 하면 중재자, 그저 복잡한 서브시스템에 쉬운 진입점만 얹으면 퍼사드다 — “부품들이 창구를 아는가, 모르는가”로 가른다. 함정: 퍼사드에 상태와 규칙이 쌓이고 부품들이 그것을 참조하기 시작하면, 이름만 퍼사드일 뿐 실체는 중재자다 — 그 전환은 우연히 일어나므로, 퍼사드가 얇은 조율층으로 남아 있는지 주기적으로 확인해야 한다.

다음으로 플라이웨이트