C++ Adapter Pattern 완벽 가이드 | 인터페이스 변환과 호환성
이 글의 핵심
C++ Adapter Pattern : 인터페이스 변환과 호환성. Adapter Pattern이란?. 왜 필요한가·객체 어댑터.
Adapter Pattern이란? 왜 필요한가
해외에서 산 전자제품과 콘센트 모양이 맞지 않을 때 어댑터로 모양만 바꿔 꽂듯이, 소프트웨어에서도 호출 규약(Target) 과 실제 구현(Adaptee) 이 다를 때 중간에서 맞춰 주는 역할이 어댑터입니다. 아래에서는 그 불일치가 코드로 어떻게 드러나는지부터 살펴보겠습니다. 구조 패턴 시리즈에서 다른 구조 패턴과의 관계를 정리했고, JavaScript에서도 API를 맞추는 식의 통합이 자주 나옵니다.
문제 시나리오: 호환되지 않는 인터페이스
문제는 다음과 같습니다. 기존 라이브러리가 노출하는 함수 이름·매개변수 형태가, 우리가 이미 설계해 둔 추상 인터페이스와 맞지 않을 때가 있습니다.
// 내 코드가 기대하는 인터페이스
class MediaPlayer {
public:
virtual void play(const std::string& filename) = 0;
};
// 기존 라이브러리 (호환 안 됨)
class VLCPlayer {
public:
void playVLC(const std::string& filename) { /* ....*/ }
};
// 어떻게 VLCPlayer를 MediaPlayer로 사용?
해결: Adapter Pattern은 인터페이스를 변환합니다. Adapter가 Target 인터페이스를 구현하고, 내부에서 Adaptee를 호출합니다.
// Adapter
// 타입 정의
class VLCAdapter : public MediaPlayer {
public:
VLCAdapter(std::unique_ptr<VLCPlayer> player)
: vlc(std::move(player)) {}
void play(const std::string& filename) override {
vlc->playVLC(filename); // 인터페이스 변환
}
private:
std::unique_ptr<VLCPlayer> vlc;
};
핵심 개념: Adapter Pattern은 호환되지 않는 인터페이스를 연결하는 다리 역할을 합니다. 레거시 시스템 통합이나 서드파티 라이브러리 사용 시 필수적인 패턴입니다.
1. 객체 어댑터
객체 어댑터는 상속으로 타입을 합치지 않고, 멤버로 VLCPlayer 같은 구현체를 들고 있는 방식(조합) 입니다. 구현을 바꾸거나 목(mock)으로 바꿀 때 어댑터 한곳만 건드리면 되므로, 실무에서는 이 형태를 가장 많이 권장합니다.
조합 방식
아래 예제에서는 MediaPlayer 하나의 인터페이스로 VLCAdapter와 MP4Adapter를 바꿔 끼울 수 있게 하여, 호출부(player->play)는 동일한 문장을 유지합니다.
#include <iostream>
#include <memory>
#include <string>
class MediaPlayer {
public:
virtual void play(const std::string& filename) = 0;
virtual ~MediaPlayer() = default;
};
class VLCPlayer {
public:
void playVLC(const std::string& filename) {
std::cout << "Playing VLC: " << filename << '\n';
}
};
class MP4Player {
public:
void playMP4(const std::string& filename) {
std::cout << "Playing MP4: " << filename << '\n';
}
};
class VLCAdapter : public MediaPlayer {
public:
VLCAdapter() : vlc(std::make_unique<VLCPlayer>()) {}
void play(const std::string& filename) override {
vlc->playVLC(filename);
}
private:
std::unique_ptr<VLCPlayer> vlc;
};
class MP4Adapter : public MediaPlayer {
public:
MP4Adapter() : mp4(std::make_unique<MP4Player>()) {}
void play(const std::string& filename) override {
mp4->playMP4(filename);
}
private:
std::unique_ptr<MP4Player> mp4;
};
int main() {
std::unique_ptr<MediaPlayer> player;
player = std::make_unique<VLCAdapter>();
player->play("movie.vlc");
player = std::make_unique<MP4Adapter>();
player->play("movie.mp4");
}
2. 클래스 어댑터
클래스 어댑터는 다중 상속을 사용하는 방식입니다. C++에서는 가능하지만, 객체 어댑터보다 유연성이 떨어집니다.
다중 상속 방식
#include <iostream>
#include <string>
class MediaPlayer {
public:
virtual void play(const std::string& filename) = 0;
virtual ~MediaPlayer() = default;
};
class VLCPlayer {
public:
void playVLC(const std::string& filename) {
std::cout << "Playing VLC: " << filename << '\n';
}
};
// 클래스 어댑터 (다중 상속)
class VLCAdapter : public MediaPlayer, private VLCPlayer {
public:
void play(const std::string& filename) override {
playVLC(filename); // 직접 호출
}
};
int main() {
MediaPlayer* player = new VLCAdapter();
player->play("movie.vlc");
delete player;
}
장점: Adaptee 객체를 저장할 필요 없음. 단점: 다중 상속, Adaptee가 final이면 불가.
3. 레거시 코드 통합
레거시 시스템을 현대적인 코드베이스에 통합할 때 Adapter Pattern이 빛을 발합니다. 기존 코드를 수정하지 않고도 새로운 인터페이스로 사용할 수 있습니다.
오래된 API를 현대적 인터페이스로
#include <iostream>
#include <string>
#include <memory>
// 레거시 API (C 스타일)
class LegacyRectangle {
public:
void draw(int x1, int y1, int x2, int y2) {
std::cout << "Legacy: Rectangle from (" << x1 << "," << y1
<< ") to (" << x2 << "," << y2 << ")\n";
}
};
// 현대적 인터페이스
class Shape {
public:
virtual void draw() = 0;
virtual ~Shape() = default;
};
class Rectangle : public Shape {
public:
Rectangle(int x, int y, int w, int h)
: x_(x), y_(y), width_(w), height_(h) {}
void draw() override {
std::cout << "Modern: Rectangle at (" << x_ << "," << y_
<< ") size " << width_ << "x" << height_ << '\n';
}
private:
int x_, y_, width_, height_;
};
// Adapter
class LegacyRectangleAdapter : public Shape {
public:
LegacyRectangleAdapter(int x, int y, int w, int h)
: x_(x), y_(y), width_(w), height_(h),
legacy(std::make_unique<LegacyRectangle>()) {}
void draw() override {
legacy->draw(x_, y_, x_ + width_, y_ + height_);
}
private:
int x_, y_, width_, height_;
std::unique_ptr<LegacyRectangle> legacy;
};
int main() {
std::unique_ptr<Shape> shape1 = std::make_unique<Rectangle>(10, 20, 100, 50);
shape1->draw();
std::unique_ptr<Shape> shape2 = std::make_unique<LegacyRectangleAdapter>(10, 20, 100, 50);
shape2->draw();
}
4. 자주 발생하는 문제와 해결법
문제 1: 메모리 누수
증상: 메모리 누수. 원인: raw pointer 사용.
// ❌ 잘못된 사용
class Adapter {
Adaptee* adaptee; // 누가 delete?
};
// ✅ 올바른 사용
class Adapter {
std::unique_ptr<Adaptee> adaptee;
};
문제 2: 양방향 어댑터
증상: 순환 의존성. 원인: A를 B로, B를 A로 변환.
// ✅ 해결: 공통 인터페이스
class CommonInterface {
virtual void operation() = 0;
};
class AdapterA : public CommonInterface { /* ....*/ };
class AdapterB : public CommonInterface { /* ....*/ };
5. 프로덕션 패턴
패턴 1: 팩토리와 결합
class MediaPlayerFactory {
public:
static std::unique_ptr<MediaPlayer> create(const std::string& type) {
if (type == "vlc") {
return std::make_unique<VLCAdapter>();
} else if (type == "mp4") {
return std::make_unique<MP4Adapter>();
}
return nullptr;
}
};
auto player = MediaPlayerFactory::create("vlc");
player->play("movie.vlc");
패턴 2: 템플릿 어댑터
template<typename Adaptee>
class GenericAdapter : public MediaPlayer {
public:
GenericAdapter() : adaptee(std::make_unique<Adaptee>()) {}
void play(const std::string& filename) override {
adaptee->playSpecific(filename);
}
private:
std::unique_ptr<Adaptee> adaptee;
};
6. 완전한 예제: 결제 시스템
#include <iostream>
#include <memory>
#include <string>
class PaymentProcessor {
public:
virtual bool processPayment(double amount) = 0;
virtual ~PaymentProcessor() = default;
};
// 레거시 PayPal API
class PayPalAPI {
public:
bool sendPayment(double dollars) {
std::cout << "PayPal: Processing $" << dollars << '\n';
return true;
}
};
// 레거시 Stripe API
class StripeAPI {
public:
bool charge(int cents) {
std::cout << "Stripe: Charging " << cents << " cents\n";
return true;
}
};
// 새로운 Square API
class SquareAPI {
public:
bool makePayment(const std::string& amount) {
std::cout << "Square: Payment of " << amount << '\n';
return true;
}
};
// Adapters
class PayPalAdapter : public PaymentProcessor {
public:
PayPalAdapter() : paypal(std::make_unique<PayPalAPI>()) {}
bool processPayment(double amount) override {
return paypal->sendPayment(amount);
}
private:
std::unique_ptr<PayPalAPI> paypal;
};
class StripeAdapter : public PaymentProcessor {
public:
StripeAdapter() : stripe(std::make_unique<StripeAPI>()) {}
bool processPayment(double amount) override {
int cents = static_cast<int>(amount * 100);
return stripe->charge(cents);
}
private:
std::unique_ptr<StripeAPI> stripe;
};
class SquareAdapter : public PaymentProcessor {
public:
SquareAdapter() : square(std::make_unique<SquareAPI>()) {}
bool processPayment(double amount) override {
return square->makePayment("$" + std::to_string(amount));
}
private:
std::unique_ptr<SquareAPI> square;
};
class PaymentService {
public:
PaymentService(std::unique_ptr<PaymentProcessor> processor)
: processor_(std::move(processor)) {}
void checkout(double amount) {
std::cout << "Processing checkout for $" << amount << '\n';
if (processor_->processPayment(amount)) {
std::cout << "Payment successful!\n\n";
} else {
std::cout << "Payment failed!\n\n";
}
}
private:
std::unique_ptr<PaymentProcessor> processor_;
};
int main() {
PaymentService service1(std::make_unique<PayPalAdapter>());
service1.checkout(99.99);
PaymentService service2(std::make_unique<StripeAdapter>());
service2.checkout(49.50);
PaymentService service3(std::make_unique<SquareAdapter>());
service3.checkout(29.99);
}
정리
| 개념 | 설명 |
|---|---|
| Adapter Pattern | 인터페이스를 변환 |
| 목적 | 호환되지 않는 인터페이스 통합 |
| 구조 | Target, Adapter, Adaptee |
| 장점 | 레거시 통합, OCP 준수, 재사용성 |
| 단점 | 클래스 증가, 간접 참조 |
| 사용 사례 | 레거시 통합, 서드파티 라이브러리, API 변환 |
| Adapter Pattern은 호환되지 않는 인터페이스를 통합하는 필수 패턴입니다. |
FAQ
Q1: Adapter Pattern은 언제 쓰나요?
A: 레거시 코드 통합, 서드파티 라이브러리 사용, 인터페이스 불일치 해결 시 사용합니다.
Q2: 객체 어댑터 vs 클래스 어댑터?
A: 객체 어댑터는 조합(권장), 클래스 어댑터는 다중 상속(C++ 가능).
Q3: Decorator와 차이는?
A: Adapter는 인터페이스 변환, Decorator는 기능 추가에 집중합니다.
Q4: Facade와 차이는?
A: Adapter는 단일 클래스 변환, Facade는 서브시스템 단순화에 집중합니다.
Q5: 성능 오버헤드는?
A: 간접 참조 1회, 무시할 수 있는 수준입니다.
Q6: Adapter Pattern 학습 리소스는?
A:
- “Design Patterns” by Gang of Four
- “Head First Design Patterns” by Freeman & Freeman
- Refactoring Guru: Adapter Pattern 한 줄 요약: Adapter Pattern으로 호환되지 않는 인터페이스를 통합할 수 있습니다. 다음으로 Proxy Pattern을 읽어보면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ Decorator Pattern 완벽 가이드 | 기능 동적 추가와 조합
- C++ Facade 패턴 완벽 가이드 | 복잡한 서브시스템을 하나의 간단한 인터페이스로
- C++ Bridge 패턴 완벽 가이드 | 구현과 추상화 분리로 확장성 높이기
관련 글
- C++ Decorator Pattern 완벽 가이드 | 기능 동적 추가와 조합
- C++ Command Pattern 완벽 가이드 | 실행 취소와 매크로 시스템
- C++ CRTP 완벽 가이드 | 정적 다형성과 컴파일 타임 최적화
- C++ Facade 패턴 완벽 가이드 | 복잡한 서브시스템을 하나의 간단한 인터페이스로
- C++ Factory Pattern 완벽 가이드 | 객체 생성 캡슐화와 확장성
심화 부록: 구현·운영 관점
이 부록은 앞선 본문에서 다룬 주제(「C++ Adapter Pattern 완벽 가이드 | 인터페이스 변환과 호환성」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.
내부 동작과 핵심 메커니즘
flowchart TD A[입력·요청·이벤트] --> B[파싱·검증·디코딩] B --> C[핵심 연산·상태 전이] C --> D[부작용: I/O·네트워크·동시성] D --> E[결과·관측·저장]
sequenceDiagram participant C as 클라이언트/호출자 participant B as 경계(런타임·게이트웨이·프로세스) participant D as 의존성(API·DB·큐·파일) C->>B: 요청/이벤트 B->>D: 조회·쓰기·RPC D-->>B: 지연·부분 실패·재시도 가능 B-->>C: 응답 또는 오류(코드·상관 ID)
- 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
- 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
- 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
- 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.
프로덕션 운영 패턴
| 영역 | 운영 관점 질문 |
|---|---|
| 관측성 | 요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가 |
| 안전성 | 입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가 |
| 신뢰성 | 재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가 |
| 성능 | 캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가 |
| 배포 | 롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가 |
| 용량 | 피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가 |
스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.
확장 예시: 엔드투엔드 미니 시나리오
앞선 본문 주제(「C++ Adapter Pattern 완벽 가이드 | 인터페이스 변환과 호환성」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.
- 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
- 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
- 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
- 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
- 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
ctx = newCorrelationId()
validated = validateSchema(request)
authorize(validated, ctx)
result = domainCore(validated)
persistOrEmit(result, idempotentKey)
recordMetrics(ctx, latency, outcome)
return result
문제 해결(Troubleshooting)
| 증상 | 가능 원인 | 조치 |
|---|---|---|
| 간헐적 실패 | 레이스, 타임아웃, 외부 의존성, DNS | 최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검 |
| 성능 저하 | N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스 | 프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거 |
| 메모리 증가 | 캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납 | 상한·TTL·힙/FD 스냅샷 비교 |
| 빌드·배포만 실패 | 환경 변수, 권한, 플랫폼 차이, lockfile | CI 로그와 로컬 diff, 런타임·이미지 버전 핀 |
| 설정 불일치 | 프로필·시크릿·기본값, 리전 | 스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화 |
| 데이터 불일치 | 비멱등 재시도, 부분 쓰기, 캐시 무효화 누락 | 멱등 키·아웃박스·트랜잭션 경계 재검토 |
권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.
배포 전에는 git add → git commit → git push 후 npm run deploy 순서를 권장합니다.
이 글에서 다루는 키워드 (관련 검색어)
C++, adapter, pattern, wrapper, interface, legacy 등으로 검색하시면 이 글이 도움이 됩니다.