[모던 C++로 다시 쓰는 GoF 디자인 패턴 총정리 #7] 퍼사드(Facade) 패턴: 복잡한 서브시스템을 단순화하고 모듈화하기

지난 글에서는 데코레이터(Decorator) 패턴을 모던 C++ 관점에서 재해석하며, 상속 기반 장식(Decoration) 대신 함수 합성, 람다, Ranges 등을 활용해 기능 확장 과정을 유연하고 단순화할 수 있음을 확인했습니다. 이번 글에서는 퍼사드(Facade) 패턴에 주목합니다.

퍼사드 패턴은 복잡한 서브시스템을 단일 인터페이스로 감싸, 클라이언트가 내부 구성 요소를 알 필요 없이 단순하고 일관된 방식으로 기능을 이용하도록 돕습니다. 전통적으로는 "정적인 클래스"나 "정적으로 링크된 라이브러리"를 단일 진입점으로 묶는 식으로 구현했지만, 모던 C++에서는 모듈(Modules), Concepts, std::expected, std::format 등을 활용해 퍼사드를 더 명확하고 문서화된 형태로 제공할 수 있습니다. 또한, 런타임 종속성 관리가 필요할 경우 std::variant나 Ranges 파이프라인을 통해 다양한 서브시스템 구성 조합을 동적으로 처리할 수도 있습니다.

패턴 소개: 퍼사드(Facade)

의도:

  • 복잡한 서브시스템에 대한 단순화된 인터페이스를 제공.
  • 내부 구성 요소를 숨기고, 클라이언트는 퍼사드를 통해 편리하고 일관된 방식으로 기능 이용.
  • 예: 복잡한 미디어 처리 라이브러리를 MediaFacade로 감싸고, play(), stop(), pause() 같은 단순한 메서드로 클라이언트에 제공.

전통적 구현 문제점:

  • 퍼사드 클래스가 종종 거대한 헤더나 구현 파일로 구성, 모든 내부 구성요소에 대한 include와 정적 링크 필요.
  • 에러 처리, 의존성 주입 어려움.
  • 문서화나 타입 안전성 부족.

기존 C++ 스타일 구현 (C++11/14/17)

예를 들어, 복잡한 오디오 시스템을 가정해봅시다. 내부적으로 AudioDecoder, AudioRenderer, AudioMixer 등이 있고, 클라이언트는 AudioFacade를 통해 단순한 play, stop 호출만 하면 됩니다.

#include <iostream>
#include <memory>

struct AudioDecoder {
    bool load(const std::string& file) {
        std::cout << "Decoding audio from " << file << "\n";
        return true; // 가상의 성공
    }
};

struct AudioRenderer {
    void start() {
        std::cout << "Starting audio render\n";
    }
    void stop() {
        std::cout << "Stopping audio render\n";
    }
};

struct AudioMixer {
    void setVolume(float vol) {
        std::cout << "Setting volume to " << vol << "\n";
    }
};

struct AudioFacade {
    AudioFacade()
        : decoder(std::make_unique<AudioDecoder>()),
          renderer(std::make_unique<AudioRenderer>()),
          mixer(std::make_unique<AudioMixer>()) {}

    bool play(const std::string& file) {
        if(!decoder->load(file)) return false;
        mixer->setVolume(0.5f);
        renderer->start();
        return true;
    }

    void stop() {
        renderer->stop();
    }

private:
    std::unique_ptr<AudioDecoder> decoder;
    std::unique_ptr<AudioRenderer> renderer;
    std::unique_ptr<AudioMixer> mixer;
};

int main() {
    AudioFacade facade;
    facade.play("music.mp3");
    facade.stop();
}

고찰:

  • 단순한 오브젝트 합성을 통해 서브시스템을 감추고, 단일 인터페이스(AudioFacade) 제공.
  • 에러 처리 시 bool 반환, 런타임 다형성 크게 필요 없으나, 내부 구현 클래스 교체 시 recompile 필요.
  • 클라이언트는 내부 구조 모르고 play(), stop()만 알면 됨.

모던 C++20 이상의 개선: Modules, Concepts, expected, format

1. 모듈(Modules)로 내부 구현 감추기

C++20 모듈을 사용하면, 퍼사드가 노출해야 할 인터페이스와 내부 구현을 명확히 분리할 수 있습니다. 퍼사드 모듈 인터페이스 파일(audio_facade.ixx)에 외부에 공개할 함수 선언만 내보내고, 내부 구현은 구현부 모듈에 숨길 수 있습니다.

// audio_facade.ixx
export module audio_facade;

import <string>;
import <expected>;

export struct AudioFacade {
    AudioFacade();
    std::expected<void,std::string> play(const std::string& file);
    void stop();
};

고찰:

  • export module로 퍼사드 인터페이스를 모듈화.
  • 내부 세부사항(Decoder, Renderer, Mixer)을 모듈 구현부에 감추면, 클라이언트는 오직 AudioFacade 모듈만 import.
  • 빌드 속도, 의존성 관리 개선.

2. std::expected로 에러 처리 명확화

예전에는 bool로 성공/실패를 반환했지만, std::expected를 사용해 실패 원인 메시지를 함께 반환할 수 있습니다.

// audio_facade.cpp (모듈 구현부)
module audio_facade;

import <expected>;
import <iostream>;
import <string>;

struct AudioDecoder {
    bool load(const std::string& file) {
        if (file.empty()) return false; // 가상의 실패
        std::cout << "Decoding audio from " << file << "\n";
        return true;
    }
};

struct AudioRenderer {
    void start() { std::cout << "Starting audio render\n"; }
    void stop() { std::cout << "Stopping audio render\n"; }
};

struct AudioMixer {
    void setVolume(float vol) { std::cout << "Setting volume to " << vol << "\n"; }
};

AudioFacade::AudioFacade()
    : decoder(std::make_unique<AudioDecoder>()),
      renderer(std::make_unique<AudioRenderer>()),
      mixer(std::make_unique<AudioMixer>()) {}

std::expected<void,std::string> AudioFacade::play(const std::string& file) {
    if(!decoder->load(file)) {
        return std::unexpected("Failed to decode file: " + file);
    }
    mixer->setVolume(0.5f);
    renderer->start();
    return {};
}

void AudioFacade::stop() {
    renderer->stop();
}

// 클라이언트는 모듈 import 후:
// import audio_facade;
// AudioFacade facade;
// auto res = facade.play("");
// if(!res) std::cerr << "Error: " << res.error() << "\n";

고찰:

  • std::expected로 오류 원인을 명확히 전달, 클라이언트가 적절히 처리 가능.

3. Concepts로 퍼사드가 제공해야 할 기능 제약

퍼사드 자체는 하나의 인터페이스지만, 어떤 모듈에서든 유사한 퍼사드를 정의할 수 있습니다. Concepts를 사용해 "퍼사드로서 최소한 play와 stop을 제공해야 한다"는 요구사항을 정의할 수 있습니다.

template<typename F>
concept AudioFacadeConcept = requires(F& f, const F& cf) {
    { cf.play(std::string()) } -> std::convertible_to<std::expected<void,std::string>>;
    { f.stop() } -> std::same_as<void>;
};

고찰:

  • 이를 통해 클라이언트 템플릿 함수에서 팩사드 타입 제약 가능.
  • 상속 없이도 정적 타입 검사로 퍼사드 요구사항 충족 여부 확인.

4. std::format으로 디버깅 메시지 정교화

디버그나 로깅 시 std::format으로 형식화된 출력 가능:

// 예: play 함수 내에서
#include <format>
// ...
std::cout << std::format("Playing file: {}\n", file);

고찰:

  • 깔끔한 로그 출력, 유지보수성 향상.

5. 동적 종속성 관리: std::variant나 함수 합성

만약 여러 Audio 시스템(예: ALSA, PulseAudio, Dummy) 중 하나를 런타임에 선택할 수 있어야 한다면, std::variant를 사용해 서로 다른 내부 구현을 래핑한 Facade를 제공할 수도 있습니다.

#include <variant>

struct ALSAImpl {
    std::expected<void,std::string> play(const std::string& f) { /*...*/ return {}; }
    void stop() { /*...*/ }
};

struct PulseAudioImpl {
    std::expected<void,std::string> play(const std::string& f) { /*...*/ return {}; }
    void stop() { /*...*/ }
};

using AnyAudio = std::variant<ALSAImpl, PulseAudioImpl>;

template<AudioFacadeConcept A>
void tryPlay(A& facade, const std::string& file) {
    auto res = facade.play(file);
    if(!res) std::cerr << "Error: " << res.error() << "\n";
}

int main() {
    AnyAudio anyAudio = ALSAImpl{};
    std::visit([&](auto& impl){
        tryPlay(impl, "sound.wav");
        impl.stop();
    }, anyAudio);
}

고찰:

  • 모듈, Concepts, std::variant 조합으로 서로 다른 내부 구현체를 하나의 퍼사드 개념 하에 묶을 수 있음.
  • 상속 계층 불필요, Template + Variant로 다형성 제공.

비교 및 분석

  • 전통적 구현(C++11 전후):
    • 퍼사드 클래스를 단순히 정적 include와 합성으로 구성
    • 내부 구현 교체 시 재컴파일 필요, 에러 처리 단순
    • 문서화와 의존성 관리가 비교적 모호
  • 모던 C++(C++20 이상):
    • 모듈(Modules)로 내부 구현 감추어 빌드, 의존성 관리 개선
    • Concepts로 퍼사드 인터페이스 요구사항 정적 표현
    • std::expected로 실패 처리 명확화, std::format으로 로깅 향상
    • std::variant로 런타임 구현 선택 가능, 상속 없이 다형성 제공
    • 유지보수성과 확장성, 타입 안전성, 빌드 성능 향상

결국 모던 C++에서는 퍼사드 패턴을 단순한 "큰 헤더 + 정적 객체 합성" 방식에서 벗어나, 모듈을 통한 명확한 인터페이스/구현 분리, Concepts 기반 검증, std::expected로 에러 처리 개선, std::variant로 런타임 유연성 확보 등 더 세련된 방식으로 구현할 수 있습니다.

마무리

퍼사드 패턴은 복잡한 서브시스템을 단순화하는 데 유용하지만, 전통적인 방식으로는 문서화나 빌드 의존성 관리 측면에서 아쉬움이 있었습니다. 모던 C++20 이상에서는 모듈, Concepts, std::expected, std::format, std::variant 등 다양한 기능을 활용해 퍼사드를 더 선언적이고 유지보수 친화적으로 구현할 수 있습니다. 이렇게 하면 내부 구현을 감추면서도 에러 처리, 문서화, 테스트가 수월해집니다.

다음 글에서는 구조적 패턴 중 또 다른 패턴을 다루거나, 행동 패턴(Behavioral Patterns)으로 넘어가며 모던 C++에서 전략(Strategy), 상태(State), 옵저버(Observer) 같은 패턴을 재구현해볼 계획입니다.

반응형