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

이전 글에서는 데코레이터(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를 구현할 수 있습니다. 조건부 처리, 비동기 실행, 로깅, 에러 처리 등 다양한 요구사항을 적은 코드로 처리 가능하며, 전통적 구현 대비 유지보수성과 확장성이 크게 개선됩니다.

반응형