C++ Facade 패턴: 복잡한 서브시스템 앞에 단순한 진입점 두기와 Pimpl·CRTP 변형

이 글의 핵심

Facade는 여러 서브시스템의 호출 순서와 초기화·정리 규칙을 한 클래스에 가두는 패턴입니다. 비디오·DB·게임 엔진 예제로 구조를 보고, 트랜잭션 경계를 숨길 때의 함정, 부분 초기화 실패와 역순 정리, Pimpl 컴파일 방화벽, 가상 디스패치 비용과 템플릿·CRTP 대안을 정리합니다.

Facade 패턴이란? 왜 필요한가

여러 서브시스템을 한 얼굴로 묶는 흐름은 구조 패턴 시리즈와 종합 가이드에서 다른 패턴과 대비해 볼 수 있습니다.

문제 시나리오: 복잡한 서브시스템

문제: 비디오 재생을 위해 여러 클래스를 직접 조작해야 합니다.

// 나쁜 설계: 클라이언트가 모든 세부사항을 알아야 함
int main() {
    VideoFile video("movie.mp4");
    CodecFactory factory;
    Codec* codec = factory.extract(video);
    AudioMixer mixer;
    mixer.fix(video);
    VideoConverter converter;
    converter.convert(video, codec);
    // 5개 클래스를 직접 조작...
}

해결: Facade 패턴은 복잡한 서브시스템을 하나의 간단한 인터페이스로 감쌉니다.

// 좋은 설계: Facade
// 타입 정의
class VideoPlayer {
    VideoFile video_;
    CodecFactory factory_;
    AudioMixer mixer_;
    VideoConverter converter_;
public:
    void play(const std::string& filename) {
        video_.load(filename);
        auto codec = factory_.extract(video_);   // unique_ptr<Codec>
        mixer_.fix(video_);
        converter_.convert(video_, codec.get());
        // 내부 복잡성 숨김
    }
};
int main() {
    VideoPlayer player;
    player.play("movie.mp4");  // 간단!
}
flowchart LR
    client[Client]
    facade["Facade<br/>(VideoPlayer)"]
    subsystem1["Subsystem1<br/>(VideoFile)"]
    subsystem2["Subsystem2<br/>(Codec)"]
    subsystem3["Subsystem3<br/>(AudioMixer)"]
    
    client --> facade
    facade --> subsystem1
    facade --> subsystem2
    facade --> subsystem3

Facade가 줄여 주는 것은 코드 줄 수보다 지식의 양입니다. 첫 번째 코드에서 클라이언트는 “코덱을 먼저 추출해야 하고, 오디오 싱크는 변환 전에 맞춰야 한다”는 호출 순서 규칙을 알고 있어야 했습니다. 이런 규칙이 여러 호출부에 복사되어 있으면, 서브시스템의 순서 요구가 바뀔 때마다 모든 호출부를 찾아 고쳐야 하고 한 곳이라도 놓치면 그곳만 잘못 동작합니다. Facade는 이 규칙을 한 곳에 가두어, 서브시스템이 바뀌어도 수정 범위가 Facade 내부로 끝나게 만듭니다. 반대로 서브시스템이 하나뿐이고 호출 순서 규칙도 없다면 Facade는 이름만 바꾼 위임 계층이 되어 오히려 코드를 읽는 단계만 늘립니다.


기본 구조

#include <iostream>
#include <string>
// 서브시스템 클래스들 (복잡한 내부)
class Parser {
public:
    void parse(const std::string& path) { 
        std::cout << "Parsing " << path << "...\n";
    }
};
class Validator {
public:
    bool validate() { 
        std::cout << "Validating...\n";
        return true; 
    }
};
class Compiler {
public:
    void compile() { 
        std::cout << "Compiling...\n";
    }
};
class Linker {
public:
    void link() {
        std::cout << "Linking...\n";
    }
};
// Facade: 하나의 진입점
class BuildPipeline {
    Parser parser_;
    Validator validator_;
    Compiler compiler_;
    Linker linker_;
public:
    bool build(const std::string& path) {
        std::cout << "=== Build Started ===\n";
        parser_.parse(path);
        if (!validator_.validate()) {
            std::cout << "Validation failed!\n";
            return false;
        }
        compiler_.compile();
        linker_.link();
        std::cout << "=== Build Complete ===\n";
        return true;
    }
};
int main() {
    BuildPipeline pipeline;
    if (pipeline.build("src/main.cpp"))
        std::cout << "✓ Build OK\n";
    return 0;
}

멀티미디어 시스템 예제

비디오 플레이어 Facade

#include <iostream>
#include <string>
#include <memory>
// 서브시스템: 복잡한 비디오 처리
class VideoFile {
    std::string filename_;
public:
    explicit VideoFile(std::string filename) : filename_(std::move(filename)) {
        std::cout << "Loading video: " << filename_ << '\n';
    }
    std::string getCodecType() const { return "MPEG4"; }
};
class Codec {
public:
    virtual ~Codec() = default;
    virtual void decode() = 0;
};
class MPEG4Codec : public Codec {
public:
    void decode() override { std::cout << "Decoding MPEG4...\n"; }
};
class H264Codec : public Codec {
public:
    void decode() override { std::cout << "Decoding H264...\n"; }
};
class CodecFactory {
public:
    static std::unique_ptr<Codec> extract(const VideoFile& file) {
        if (file.getCodecType() == "MPEG4")
            return std::make_unique<MPEG4Codec>();
        return std::make_unique<H264Codec>();
    }
};
class AudioMixer {
public:
    void fix(const VideoFile& file) {
        std::cout << "Fixing audio sync...\n";
    }
};
class VideoRenderer {
public:
    void render() {
        std::cout << "Rendering video...\n";
    }
};
// Facade: 간단한 인터페이스
class VideoPlayer {
public:
    void play(const std::string& filename) {
        std::cout << "=== Playing Video ===\n";
        VideoFile video(filename);
        auto codec = CodecFactory::extract(video);
        codec->decode();
        AudioMixer mixer;
        mixer.fix(video);
        VideoRenderer renderer;
        renderer.render();
        std::cout << "=== Playback Started ===\n";
    }
};
int main() {
    VideoPlayer player;
    player.play("movie.mp4");
    return 0;
}

핵심: 클라이언트는 play() 하나만 호출하면 됩니다.


데이터베이스 래퍼

복잡한 DB 연산을 단순화

#include <iostream>
#include <string>
#include <vector>
// 서브시스템: 저수준 DB 연산
class Connection {
public:
    void connect(const std::string& host) {
        std::cout << "Connecting to " << host << "...\n";
    }
    void disconnect() {
        std::cout << "Disconnecting...\n";
    }
};
class Query {
public:
    void prepare(const std::string& sql) {
        std::cout << "Preparing query: " << sql << '\n';
    }
    void execute() {
        std::cout << "Executing query...\n";
    }
};
class ResultSet {
public:
    std::vector<std::string> fetch() {
        return {"row1", "row2", "row3"};
    }
};
class Transaction {
public:
    void begin() { std::cout << "BEGIN TRANSACTION\n"; }
    void commit() { std::cout << "COMMIT\n"; }
    void rollback() { std::cout << "ROLLBACK\n"; }
};
// Facade: 간단한 DB 인터페이스
class Database {
    Connection conn_;
    Transaction trans_;
public:
    void connect(const std::string& host) {
        conn_.connect(host);
    }
    
    std::vector<std::string> query(const std::string& sql) {
        trans_.begin();
        try {
            Query q;
            q.prepare(sql);
            q.execute();
            ResultSet rs;
            auto results = rs.fetch();
            trans_.commit();
            return results;
        } catch (...) {
            trans_.rollback();
            throw;
        }
    }
    
    void disconnect() {
        conn_.disconnect();
    }
};
int main() {
    Database db;
    db.connect("localhost:5432");
    
    auto results = db.query("SELECT * FROM users");
    for (const auto& row : results)
        std::cout << "Row: " << row << '\n';
    
    db.disconnect();
    return 0;
}

핵심: 트랜잭션, 연결, 쿼리 준비 등 복잡한 과정을 query() 하나로 처리합니다.

이 예제는 편리함 뒤에 Facade 설계에서 가장 흔한 실수를 담고 있습니다. query()가 호출마다 트랜잭션을 열고 닫기 때문에, 호출자가 “계좌 A에서 출금하고 계좌 B에 입금”처럼 두 쿼리를 하나의 트랜잭션으로 묶을 방법이 없습니다. 두 번째 쿼리가 실패해도 첫 번째는 이미 커밋된 뒤입니다. 경계를 숨기는 것이 Facade의 목적이지만, 트랜잭션 경계처럼 호출자가 결정해야 하는 정책까지 숨기면 사용할 수 없는 API가 됩니다. 실무에서는 db.transaction([&](Tx& tx) { tx.execute(...); tx.execute(...); });처럼 경계를 호출자에게 열어 주되, 람다가 예외 없이 끝나면 커밋하고 예외가 나면 롤백하는 규칙만 Facade가 책임지는 형태가 많이 쓰입니다.

예외 처리 쪽에도 함정이 있습니다. 위 코드는 trans_.commit() 자체가 예외를 던지면 catch로 들어가 이미 실패한 커밋 뒤에 다시 rollback()을 호출합니다. 트랜잭션 상태를 추적하는 RAII 가드(소멸자에서 “커밋되지 않았다면 롤백”)를 쓰면 이런 경로를 빠뜨리지 않고, rollback() 자체가 실패할 때 소멸자에서 예외가 새어 나가지 않도록 처리하는 것도 한곳에서 해결됩니다.


Facade가 비대해지거나 우회될 때

문제 1: Facade가 너무 커짐

// ❌ 나쁜 예: God Object
class SystemFacade {
public:
    void doEverything() { /* 100줄 */ }
    void doMore() { /* 100줄 */ }
    // 20개 메서드...
};

해결: 여러 Facade로 분리하세요.

// ✅ 좋은 예: 책임 분리
class VideoFacade { /* 비디오 관련만 */ };
class AudioFacade { /* 오디오 관련만 */ };
class NetworkFacade { /* 네트워크 관련만 */ };

문제 2: 서브시스템 직접 접근

// ❌ 나쁜 예: Facade 우회
VideoFile video("movie.mp4");
Codec* codec = new MPEG4Codec();  // 직접 접근

해결: Facade만 사용하도록 서브시스템을 private/internal로 만드세요.

// ✅ 좋은 예: 서브시스템 숨김
namespace internal {
    class VideoFile { /* ... */ };
}
class VideoPlayer {  // public API
    internal::VideoFile video_;
};

문제 3: 모든 기능 노출 불가

// ❌ 문제: Facade가 고급 기능을 제공하지 않음
player.play("movie.mp4");  // OK
player.setSubtitle("en");  // 없음!

해결: 필요한 고급 기능은 Facade에 추가하거나, 서브시스템 접근자를 제공하세요.

// ✅ 해결 1: 메서드 추가
class VideoPlayer {
public:
    void play(const std::string& filename);
    void setSubtitle(const std::string& lang);  // 추가
};
// ✅ 해결 2: 서브시스템 접근자
class VideoPlayer {
public:
    VideoFile& getVideoFile() { return video_; }  // 고급 사용자용
};

해결 2는 신중하게 써야 합니다. 서브시스템 참조를 한 번 내주면 Facade가 지키던 불변식(예: “재생 중에는 파일을 바꿀 수 없다”)을 호출자가 우회할 수 있고, 그 참조의 수명은 Facade 객체에 묶여 있어 Facade보다 오래 보관하면 댕글링이 됩니다. 실무에서 흔한 절충은 일반 사용자를 위한 Facade와 별도로, 서브시스템을 직접 조합할 수 있는 저수준 API를 공식적으로 함께 공개하는 것입니다. Facade는 편의 계층일 뿐 유일한 입구일 필요는 없고, 두 계층을 명확히 나눠 문서화하는 편이 “숨겨진 탈출구”보다 유지보수하기 쉽습니다.


Pimpl과 컴파일 방화벽

Facade가 여러 서브시스템 헤더를 끌어오면 클라이언트 번역 단위 전체가 그래프에 묶여 재컴파일 폭이 커집니다. Pimpl(Pointer to implementation)은 공개 헤더에서 구체 타입 정의를 제거하며, 구현 세부는 .cpp에 두어 ABI·빌드 시간을 동시에 안정화하는 관용구입니다.

컴파일 방화벽이 하는 일

  • 공개 헤더에는 전방 선언과 불투명 포인터(std::unique_ptr<Impl> 등)만 둡니다.
  • 서브시스템·서드파티 헤더는 구현 파일 한정으로 include 하여, Facade를 수정해도 의존 그래프가 Facade 경계에서 끊깁니다.
  • std::unique_ptr<Impl>의 기본 삭제자는 delete하는 지점에서 Impl의 완전한 정의가 필요하므로, 소멸자를 .cpp에 정의(= default라도 구현 파일에서)해야 합니다. 헤더에서 암시적으로 생성되면 invalid application of 'sizeof' to incomplete type 'Impl' 같은 에러가 납니다.

최소 예시: Facade를 Pimpl로 감싸기

// PlayerFacade.h — 클라이언트는 이 헤더만 보면 됨
#pragma once
#include <memory>
#include <string>

class PlayerFacade {
public:
    PlayerFacade();
    ~PlayerFacade(); // Impl이 불완전 타입일 때 구현 파일에서 정의
    PlayerFacade(PlayerFacade&&) noexcept;
    PlayerFacade& operator=(PlayerFacade&&) noexcept;

    void play(const std::string& path);

    PlayerFacade(const PlayerFacade&) = delete;
    PlayerFacade& operator=(const PlayerFacade&) = delete;

private:
    struct Impl;
    std::unique_ptr<Impl> impl_;
};
// PlayerFacade.cpp — 무거운 서브시스템 헤더는 여기서만
#include "PlayerFacade.h"
#include "codec/CodecSubsystem.h"
#include "audio/AudioSubsystem.h"

struct PlayerFacade::Impl {
    CodecSubsystem codec_;
    AudioSubsystem audio_;
};

PlayerFacade::PlayerFacade() : impl_(std::make_unique<Impl>()) {}
PlayerFacade::~PlayerFacade() = default;
PlayerFacade::PlayerFacade(PlayerFacade&&) noexcept = default;
PlayerFacade& PlayerFacade::operator=(PlayerFacade&&) noexcept = default;

void PlayerFacade::play(const std::string& path) {
    impl_->codec_.open(path);
    impl_->audio_.route(impl_->codec_);
}

이동 연산을 = default로 두면 이동된 쪽 객체의 impl_은 nullptr이 됩니다. 이동 후에 원래 객체의 play()를 호출하면 널 포인터 역참조로 크래시하므로, 공개 메서드 앞에 assert(impl_)를 두거나 “이동된 객체는 소멸과 대입만 허용”한다고 문서화해야 합니다. 또 모든 호출이 힙에 있는 Impl을 거치므로 포인터 간접 참조 한 번과 할당 한 번이 추가되는데, Facade처럼 호출 빈도가 낮은 경계에서는 문제가 되지 않지만 작은 값 타입에 Pimpl을 쓰는 것은 대개 손해입니다.

실무 포인트: Facade가 “얇은 진입점”일수록 Pimpl 효과가 큽니다. 반대로 Facade가 템플릿 매개변수로 노출되면 Pimpl과 충돌할 수 있어(구체 타입이 헤더에 필요) 그 경우에는 아래 템플릿 Facade나 별도의 비템플릿 어댑터 계층을 고려합니다.


가상 디스패치 오버헤드 분석

Facade 뒤에 추상 인터페이스(런타임 다형)를 두면 교체 가능성은 높아지지만, 동적 디스패치 비용을 이해해야 합니다.

메커니즘 요약

  • 일반적인 구현은 가상 함수 테이블(vtable)을 통해 간접 호출합니다. 호출 지점은 종종 간접 분기(indirect branch)가 되어, CPU 분기 예측이 어려운 경로에서는 파이프라인 스톨이 늘 수 있습니다.
  • 인라인 불가: 가상 호출은 대부분 컴파일 시점에 대상 함수가 고정되지 않아, 최적화기가 본문을 합치기 어렵습니다(링크 타임 최적화나 final로 가능해지는 비가상화(devirtualization)는 예외).
  • 데이터 지역성: vtable·객체 레이아웃이 캐시 라인을 추가로 건드릴 수 있어, 핫 루프에서 누적됩니다.

언제 문제가 되는가

  • 프레임 예산이 빡센 게임·오디오·네트워크 폴링 루프처럼, Facade 한두 번 호출이 아니라 다형 호출이 내부에서 수천 번 반복될 때 체감됩니다.
  • 반대로 초기화·I/O 바운드 작업을 묶는 Facade라면 가상 호출 비용은 노이즈 수준인 경우가 많습니다.

설계 대응

  • 인터페이스를 얇게: 가상 함수는 “경계”에만 두며, 내부에서는 구체 타입·정적 바인딩으로 처리합니다.
  • final / 비가상 인터페이스: 계층 끝단을 final로 닫거나, Facade 자체는 비가상·인라인 친화 API로 유지합니다.
  • 정적 다형성: 자주 호출되는 경로는 템플릿 Facade나 CRTP로 옮겨 컴파일 타임에 바인딩합니다.

측정은 추측보다 우선합니다. 동일 시나리오에서 가상 인터페이스 버전과 구체 타입·템플릿 버전을 샘플링 프로파일러로 비교하면, Facade가 “정책 경계”인지 “핫 경로”인지 즉시 갈립니다.


템플릿 기반 Facade

정책을 템플릿 매개변수로 주입하면 Facade는 여전히 단일 진입점이지만, 런타임 다형 없이 서브시스템 조합을 바꿀 수 있습니다. 이는 GoF 문헌의 Facade와는 표기가 다르지만, C++에선 “형태는 Facade, 바인딩은 컴파일 타임”으로 불리는 실무 형태입니다.

정책 조합 예시

template <typename CodecPolicy, typename AudioPolicy>
class MediaFacade {
    CodecPolicy codec_;
    AudioPolicy audio_;
public:
    void play(const std::string& path) {
        codec_.open(path);
        audio_.sync(codec_);
    }
};

장점: 핫 루프에서 가상 호출을 제거하며, 불필요한 분기를 줄입니다. 단점: 바이너리 크기·컴파일 시간 증가, 공개 API가 헤더에 노출되어 Pimpl과 상충할 수 있습니다. 해결책은 비템플릿 Facade + 내부 템플릿 어댑터, 또는 명시적 인스턴스화를 .cpp에 모으는 방식입니다.

if constexpr로 단일 Facade 유지

C++17 이후에는 한 클래스 안에서 정책 특성에 따라 분기를 컴파일 타임에 제거할 수 있어, “하나의 play()” 표면을 유지하면서도 불필요한 코드를 배제할 수 있습니다.

template <typename Policy>
void runPipeline(Policy& p) {
    p.parse();
    if constexpr (requires { p.optimize(); }) {
        p.optimize(); // 해당 정책에만 존재할 때만 컴파일·생성
    }
    p.emit();
}

C++20의 requires가 없는 환경이라면 std::is_invocable·트레이트 특수화로 동일한 효과를 낼 수 있습니다.


CRTP(Curiously Recurring Template Pattern)로 정적 다형 Facade

CRTP는 기반 클래스가 Derived를 템플릿 인자로 받아, 컴파일 타임에 캐스팅해 확장점을 노출하는 패턴입니다. Facade가 “훅”이나 “커스터마이징 지점”을 제공해야 하지만 가상 함수를 피하고 싶을 때 자주 사용됩니다.

확장점을 CRTP로 열기

template <typename Derived>
class EngineFacadeBase {
protected:
    void afterInitHook() {
        static_cast<Derived*>(this)->onAfterInit(); // 정적 바인딩
    }
public:
    void initialize() {
        // ... 서브시스템 초기화 ...
        afterInitHook();
    }
};

class GameFacade : public EngineFacadeBase<GameFacade> {
    friend class EngineFacadeBase<GameFacade>;
    void onAfterInit() {
        // 프로젝트 전용 후처리
    }
};

주의: 잘못된 상속 계층(예: Derived가 아닌 타입으로 CRTP를 쓰면) 미정의 동작으로 이어집니다. 예를 들어 class OtherFacade : public EngineFacadeBase<GameFacade>처럼 복사·붙여넣기로 템플릿 인자를 잘못 적으면 컴파일은 되지만 static_cast가 엉뚱한 타입으로 변환합니다. 이를 막는 관용구는 기저 클래스의 생성자를 private으로 두고 friend Derived;를 선언하는 것입니다. 그러면 템플릿 인자와 실제 파생 클래스가 다를 때 기저 생성자에 접근할 수 없어 컴파일 에러가 납니다. 또 Derived가 onAfterInit()을 정의하지 않으면 'class GameFacade' has no member named 'onAfterInit' 에러가 기저 클래스 템플릿 안쪽 위치로 표시되므로, C++20이라면 콘셉트로 요구 사항을 명시해 두면 에러 메시지가 훨씬 읽기 쉬워집니다.

Facade와의 관계: 외부 클라이언트에게는 여전히 단일 Facade 타입(GameFacade)만 보이며, 내부 확장은 CRTP로 처리하므로 공개 API는 단순하게 유지할 수 있습니다.


프로덕션 리팩터링 패턴

라이브 시스템에서 Facade는 레거시를 감싸는 이음매(seam) 역할을 할 때가 많습니다. 아래는 현장에서 반복되는 안전한 이동 경로입니다.

Strangler Fig(교살 무화과): 점진적 래핑

새 구현을 한 번에 갈아끼우기 어렵다면, 호출 경로를 Facade 뒤로 옮겨가며 기존 모듈을 “말려 죽이는” 방식입니다. 초기에는 Facade가 단순 위임만 하다가, 기능 단위로 새 서브시스템으로 교체합니다. 플래그·라우팅 테이블로 일부 트래픽만 신규 경로로 보내며 검증합니다.

안티코럽션 레이어(Anti-Corruption Layer)

도메인 모델과 외부 SDK/레거시 API 사이에 Facade(또는 Adapter 묶음)를 두어 외부 용어·불변식이 내부로 스며드는 것을 막습니다. 이름은 다르지만, Facade가 번역 계층이 되는 전형적인 사례입니다.

테스트 이음매(seam)와 의존성 주입으로 Facade 검증

Facade 생성자에 추상 팩토리·인터페이스를 주입하면(런타임 다형), 프로덕션은 실제 서브시스템, 테스트는 페이크로 교체할 수 있습니다. 반대로 성능이 중요한 경로는 템플릿 매개변수로 테스트 더블을 주입하는 방식도 병행됩니다.

ABI와 배포 단위

동적 라이브러리 경계에 Facade를 둘 때는 Pimpl + 안정적인 C ABI 조합으로 공개 심볼을 최소화하는 경우가 많습니다. 헤더 변경이 배포물 전파를 줄이는지, 팀 규모와 링크 모델에 맞춰 선택합니다.

Facade와 함께 쓰는 구성 패턴

Singleton Facade

class Logger {
    Logger() = default;
public:
    static Logger& instance() {
        static Logger inst;
        return inst;
    }
    void log(const std::string& msg) {
        // 복잡한 로깅 시스템 감춤
        std::cout << "[LOG] " << msg << '\n';
    }
};
// 사용
Logger::instance().log("Application started");

전역 접근이 필요할 때만 제한적으로 쓰며, 테스트 결합도를 감안해 최근 코드베이스에서는 DI 가능한 로거 Facade를 선호하는 편입니다. 함수 안의 static 지역 변수는 C++11부터 초기화가 스레드 안전하게 한 번만 일어나지만, 소멸 순서는 여전히 함정입니다. 다른 전역 객체의 소멸자가 프로그램 종료 시점에 Logger::instance().log(...)를 부르면, Logger가 이미 소멸한 뒤일 수 있어 종료 직전에만 가끔 크래시하는 버그가 됩니다. 종료 시점까지 확실히 살아 있어야 한다면 의도적으로 소멸시키지 않는 방식(static Logger* inst = new Logger;)을 쓰기도 합니다.

Builder와 조합

class VideoPlayerBuilder {
    std::string codec_;
    bool subtitles_ = false;
public:
    VideoPlayerBuilder& setCodec(const std::string& c) { codec_ = c; return *this; }
    VideoPlayerBuilder& enableSubtitles() { subtitles_ = true; return *this; }
    VideoPlayer build() { return VideoPlayer(codec_, subtitles_); }
};
// 사용
auto player = VideoPlayerBuilder()
    .setCodec("H264")
    .enableSubtitles()
    .build();

Facade가 고정 시그니처로는 파라미터 폭발이 나면, 빌더로 단계적 구성을 노출하고 최종적으로는 단일 VideoPlayer Facade만 넘기는 식으로 정리합니다.


완전한 예제: 게임 엔진 초기화

#include <iostream>
#include <string>
// 서브시스템: 복잡한 게임 엔진 컴포넌트
class GraphicsEngine {
public:
    void init() { std::cout << "Graphics: Initializing OpenGL...\n"; }
    void setResolution(int w, int h) { 
        std::cout << "Graphics: Set resolution " << w << "x" << h << '\n'; 
    }
    void enableVSync() { std::cout << "Graphics: VSync enabled\n"; }
};
class AudioEngine {
public:
    void init() { std::cout << "Audio: Initializing OpenAL...\n"; }
    void setVolume(float v) { 
        std::cout << "Audio: Volume set to " << v << '\n'; 
    }
};
class PhysicsEngine {
public:
    void init() { std::cout << "Physics: Initializing Bullet...\n"; }
    void setGravity(float g) { 
        std::cout << "Physics: Gravity set to " << g << '\n'; 
    }
};
class InputManager {
public:
    void init() { std::cout << "Input: Initializing SDL...\n"; }
    void bindKey(const std::string& key, const std::string& action) {
        std::cout << "Input: Bind " << key << " -> " << action << '\n';
    }
};
class NetworkManager {
public:
    void init() { std::cout << "Network: Initializing sockets...\n"; }
    void connect(const std::string& server) {
        std::cout << "Network: Connecting to " << server << "...\n";
    }
};
// Facade: 게임 엔진 초기화를 하나의 인터페이스로
class GameEngine {
    GraphicsEngine graphics_;
    AudioEngine audio_;
    PhysicsEngine physics_;
    InputManager input_;
    NetworkManager network_;
    
public:
    void initialize(int width, int height) {
        std::cout << "=== Game Engine Initialization ===\n";
        
        graphics_.init();
        graphics_.setResolution(width, height);
        graphics_.enableVSync();
        
        audio_.init();
        audio_.setVolume(0.8f);
        
        physics_.init();
        physics_.setGravity(9.8f);
        
        input_.init();
        input_.bindKey("W", "MoveForward");
        input_.bindKey("S", "MoveBackward");
        
        network_.init();
        
        std::cout << "=== Initialization Complete ===\n";
    }
    
    void connectToServer(const std::string& server) {
        network_.connect(server);
    }
    
    void shutdown() {
        std::cout << "=== Shutting Down ===\n";
    }
};
int main() {
    GameEngine engine;
    engine.initialize(1920, 1080);
    engine.connectToServer("game.server.com");
    
    std::cout << "\n[Game Running...]\n\n";
    
    engine.shutdown();
    return 0;
}

출력:

=== Game Engine Initialization ===
Graphics: Initializing OpenGL...
Graphics: Set resolution 1920x1080
Graphics: VSync enabled
Audio: Initializing OpenAL...
Audio: Volume set to 0.8
Physics: Initializing Bullet...
Physics: Gravity set to 9.8
Input: Initializing SDL...
Input: Bind W -> MoveForward
Input: Bind S -> MoveBackward
Network: Initializing sockets...
=== Initialization Complete ===
Network: Connecting to game.server.com...
[Game Running...]
=== Shutting Down ===

이 예제는 모든 init()이 성공한다고 가정하지만, 실제 엔진 초기화에서 Facade가 가장 가치 있는 순간은 중간에 실패했을 때입니다. 그래픽과 오디오는 초기화됐는데 물리 엔진 초기화가 실패하면, 이미 초기화한 두 서브시스템을 역순으로 정리한 뒤 에러를 보고해야 합니다. 각 서브시스템의 초기화를 생성자에, 정리를 소멸자에 두면(RAII) C++가 이 규칙을 자동으로 지켜 줍니다. 멤버는 선언 순서대로 생성되고 역순으로 소멸하며, 생성자가 예외를 던지면 이미 생성된 멤버만 소멸자가 호출되기 때문입니다. 반대로 위처럼 init()/shutdown() 쌍으로 만들면 이 순서를 Facade가 직접 관리해야 하고, 서브시스템 사이의 의존(예: 입력이 그래픽 창 핸들을 필요로 함)이 멤버 선언 순서와 어긋나지 않도록 주의해야 합니다. 저는 이런 Facade를 리뷰할 때 shutdown()이 initialize()의 정확한 역순인지, 그리고 initialize()가 중간에 실패했을 때 shutdown()을 불러도 안전한지를 가장 먼저 확인합니다.


같이 보면 좋은 글


자주 묻는 질문 (FAQ)

Q. Pimpl로 감싼 Facade에서 unique_ptr 때문에 incomplete type 에러가 나면 어떻게 하나요?

A. std::unique_ptr의 기본 삭제자는 객체를 지우는 시점에 Impl의 완전한 정의가 필요한데, 헤더에서 소멸자가 암시적으로 생성되면 그 위치에서는 Impl이 불완전 타입이라 에러가 납니다. 헤더에는 ~Facade();를 선언만 해 두고, Impl을 정의한 .cpp 파일에서 Facade::~Facade() = default;로 정의하면 해결됩니다. 이동 생성자와 이동 대입 연산자도 같은 이유로 .cpp에서 = default로 정의해야 합니다.

Q. Facade 패턴과 Adapter 패턴은 어떻게 구분하나요?

A. Adapter는 맞지 않는 인터페이스를 끼워 맞추는 데 초점이 있고, Facade는 여러 서브시스템의 호출 순서와 조합을 한 진입점으로 줄이는 데 초점이 있습니다. 둘 다 래퍼로 보일 수 있으나 설계 의도가 다르므로, 코드 리뷰와 문서에서 용어를 구분해 두는 것이 혼선을 줄입니다.