이전 글에서는 데코레이터(Decorator) 패턴을 모던 C++ 관점에서 재해석하며, 상속 없이도 람다 합성을 통해 객체에 동적으로 기능을 추가하는 방법을 살펴봤습니다. 이번에는 구조적 패턴 중 퍼사드(Facade) 패턴을 다룹니다.
퍼사드 패턴은 복잡한 서브시스템을 단일하고 단순한 인터페이스로 감싸, 클라이언트가 시스템을 쉽게 사용할 수 있게 하는 패턴입니다. 전통적으로는 Facade 클래스를 만들어 서브시스템 객체들을 정적 호출 계층으로 감싸았으나, 이는 서브시스템 변경 시 Facade 수정 필요, 에러 처리나 비동기 처리 시 복잡성 증가 등의 문제를 야기합니다.
C++20 이상에서는 Concepts, 람다, std::function, std::expected, std::format, coroutine, Ranges 등 기능을 활용해 복잡한 서브시스템 호출을 파이프라인이나 함수 합성 형태로 래핑할 수 있습니다. 이를 통해 상속 없이도 조건부 처리, 비동기 처리, 로깅, 에러 처리 등 다양한 요구사항에 유연하게 대응할 수 있으며, 유지보수성과 확장성이 크게 향상됩니다.
패턴 소개: 퍼사드(Facade)
의도:
- 복잡한 서브시스템을 단순한 인터페이스(퍼사드)로 감싸, 클라이언트가 시스템을 쉽게 사용할 수 있도록 함.
- 예: 미디어 처리 라이브러리, 그래픽 렌더링 엔진, 데이터베이스 접근 로직을 한 곳(Facade)에 모아 간단한 API 제공.
전통적 구현 문제점:
- Facade 클래스를 정적으로 정의, 서브시스템 객체를 내부에서 관리
- 서브시스템 변경 시 Facade 수정 필요
- 에러 처리, 비동기 처리, 조건부 호출, 로깅 등 기능 추가 시 코드 복잡 증가
기존 C++ 스타일 구현 (전통적 방식)
예를 들어, 오디오 서브시스템(Decoder, Renderer, Mixer)와 Facade( AudioFacade ):
#include <iostream>
#include <string>
struct AudioDecoder {
bool load(const std::string& file) {
std::cout << "Decoding " << file << "\n";
return !file.empty();
}
};
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 {
AudioDecoder dec;
AudioRenderer ren;
AudioMixer mix;
bool play(const std::string& file) {
if(!dec.load(file)) return false;
mix.setVolume(0.5f);
ren.start();
return true;
}
void stop() {
ren.stop();
}
};
int main() {
AudioFacade facade;
if(!facade.play("music.mp3")) std::cerr << "Play error\n";
facade.stop();
}
문제점:
- Facade 클래스 정적으로 서브시스템 관리, 서브시스템 변경 시 Facade 수정 필요
- 비동기 I/O나 조건부 처리, 에러 처리 시 Facade 코드 복잡 증가
모던 C++20 이상의 개선: 람다 합성, Concepts, std::expected
1. Concepts로 Facade 인터페이스 요구사항 정의
클라이언트가 원하는 인터페이스: play(file), stop()을 호출 시 std::expected<void,std::string> 반환한다고 가정:
template<typename F>
concept FacadePlayable = requires(F& f, const std::string& file) {
{ f.play(file) } -> std::convertible_to<std::expected<void,std::string>>;
{ f.stop() } -> std::convertible_to<std::expected<void,std::string>>;
};
2. 서브시스템 호출을 람다로 표현
각 서브시스템 함수를 람다로 정의:
auto loadAudio = [](const std::string& file)->std::expected<void,std::string> {
if(file.empty()) return std::unexpected("Empty filename");
std::cout << "Decoding " << file << "\n";
return {};
};
auto setVolume = [](float vol)->std::expected<void,std::string> {
if(vol<0||vol>1) return std::unexpected("Volume out of range");
std::cout << "Setting volume to " << vol << "\n";
return {};
};
auto startRender = []()->std::expected<void,std::string> {
std::cout << "Starting audio render\n";
return {};
};
auto stopRender = []()->std::expected<void,std::string> {
std::cout << "Stopping audio render\n";
return {};
};
3. Facade를 함수 합성으로 구현
play(file) 호출 시 loadAudio → setVolume(0.5) → startRender 순서 실행:
auto makeFacade = [&](auto loadFunc, auto volumeFunc, auto startFunc, auto stopFunc) {
return [=](auto action) {
return [=](const std::string& file)->std::expected<void,std::string> {
return action(loadFunc, volumeFunc, startFunc, stopFunc, file);
};
};
};
// action 람다 정의: load->volume->start
auto playAction = [](auto loadFunc, auto volumeFunc, auto startFunc, auto stopFunc, const std::string& file)
-> std::expected<void,std::string> {
auto lr = loadFunc(file);
if(!lr) return lr;
auto vr = volumeFunc(0.5f);
if(!vr) return vr;
auto sr = startFunc();
return sr; // 마지막 결과 반환
};
auto stopAction = [](auto loadFunc, auto volumeFunc, auto startFunc, auto stopFunc, const std::string&)
-> std::expected<void,std::string> {
return stopFunc();
};
// Facade 생성
auto facade = [=](auto load, auto vol, auto start, auto stop) {
return [=](const std::string& file, bool doPlay) -> std::expected<void,std::string> {
if(doPlay) return playAction(load,vol,start,stop,file);
else return stopAction(load,vol,start,stop,file);
};
}(loadAudio, setVolume, startRender, stopRender);
// 사용 예:
int main() {
auto res = facade("music.mp3", true); // play
if(!res) std::cerr << "Error: " << res.error() << "\n";
else std::cout << "Played successfully.\n";
facade("", true); // play with empty file → error
facade("music.mp3", false); // stop
}
비교:
- 전통적: Facade 클래스 필요, 내부에서 서브시스템 호출
- C++20: 람다 합성으로 서브시스템 호출 흐름 구성, 상속 없음, std::expected로 에러 처리 명확
조건부 처리, 로깅 추가
조건부 처리(예: 특정 파일 확장자만 재생):
auto conditionalPlay = [&](auto baseFacade) {
return [=](const std::string& file, bool doPlay)->std::expected<void,std::string> {
if(doPlay && !file.ends_with(".mp3")) {
return std::unexpected("Only .mp3 supported");
}
return baseFacade(file, doPlay);
};
};
auto conditionalFacade = conditionalPlay(facade);
로깅 추가(std::format):
auto loggingFacade = [&](auto baseFacade) {
return [=](const std::string& file, bool doPlay)->std::expected<void,std::string> {
std::cout << std::format("[LOG] {} {}\n", doPlay?"Play":"Stop", file);
auto res = baseFacade(file, doPlay);
if(!res) std::cout << std::format("[LOG] Error: {}\n", res.error());
else std::cout << "[LOG] Success.\n";
return res;
};
};
auto loggedConditionalFacade = loggingFacade(conditionalFacade);
loggedConditionalFacade("music.wav", true); // Error: Only .mp3 supported
비동기 처리: coroutine
비동기 I/O 기반 서브시스템 호출 시 coroutine으로 구현 가능:
// co_loadAudio(...)로 네트워크 스트림에서 오디오 로드 co_await
// co_facade(...)로 비동기 서브시스템 호출
Ranges로 다양한 파일 처리 파이프라인
여러 파일을 Ranges로 순회하며 facade 호출:
#include <vector>
#include <ranges>
std::vector<std::string> files = {"music.mp3","sound.mp3","corrupt.mp3"};
for (auto res : files | std::views::transform([&](auto& f){return loggedConditionalFacade(f,true);} )) {
if(!res) std::cerr << "Error in one of the files\n";
}
비교:
- 전통적: Facade 수정 필요, 조건부 처리나 로깅 추가 시 Facade 클래스 수정
- C++20: 람다 합성으로 기능 추가, coroutine, Ranges, std::expected로 고급 기능 쉽게 구현
전통적 구현 vs C++11/14/17 vs C++20 이상 비교
전통적(C++98/03):
- Facade 클래스 정의, 서브시스템 호출 내부 구현
- 서브시스템 변경 시 Facade 수정, 기능 추가 시 코드 복잡 증가
- 비동기, 조건부, 로깅 등 고급 기능 구현 번거로움
C++11/14/17:
- 람다, std::function 도입으로 일부 개선 가능
- Concepts, std::expected, coroutine, Ranges 미지원 → 정적 타입 제약, 비동기/에러 처리 불편
C++20 이상(모던 C++):
- 람다 합성과 Concepts로 상속 없이 Facade 인터페이스 정의
- std::expected로 에러 처리 명확, std::format으로 로깅 간단
- coroutine으로 비동기 처리, Ranges로 파일 리스트 파이프라인 처리
- 클래스 증가 없이 확장 가능, 유지보수성, 확장성, 타입 안전성 대폭 향상
결론
퍼사드(Facade) 패턴은 복잡한 서브시스템을 단순한 인터페이스로 감싸 클라이언트가 쉽게 사용할 수 있게 하는 패턴입니다. 전통적 구현은 Facade 클래스를 정적으로 정의하고, 서브시스템 변경 시 Facade 수정, 기능 추가 시 코드 복잡 증가 문제가 있었습니다.
C++20 이상에서는 람다, Concepts, std::expected, std::format, coroutine, Ranges 등을 활용해 상속 없이도 복잡한 서브시스템을 함수 합성 형태로 감싸는 Facade를 구현할 수 있습니다. 조건부 처리, 비동기 실행, 로깅, 에러 처리 등 다양한 요구사항을 적은 코드로 처리 가능하며, 전통적 구현 대비 유지보수성과 확장성이 크게 개선됩니다.