이전 글에서는 플라이웨이트(Flyweight) 패턴을 모던 C++ 관점에서 재해석하며, 상속 없이 값 기반 공유 로직을 단순히 구현하고, 조건부 처리, 비동기 로딩, 로깅, 에러 처리 등 다양한 요구에도 쉽게 대응할 수 있음을 확인했습니다. 이번에는 구조적 패턴 중 프록시(Proxy) 패턴을 다룹니다.
프록시 패턴은 객체 접근을 제어하거나, 지연 로딩(Lazy Loading), 원격 호출(Remote Proxy), 캐싱(Caching), 권한 체크 등의 기능을 대리 객체를 통해 투명하게 추가하는 패턴입니다. 전통적으로는 상속 기반 인터페이스와 Proxy 클래스를 정의해 접근 로직을 구현했으나, 이는 클래스 증가, 유지보수 어려움, 에러 처리나 비동기 처리 시 복잡성을 초래합니다.
C++20 이상에서는 Concepts로 원하는 인터페이스를 제약하고, 람다와 함수 합성으로 접근 제어 로직을 구현하여 상속 없이도 대리 객체를 쉽게 구성할 수 있습니다. std::expected로 에러 처리, std::format으로 로깅, coroutine으로 비동기 호출, Ranges로 조건부 처리 등 다양한 고급 기능에도 간결하게 대응할 수 있습니다.
패턴 소개: 프록시(Proxy)
의도:
- 다른 객체에 대한 접근을 제어하기 위해 대리 객체를 제공하여, 접근 로직(권한 체크, 지연 로딩, 캐싱 등)을 투명하게 삽입.
- 예: 원격 서버 객체를 로컬 Proxy로 감싸, 네트워크 호출 로직을 숨기거나, 객체 사용 전 실제 인스턴스 생성을 지연.
전통적 구현 문제점:
- Proxy 인터페이스, RealSubject 인터페이스 및 구현 클래스 필요
- 새로운 접근 제어 로직 추가 시 클래스 증가
- 비동기 처리, 조건부 접근, 로깅, 에러 처리 등 추가 기능 구현 시 복잡성 증가
기존 C++ 스타일 구현 (전통적 방식)
예를 들어, RealSubject가 request() 메서드를 제공하고, Proxy가 이 메서드 호출 전후 권한 체크를 수행하는 구조:
#include <iostream>
#include <memory>
#include <string>
struct Subject {
virtual ~Subject()=default;
virtual void request()=0;
};
struct RealSubject : Subject {
void request() override {
std::cout << "RealSubject: Handling request.\n";
}
};
struct Proxy : Subject {
std::unique_ptr<Subject> real;
Proxy(std::unique_ptr<Subject> r) : real(std::move(r)){}
void request() override {
std::cout << "Proxy: Checking access...\n";
real->request();
}
};
int main() {
auto real = std::make_unique<RealSubject>();
Proxy proxy(std::move(real));
proxy.request();
}
문제점:
- Proxy 클래스 정의 필요
- 다른 접근 제어 로직 추가 시 Decorator나 또 다른 Proxy 클래스 필요
- 비동기, 조건부 로직, 로깅, 에러 처리 시 코드 복잡 증가
모던 C++20 이상의 개선: Concepts, 람다로 대리 로직 구현
1. Concepts로 Subject 인터페이스 요구사항 정의
request() 호출 시 std::expected<void,std::string> 반환한다고 가정:
template<typename S>
concept SubjectConcept = requires(S& s) {
{ s.request() } -> std::convertible_to<std::expected<void,std::string>>;
};
2. RealSubject를 람다나 함수 객체로 표현
auto RealSubjectLambda = []() -> std::expected<void,std::string> {
std::cout << "RealSubject: Handling request.\n";
return {};
};
static_assert(SubjectConcept<decltype(RealSubjectLambda)>);
3. Proxy 로직을 함수 합성으로 추가
auto proxyLogic = [&](auto baseSubject) {
return [=]() -> std::expected<void,std::string> {
std::cout << "Proxy: Checking access...\n";
auto res = baseSubject.request();
// 권한 체크 후 실제 호출
if(!res) return res;
return {};
};
};
auto proxiedSubject = proxyLogic(RealSubjectLambda);
static_assert(SubjectConcept<decltype(proxiedSubject)>);
사용 예:
int main() {
auto res = proxiedSubject.request();
if(!res) std::cerr << "Error: " << res.error() << "\n";
else std::cout << "Request success.\n";
}
출력:
Proxy: Checking access...
RealSubject: Handling request.
Request success.
비교:
- 전통적: Proxy 클래스 필요
- C++20: 람다 합성으로 프록시 기능 추가, 상속 없음
조건부 접근, 로깅, 비동기 처리, Ranges 적용
조건부 접근(예: 특정 사용자만 접근 가능):
auto conditionalProxy = [&](auto baseSubject) {
return [=]() -> std::expected<void,std::string> {
bool authorized = false; // 예: 인증 로직
if(!authorized) return std::unexpected("Access denied");
return baseSubject.request();
};
};
auto authorizedSubject = conditionalProxy(RealSubjectLambda);
authorizedSubject.request(); // "Access denied"
로깅(std::format):
auto loggingProxy = [&](auto baseSubject) {
return [=]() -> std::expected<void,std::string> {
std::cout << "[LOG] Before request\n";
auto res = baseSubject.request();
if(!res) std::cout << std::format("[LOG] Error: {}\n", res.error());
else std::cout << "[LOG] After request\n";
return res;
};
};
auto loggedProxy = loggingProxy(proxiedSubject);
loggedProxy.request();
비동기 처리: coroutine으로 request 구현 가능:
// co_request()로 비동기 I/O후 결과 반환,
// coroutine으로 비동기 프록시 로직 구현
Ranges: 여러 주체(Subject) 리스트에 대해 파이프라인 처리를 할 수도 있음.
#include <vector>
#include <ranges>
std::vector<decltype(proxiedSubject)> subjects = {proxiedSubject, authorizedSubject};
for (auto res : subjects | std::views::transform([](auto& s){return s.request();})) {
if(!res) std::cerr << "One subject failed: " << res.error() << "\n";
}
비교:
- 전통적: 새로운 로직(조건부, 로깅, 비동기) 추가 시 클래스 증가
- C++20: 람다 장식으로 쉽게 기능 추가, coroutine, Ranges, std::expected로 다양한 요구사항 처리
전통적 구현 vs C++11/14/17 vs C++20 이상 비교
전통적(C++98/03):
- Proxy 클래스 필요, RealSubject와 동일 인터페이스 구현
- 조건부 접근, 비동기 처리, 로깅 추가 시 클래스나 코드 증가
- 에러 처리나 파이프라인 처리 복잡
C++11/14/17:
- 람다, std::function 도입으로 일부 단순화 가능
- Concepts, coroutine, std::expected, Ranges 부재로 정적 타입 제약, 비동기/에러 처리 한계
C++20 이상(모던 C++):
- Concepts로 Subject 요구사항 정의, 상속 없이 타입 안전 인터페이스
- 람다 합성으로 Proxy 기능 추가, std::expected로 에러 처리 명확, std::format으로 로깅, coroutine으로 비동기 처리, Ranges로 파이프라인 처리
- 클래스 증가 없이 확장 가능, 유지보수성, 확장성, 타입 안전성 대폭 향상
결론
프록시(Proxy) 패턴은 객체 접근을 제어하거나 부가 기능(로깅, 권한 체크, 캐싱, 비동기 호출)을 투명하게 삽입하는 강력한 패턴이지만, 전통적 구현은 Proxy 클래스 정의와 상속 기반 구조로 인해 유지보수성과 확장성에 한계를 가진다.
C++20 이상에서는 Concepts, 람다, std::function, std::expected, coroutine, Ranges, std::format 등을 활용해 상속 없이도 프록시 로직을 함수 합성 형태로 구현할 수 있습니다. 이를 통해 조건부 접근, 비동기 처리, 로깅, 에러 처리 등 다양한 요구사항에 적은 코드로 대응 가능하며, 전통적 구현 대비 유지보수성과 확장성이 크게 개선됩니다.