포스트

Adapter Pattern

기존 Adaptee 클래스를 수정하지 않고 클라이언트가 원하는 Target 인터페이스로 변환하는 패턴. Adapter가 Adaptee를 보유해 호출을 위임한다.

Adapter Pattern

난이도 입문 · 선행 없음

한 줄 요약

이미 있는 클래스(Adaptee)를 손대지 않고, 클라이언트가 기대하는 인터페이스(Target)로 모양만 바꿔주는 중간 객체를 두는 패턴. 220V 콘센트와 110V 플러그 사이의 변환 어댑터와 같은 발상이다.

어떤 문제를 푸는가

이미 동작 중인 결제 시스템이 있다. 시스템은 PaymentGateway 인터페이스를 통해 결제한다.

1
2
3
4
5
class PaymentGateway {
public:
    virtual void pay(int amount) = 0;
    virtual ~PaymentGateway() = default;
};

여기에 외부 PG사 SDK를 붙여야 하는데, 그 SDK는 인터페이스가 우리와 다르다.

1
2
3
4
5
6
class LegacyPaymentSDK {
public:
    void makePayment(double amountInDollars, const std::string& currency) {
        std::cout << amountInDollars << " " << currency << " 결제 완료" << std::endl;
    }
};

선택지:

  • PaymentGateway를 수정해서 SDK 모양에 맞춘다 → 기존 호출자 전부 깨진다.
  • LegacyPaymentSDK를 수정한다 → 외부 라이브러리라 수정 불가능.
  • ❌ 호출하는 쪽마다 if/else로 분기 → 새 PG 붙일 때마다 호출 사이트가 늘어난다.

패턴 적용 후

PaymentGateway를 구현하는 어댑터 클래스를 두고, 그 안에서 SDK 호출 모양으로 변환한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>
#include <memory>
#include <string>

class LegacyPaymentSDK {
public:
    void makePayment(double amountInDollars, const std::string& currency) {
        std::cout << amountInDollars << " " << currency << " 결제 완료" << std::endl;
    }
};

class PaymentGateway {
public:
    virtual void pay(int amount) = 0;
    virtual ~PaymentGateway() = default;
};

class LegacyPaymentAdapter : public PaymentGateway {
    std::unique_ptr<LegacyPaymentSDK> sdk;
public:
    LegacyPaymentAdapter(std::unique_ptr<LegacyPaymentSDK> sdk)
        : sdk(std::move(sdk)) {}

    void pay(int amountInWon) override {
        constexpr double WON_PER_DOLLAR = 1300.0;
        double dollars = amountInWon / WON_PER_DOLLAR;
        sdk->makePayment(dollars, "USD");
    }
};

void checkout(PaymentGateway& gateway, int amount) {
    gateway.pay(amount);
}

int main() {
    LegacyPaymentAdapter adapter(std::make_unique<LegacyPaymentSDK>());
    checkout(adapter, 13000);
    // 출력: 10 USD 결제 완료
}

달라진 점:

  • 호출하는 쪽(checkout)은 SDK의 존재를 모른다. PaymentGateway만 안다.
  • 다른 PG사가 붙어도 어댑터만 새로 작성하면 된다. 본체는 무손상.
  • 단위·통화 변환 같은 자잘한 변환 로직이 어댑터에 격리된다.

구조

1
2
3
4
Client ──uses──▶ Target (interface)
                    ▲
                    │ implements
                  Adapter ──has──▶ Adaptee (기존 클래스)
  • Target: 클라이언트가 사용하는 인터페이스 (PaymentGateway)
  • Adapter: Target을 구현하고 Adaptee로 위임 (LegacyPaymentAdapter)
  • Adaptee: 모양이 다른 기존 클래스 (LegacyPaymentSDK)
  • Client: Target만 보고 동작 (checkout)

객체 어댑터 vs 클래스 어댑터

위 예제는 객체 어댑터다. Adapter가 Adaptee를 멤버로 보유한다.

클래스 어댑터는 다중 상속을 사용한다. Target과 Adaptee를 동시에 상속한다.

1
2
3
4
5
6
class LegacyPaymentAdapter : public PaymentGateway, private LegacyPaymentSDK {
public:
    void pay(int amountInWon) override {
        makePayment(amountInWon / 1300.0, "USD");
    }
};
  • 객체 어댑터: 다중 상속이 없는 언어(Java, C#)에서도 동작. 런타임에 Adaptee 교체 가능. 일반적으로 선호됨.
  • 클래스 어댑터: 코드가 짧다. 단, Adaptee의 private 멤버에는 여전히 접근 불가, 그리고 다중 상속의 함정.

실전 사례

  • std::back_inserter: 컨테이너를 출력 이터레이터처럼 보이게 어댑팅. std::copy(src.begin(), src.end(), std::back_inserter(dst)).
  • Java Arrays.asList(arr): 배열을 List 인터페이스로 어댑팅.
  • Slf4j: 다양한 로깅 라이브러리(Log4j, Logback, JUL)를 같은 API로 사용하도록 어댑팅.
  • PG 통합 모듈: 여러 결제사 SDK를 사내 공통 인터페이스로 모으는 패턴이 그대로.

비슷한 패턴과의 차이

 AdapterDecoratorFacadeProxy
인터페이스바꿈유지새로 정의(단순화)유지
의도모양 변환기능 추가복잡한 서브시스템 단순화접근 제어

Adapter는 “모양이 안 맞아서 못 끼우는 것”을 끼울 수 있게 만든다. 기능을 늘리거나 줄이지 않는다.

안티패턴 / 주의

  • 어댑터가 변환 이상의 일을 시작하면 위험. 단위 변환·이름 매핑 정도는 OK. 비즈니스 로직이 어댑터에 쌓이면 책임 경계가 흐려진다.
  • 양방향 어댑터의 유혹. Adaptee → Target만이 아니라 Target → Adaptee 변환도 욱여넣으면 복잡도가 두 배. 정말 양방향이 필요한지 점검.
  • 본체를 고칠 수 있다면 굳이 어댑터를 만들지 마라. 어댑터는 “내가 못 고치는 코드”에 대한 대응. 내가 만든 코드 두 개를 어댑터로 잇는 건 인터페이스 설계 실패의 신호.

스스로 점검

1. 외부 SDK는 USD만 받는데 우리 시스템은 KRW로 호출한다. 통화 변환 로직을 어디에 두는 게 맞나?

Adapter 안. 단위·통화 변환·이름 매핑 같은 자잘한 변환은 어댑터의 정통 역할이다. 호출자도 SDK도 모르는 채로 어댑터가 책임진다.

2. 내가 만든 두 모듈 사이에 어댑터를 두고 있다. 이 신호가 의미하는 건?

인터페이스 설계가 잘못된 신호. 어댑터는 “내가 못 고치는 외부 코드”에 대한 대응이다. 둘 다 내 코드면 한쪽 인터페이스를 다른 쪽에 맞추는 게 정공법.

3. 어댑터에 비즈니스 로직(예: “1만원 이상이면 무료배송”)을 넣으면 안 되는 이유는?

책임 경계가 흐려진다. 어댑터의 역할은 모양 변환. 비즈니스 로직이 섞이면 다음 PG사로 갈아끼울 때 로직까지 함께 흔들린다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.