
낯선 나라로의 여행을 떠올려 보세요. 설레는 마음으로 호텔에 도착해 스마트폰을 충전하려는데, 벽에 붙은 콘센트 모양이 우리네 '돼지코'와 달라 당황했던 경험, 아마 여러분도 한 번쯤 있으실 거예요. 분명 전기는 흐르고 있고 기기도 멀쩡하지만, 단지 '모양'이 맞지 않는다는 이유로 연결은 거부됩니다. 이때 우리를 구원해 주는 작은 도구가 바로 **어댑터(Adapter)**입니다.
소프트웨어의 세계에서도 이런 일은 비일비재합니다. 수십 년 전 선배들이 짠 견고한 '레거시(Legacy)' 코드와 오늘날 우리가 사용하는 세련된 최신 라이브러리가 만날 때, 그들은 서로의 언어를 이해하지 못해 충돌하곤 하죠. 이 난처한 상황을 해결하기 위해 등장한 것이 바로 어댑터 패턴입니다.

1. 디지털 세계의 통역사: 어댑터 패턴이란?
어댑터 패턴의 본질은 간단합니다. **"호환되지 않는 인터페이스를 가진 클래스들을 함께 작동할 수 있도록 연결하는 것"**이죠. 중간에서 양쪽의 규격을 맞춰주는 '중재자'를 세우는 전략입니다.
2. 클래스 vs 객체 어댑터: 혈통인가 계약인가?
어댑터 패턴을 공부하다 보면 마주하게 되는 두 갈래 길, 바로 클래스 어댑터와 객체 어댑터입니다. "결국 연결만 하면 되는 것 아닌가?" 싶으시겠지만, 이 둘은 '어떻게 연결하는가'라는 철학에서 큰 차이를 보입니다. 여러분의 이해를 돕기 위해, 이 두 방식을 아주 쉬운 비유로 설명해 볼게요.
① 클래스 어댑터: "나는 두 집안의 피를 이어받았다" (상속)
클래스 어댑터는 **다중 상속(Multiple Inheritance)**을 사용합니다. 어댑터가 '현대적 규격(Target)'과 '낡은 기계(Adaptee)'의 자식으로 태어나는 것이죠.
- 비유: 여러분이 한국어와 영어를 모두 완벽하게 구사하는 '혼혈 통역사'로 태어난 것과 같습니다. 여러분 자신이 곧 한국인이자 미국인인 셈이죠.
- 특징:
- 직접성: 부모의 기능을 내 것처럼 직접 사용할 수 있어 코드가 간결해 보일 수 있습니다.
- C++의 한계: C++은 다중 상속을 지원하지만, 설계가 복잡해지면 '다이아몬드 상속' 같은 치명적인 결함에 빠질 위험이 있습니다.
- 유연성 부족: 태어날 때 결정된 부모(레거시 클래스)를 바꿀 수 없습니다. 오직 그 특정 모델 전용 통역사가 됩니다.
② 객체 어댑터: "나는 유능한 전문가를 고용했다" (Composition)
객체 어댑터는 **구성(Composition)**을 사용합니다. 어댑터 내부에 레거시 객체를 '부품'으로 포함시키는 방식입니다.
- 여러분은 현대적인 비즈니스맨입니다. 영어를 못하지만, 옆에 아주 유능한 '영어 통역사(레거시 객체)'를 비서로 고용했습니다. 누군가 영어로 질문하면, 여러분은 그 질문을 비서에게 전달하고 답변을 받아 대신 말해줍니다.
- 유연성: 비서(레거시 객체)를 언제든 바꿀 수 있습니다. A 모델 프린터 비서를 쓰다가 나중에 B 모델 비서로 교체해도 여러분(어댑터)의 겉모습은 변하지 않죠.
- 현대적 권장 사항: 소프트웨어 공학의 대원칙, **"상속보다는 구성을 사용하라"**에 완벽히 부합하는 방식입니다. 대부분의 실무에서는 이 객체 어댑터를 선호합니다.
③ 한눈에 비교하기
| 구분 | 클래스 어댑터 (Class Adapter) | 객체 어댑터 (Object Adapter) |
|---|---|---|
| 핵심 기법 | 다중 상속 (is-a) | 객체 포함/구성 (has-a) |
| 장점 | 코드가 간결하고 직접 호출이 빠름 | 결합도가 낮고 런타임에 대상 교체 가능 |
| 단점 | 다중 상속의 위험, 유연성 저하 | 객체를 생성/관리하는 추가 코드가 필요함 |
| 권장 상황 | 특정 클래스에 강하게 결합되어야 할 때 | 대부분의 일반적인 상황 (추천) |
④ 객체 어댑터를 쓰세요
우리가 살고 있는 현대 소프트웨어 세계는 변화무쌍합니다. 클래스 어댑터처럼 '혈통'으로 묶여버리면, 나중에 레거시 시스템이 조금만 바뀌어도 어댑터 전체를 새로 설계해야 하는 '아마겟돈'급 상황이 올 수 있어요. 반면, 객체 어댑터는 레거시 객체를 인터페이스나 포인터로 들고 있기 때문에, 해당 레거시의 자식 클래스들이라면 누구든 받아들일 수 있습니다. 훨씬 더 너그럽고 포용력 있는 설계가 되는 것이죠.
3. 레거시의 유령과 공존하는 법
기술의 발전 속도는 눈부시지만, 현장에서는 20년 된 시스템이 여전히 핵심 엔진을 돌리고 있는 경우가 많습니다. "기존 코드가 너무 낡았으니 전부 갈아엎자"는 호기로운 주장은 비즈니스의 세계에서 흔히 **'아마겟돈'**급 재앙을 불러오곤 합니다. 비용과 위험이 너무 크기 때문이죠. 마치 산업혁명 시기 기계를 파괴하려 했던 '러다이트 운동'처럼, 과거의 유산을 부정하는 것만으로는 문제를 해결할 수 없습니다.
이때 어댑터 패턴은 일종의 '기술적 외교관' 역할을 수행합니다. 기존의 검증된 로직(Adaptee)은 그대로 두면서, 새로운 인터페이스(Target)에 맞춰 동작하도록 감싸주는(Wrap) 것이죠. 개발자는 낡은 코드의 내부를 파헤칠 필요 없이, 익숙한 인터페이스를 통해 그 기능을 고스란히 누릴 수 있게 됩니다. "야만인들을 들어오게 하되, 그들에게 우리의 예법을 가르치는 것", 그것이 어댑터의 진정한 가치입니다.
하지만 어댑터가 늘어날수록 시스템의 복잡도는 교묘하게 올라갑니다. "한 겹만 더 감싸면 돼"라는 유혹에 빠져 어댑터 위에 어댑터를 쌓다 보면, 나중에는 실제 로직이 어디에 있는지조차 알기 힘든 '스파게티 어댑터'가 탄생할지도 모릅니다.

4. 실전 설계도: C++ 구현 예시
실제로 90년대의 구식 열전사 프린터를 현대적인 POS 시스템에 통합하는 과정을 C++ 코드로 살펴보겠습니다. 아마 여러분도 이 코드를 보시면 어댑터가 어떻게 '중재'를 하는지 금방 감이 오실 거예요.

#include <iostream>
#include <string>
#include <memory>
#include <algorithm>
/**
* [Adaptee: 개조 대상]
* 90년대에 작성된 레거시 클래스입니다.
* 인터페이스가 현대적인 시스템과 맞지 않지만, 내부 로직은 여전히 유효하고 중요합니다.
*/
class LegacyThermalPrinter {
public:
// 현대적인 시스템은 'print'를 기대하지만, 이 클래스는 'printInOldWay'를 사용합니다.
void printInOldWay(const std::string& text) {
std::cout << "[Legacy Printer] 저해상도 열전사 출력 중: " << text << std::endl;
}
};
/**
* [Target: 현대적인 인터페이스]
* 현재 시스템에서 사용 중인 표준 출력 인터페이스입니다.
* 우리 시스템의 모든 클라이언트는 이 인터페이스를 기반으로 작동합니다.
*/
class IModernPrinter {
public:
virtual ~IModernPrinter() = default;
virtual void print(const std::string& text) const = 0;
};
/**
* [Adapter: 기술적 외교관]
* 레거시 클래스를 감싸서 현대적인 인터페이스로 변환해주는 역할을 합니다.
* 여기서는 '객체 어댑터' 방식을 사용하여 유연성을 높였습니다.
*/
class PrinterAdapter : public IModernPrinter {
private:
// 상속보다는 구성을 사용하라(Composition over Inheritance)는 원칙을 따릅니다.
// unique_ptr을 사용하여 현대적인 메모리 관리 기법을 적용했습니다.
std::unique_ptr<LegacyThermalPrinter> legacyPrinter;
public:
PrinterAdapter() : legacyPrinter(std::make_unique<LegacyThermalPrinter>()) {}
void print(const std::string& text) const override {
// 클라이언트의 요청을 받아서 데이터 포맷을 가공합니다. (예: 대문자 변환)
std::string processedText = text;
std::transform(processedText.begin(), processedText.end(), processedText.begin(), ::toupper);
// 레거시 시스템이 이해할 수 있는 언어로 '번역'하여 전달합니다.
std::cout << "[Adapter] 현대적 요청을 레거시 명령으로 변환 중..." << std::endl;
legacyPrinter->printInOldWay(processedText);
}
};
/**
* [Client: 현대적인 시스템]
* 어댑터 덕분에 레거시 클래스의 존재를 몰라도 표준 인터페이스로 통신할 수 있습니다.
*/
void executePrintJob(const IModernPrinter& printer, const std::string& message) {
// 클라이언트는 그저 'print'가 있을 것이라 믿고 호출합니다.
// 이 단계에서 클라이언트와 레거시 시스템 사이의 '디커플링(Decoupling)'이 완성됩니다.
printer.print(message);
}
/**
* 프로그램의 실행 시점(Entry Point)입니다.
* 인터페이스의 불일치가 어댑터를 통해 어떻게 해결되는지 보여줍니다.
*/
int main() {
std::cout << "--- 현대적인 결제 시스템 가동 ---" << std::endl;
// 클라이언트는 PrinterAdapter가 내부적으로 어떤 낡은 코드를 쓰는지 알 필요가 없습니다.
// 추상화된 인터페이스인 IModernPrinter에 의존합니다.
std::unique_ptr<IModernPrinter> myPrinter = std::make_unique<PrinterAdapter>();
// 현대적인 방식으로 명령을 내리지만, 실제로는 90년대 프린터가 동작합니다.
executePrintJob(*myPrinter, "hello, adapter pattern!");
std::cout << "--- 출력 작업 완료 ---" << std::endl;
return 0;
}
5. 현대 C++에서의 변주: 래퍼와 인터페이스의 미래
과거의 어댑터 패턴이 클래스 구조 설계에 집중했다면, 현대 C++(C++11 이후)에서는 조금 더 유연한 모습으로 진화하고 있습니다. std::function이나 람다(Lambda) 표현식은 아주 가벼운 어댑터 역할을 훌륭히 수행합니다. 굳이 거창한 클래스를 만들지 않아도, 즉석에서 인터페이스를 변환해 주는 '일회용 통역사'를 고용할 수 있게 된 셈이죠.
또한, '컨셉(Concepts)' 도입은 우리가 어댑터를 설계하는 방식에 근본적인 질문을 던집니다. "과연 타입을 맞춰주는 것이 최선인가, 아니면 동작의 요구사항을 정의하는 것이 우선인가?"에 대한 고민이죠. 여러분은 어떻게 생각하시나요? 어쩌면 미래의 코드는 어댑터라는 별도의 구조 없이도 스스로 형태를 바꾸며 연결되는 **'유동적 인터페이스'**의 시대로 나아갈지도 모릅니다.
결국 중요한 것은 무엇일까요?
결국 어댑터 패턴이 우리에게 가르쳐 주는 것은 **'존중'과 '공존'**입니다. 과거의 유산을 부정하지 않으면서도 미래의 변화를 수용하는 유연함, 그것이 바로 숙련된 아키텍트의 자질이 아닐까 싶습니다.
감이 좀 오시나요? 클래스 어댑터가 **"나는 두 세계에 모두 속해 있다"**라고 외치는 강한 자의 방식이라면, 객체 어댑터는 **"나는 두 세계를 연결하는 다리를 놓겠다"**라고 말하는 현명한 중재자의 방식입니다. 여러분이 지금 마주한 코드가 아주 단순하고 절대 변하지 않을 것 같다면 클래스 어댑터도 나쁜 선택은 아니에요. 하지만 미래의 불확실성이 두렵다면, 주저하지 말고 객체 어댑터를 선택하세요.
결국 중요한 건 코드의 화려함이 아니라, 그 코드를 읽고 유지보수할 우리들의 마음일 테니까요. 과연 여러분의 다음 어댑터는 어떤 세상을 연결하게 될까요? 그 결과가 어떻게 될지는 아직 미지수지만, 지켜보는 것도 흥미로운 관전 포인트가 될 것 같네요.