포스트

Facade Pattern

복잡한 서브시스템 앞에 단순한 진입점을 둬 클라이언트의 결합도를 낮추는 패턴. Adapter는 모양 변환, Facade는 단순화가 목적.

Facade Pattern

난이도 중급 · 선행 Adapter

한 줄 요약

여러 모듈을 호출해야 하는 복잡한 절차를 한 객체의 단순한 메서드 뒤로 숨긴다. 클라이언트는 그 한 메서드만 알면 된다.

어떤 문제를 푸는가

쇼핑몰에서 주문 한 건을 처리하려면 여러 서비스를 차례로 호출해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void placeOrder(const Cart& cart, const User& user) {
    if (!inventory.reserve(cart.items)) throw OutOfStock();
    if (!payment.charge(user, cart.total())) {
        inventory.release(cart.items); // 결제 실패 시 재고 복구
        throw PaymentFailed();
    }
    try {
        shipping.schedule(cart, user.address);
    } catch (...) {
        payment.refund(user, cart.total());
        inventory.release(cart.items);
        throw;
    }
    notification.send(user, "주문 완료");
    analytics.record("order", cart);
}

문제:

  • 클라이언트가 5개 서비스를 모두 알고, 호출 순서·실패 시 보상까지 책임진다.
  • 같은 주문 처리를 다른 곳(모바일 API, 관리자 콘솔)에서도 호출하면 그곳에도 이 코드를 복제하게 된다.
  • 서브시스템 하나의 시그니처가 바뀌면 모든 호출 사이트가 깨진다.

패턴 적용 후

복잡한 절차를 OrderService(Facade) 한 곳에 모아 단순한 진입점만 노출한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include <iostream>
#include <stdexcept>
#include <string>

class InventoryService {
public:
    bool reserve(const std::string& item) { std::cout << item << " 재고 예약\n"; return true; }
    void release(const std::string& item) { std::cout << item << " 재고 복구\n"; }
};

class PaymentService {
public:
    bool charge(const std::string& user, int amount) {
        std::cout << user << "에게 " << amount << "원 결제\n"; return true;
    }
    void refund(const std::string& user, int amount) {
        std::cout << user << "에게 " << amount << "원 환불\n";
    }
};

class ShippingService {
public:
    void schedule(const std::string& item, const std::string& addr) {
        std::cout << item << " → " << addr << " 배송 예약\n";
    }
};

class NotificationService {
public:
    void send(const std::string& user, const std::string& msg) {
        std::cout << "[알림→" << user << "] " << msg << "\n";
    }
};

// Facade: 복잡한 협력을 한 메서드로 노출
class OrderService {
    InventoryService inventory;
    PaymentService payment;
    ShippingService shipping;
    NotificationService notification;
public:
    void placeOrder(const std::string& user, const std::string& item,
                    int amount, const std::string& addr) {
        if (!inventory.reserve(item)) throw std::runtime_error("재고 없음");

        if (!payment.charge(user, amount)) {
            inventory.release(item);
            throw std::runtime_error("결제 실패");
        }

        try {
            shipping.schedule(item, addr);
        } catch (...) {
            payment.refund(user, amount);
            inventory.release(item);
            throw;
        }

        notification.send(user, "주문 완료");
    }
};

int main() {
    OrderService service;
    service.placeOrder("alice", "노트북", 1500000, "서울시 종로구");
}

달라진 점:

  • 클라이언트는 OrderService::placeOrder 하나만 안다.
  • 호출 순서·보상 로직이 한 곳에 모인다.
  • 서브시스템이 바뀌어도 Facade만 수정. 클라이언트 무손상.

구조

1
2
3
4
5
Client ──▶ Facade
              │
              ├──▶ SubsystemA
              ├──▶ SubsystemB
              └──▶ SubsystemC
  • Facade: 단순한 진입점 (OrderService)
  • Subsystem: 실제 일을 하는 모듈들 (InventoryService, PaymentService, …)

핵심은 Facade가 서브시스템의 한 가지 사용 방식을 노출한다는 점이다. 서브시스템을 직접 쓰는 것도 여전히 가능하다 — Facade는 길을 막는 게 아니라 지름길을 제공한다.

실전 사례

  • jQuery $(selector): DOM API 복잡성(브라우저 호환성 포함)을 단순한 한 함수 뒤로 숨김.
  • Spring JdbcTemplate: JDBC의 5단계(Connection → Statement → ResultSet → 닫기 → 예외 변환)를 한 메서드로.
  • HikariCP getConnection(): 커넥션 풀의 복잡한 자원 관리를 단순한 호출로.
  • Compiler 프론트엔드: compile(source) 한 함수 뒤에 lexer/parser/typecheck/codegen이 숨음.
  • payment SDK: 토스/스트라이프의 단일 Charge.create(...)가 내부적으로 카드 검증·3DS·라우팅을 다 처리.

Adapter / Mediator와의 차이

 FacadeAdapterMediator
의도복잡한 서브시스템 단순화인터페이스 모양 변환객체 간 통신 중재
새 인터페이스인가그렇다 (새 진입점)아니다 (기존 Target 인터페이스에 맞춤)그렇다 (객체들이 Mediator에만 의존)
관계 방향외부 → 시스템 (단방향)외부 → 시스템시스템 내부 객체끼리

Facade는 “여러 모듈 → 단순화”이고 Adapter는 “한 모듈 → 모양 변환”. 헷갈리면 의도를 보면 된다.

안티패턴 / 주의

  • Facade가 서브시스템 메서드를 단순 forwarding만 한다: 가치 없음. 단순화·정책 결정·보상 로직처럼 의미 있는 일을 해야 Facade.
  • Facade가 비대해진다 (God Object): 단일 Facade가 50개 메서드를 갖는다면 책임 분리가 잘못된 신호. 도메인별로 쪼개라 (OrderService, RefundService, …).
  • Facade로 서브시스템 직접 접근을 막지 마라: 특수한 경우엔 서브시스템을 직접 써야 한다. 보호 프록시가 아니다.
  • 순환 의존: Facade가 서브시스템을 호출하는데, 서브시스템이 다시 Facade를 호출하면 위험. 단방향을 유지하라.

스스로 점검

1. Facade가 서브시스템 메서드를 단순 forwarding만 하면 어떤 점이 문제?

가치가 없다. 단순화·정책 결정·보상 로직(트랜잭션 롤백 같은) 같은 의미 있는 일을 해야 Facade. 메서드 1:1 매핑이면 한 단계 더 들어가는 비용만 생긴다.

2. OrderService Facade가 메서드 50개로 부풀었다면?

God Object 신호. 도메인 단위로 쪼개야 한다 (RefundService, ShippingService, InventoryService 등). 단일 Facade가 모든 걸 떠안으면 그 자체가 결합점.

3. Facade를 두면 서브시스템 직접 호출을 막아야 하나?

아니다. Facade는 “지름길”을 제공할 뿐, “길을 막는 보안 장치”가 아니다. 특수한 경우 서브시스템 직접 호출도 가능해야 한다. 그건 보호 프록시의 역할.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.