지난 글에서는 데코레이터(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) 같은 패턴을 재구현해볼 계획입니다.