← Posts

GoF · Structural · Adapter

Adapter

내 코드가 기대하는 인터페이스와 남의 코드가 주는 인터페이스가 안 맞을 때. 사이에 번역기를 끼워 붙게 만드는 구조 패턴.

목차
  1. 인터페이스가 안 맞아 못 붙인다
  2. 어댑터 = 안 맞는 인터페이스를 맞게 바꾼다
  3. 왜 그냥 고치지 못하나
  4. 어댑터를 끼운다
  5. 변환은 어댑터 안에서 일어난다
  6. 클라이언트는 어댑터를 모른다
  7. 객체 어댑터와 클래스 어댑터
  8. 어댑터가 하는 건 ‘번역’뿐이다
  9. 실무: 레거시·외부 연동의 이음매
  10. 정리

인터페이스가 안 맞아 못 붙인다

결제 기능을 붙인다고 하자. 내 코드는 이런 인터페이스를 기대한다.

java
public interface PaymentProcessor {
    void pay(int amountWon);
}

그런데 실제로 연동할 외부 결제 라이브러리는 이렇게 생겼다.

java
public class LegacyPayGateway {
    public void sendPayment(long cents, String currency) { ... }
}

하는 일은 결제로 같은데, 모양이 안 맞는다. 내 코드는 pay(원)을 부르고 싶은데 라이브러리는 sendPayment(센트, 통화)를 내놓는다. 이름도 다르고 인자도 다르다. 이대로는 내 코드에 못 끼운다.

어댑터 = 안 맞는 인터페이스를 맞게 바꾼다

어댑터 패턴이 이걸 푼다.

한 인터페이스를, 클라이언트가 기대하는 다른 인터페이스로 변환해서, 원래 못 맞던 둘이 함께 동작하게 한다.

해외에서 콘센트 모양이 달라도 여행용 어댑터를 끼우면 꽂히듯, 코드 사이에도 모양을 맞춰주는 어댑터를 끼운다. 전기(하는 일)는 그대로고 모양만 바꿔준다.

왜 그냥 고치지 못하나

“라이브러리 코드를 내 인터페이스에 맞게 고치면 되지 않나” 싶지만, 대개 못 고친다.

  • 남의 코드다. 외부 라이브러리나 레거시 시스템이라 손댈 수 없다.
  • 고칠 수 있어도, 그걸 이미 쓰는 다른 곳들이 다 깨진다.

바꿀 수 없는 걸 바꾸려 하지 말고, 사이에 맞춤 조각을 하나 끼우는 게 어댑터의 발상이다.

어댑터를 끼운다

내 인터페이스(PaymentProcessor)를 구현하면서, 속으로는 라이브러리를 부르는 클래스를 만든다.

java
public class LegacyPayAdapter implements PaymentProcessor {

    private final LegacyPayGateway gateway;   // 감쌀 대상

    public LegacyPayAdapter(LegacyPayGateway gateway) {
        this.gateway = gateway;
    }

    @Override
    public void pay(int amountWon) {
        long cents = amountWon * 100L;         // 원 → 센트로 변환
        gateway.sendPayment(cents, "KRW");     // 라이브러리에 맞춰 위임
    }
}

LegacyPayAdapter는 밖으로는 PaymentProcessor인 척하고, 안에서는 LegacyPayGateway를 부른다. 내 코드는 PaymentProcessor.pay()를 부르면 되고, 그 호출이 어댑터를 거쳐 라이브러리로 번역된다.

변환은 어댑터 안에서 일어난다

어댑터의 일은 pay(amountWon) 안에 다 들어 있다. 인자 이름과 단위를 맞추고(원→센트), 없는 인자를 채우고(통화 "KRW"), 메서드 이름을 이어준다(paysendPayment).

이 지저분한 변환이 딱 한 곳, 어댑터 안에 모인다. 내 코드에도, 라이브러리에도 번역 코드가 새지 않는다. 둘 사이의 이음매를 어댑터가 혼자 떠안는 것이다.

클라이언트는 어댑터를 모른다

쓰는 쪽은 PaymentProcessor만 안다. 그 뒤에 어댑터가 있는지, 라이브러리가 무엇인지 모른다.

diagramdiagram

그래서 나중에 다른 결제 라이브러리로 바꿔도, 그 라이브러리용 어댑터만 새로 쓰면 된다. 클라이언트는 그대로다. 어댑터가 변경의 충격을 흡수하는 완충재가 된다.

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

어댑터를 만드는 방식은 둘이다.

  • 객체 어댑터 - 위 코드처럼 대상을 필드로 들고 위임한다(조합). 자바에서 흔히 쓰는 방식이고, 여러 대상을 감쌀 수 있어 유연하다.
  • 클래스 어댑터 - 대상을 상속받아 인터페이스를 구현한다. 그런데 자바는 클래스 다중 상속이 없어서, 대상과 인터페이스를 동시에 상속하기 어렵다. 그래서 자바에선 대개 객체 어댑터를 쓴다.

둘의 목적은 같다. 안 맞는 모양을 맞는 모양으로 바꾼다.

어댑터가 하는 건 ‘번역’뿐이다

한 가지 분명히 해두면, 어댑터는 기능을 더하거나 바꾸지 않는다. 하는 일은 그대로고, 부르는 모양만 바꾼다. paysendPayment로 이어줄 뿐, 결제 로직에 뭘 얹지 않는다.

이 “순수 번역”이 어댑터의 정체성이다. 감싸긴 하지만 그 안에서 새 행동을 만들지 않는다. 모양이 안 맞아 못 붙던 둘을 붙게만 해준다.

실무: 레거시·외부 연동의 이음매

실무에서 어댑터는 내 코드와 남의 코드가 만나는 지점에 거의 항상 있다. 외부 API, 결제사, 레거시 모듈처럼 내가 못 고치는 것을 내 인터페이스에 맞춰 감쌀 때다.

프레임워크 안에도 흔하다. Spring MVC의 HandlerAdapter는 제각각인 핸들러들을 DispatcherServlet이 기대하는 하나의 형태로 맞춰준다. 자바 표준의 InputStreamReader는 바이트 스트림을 문자 스트림으로 맞춰주는 어댑터다. 서로 다른 것들을 이어붙이는 곳이면 어김없이 이 패턴이 있다.

정리

어댑터란안 맞는 인터페이스를 클라이언트가 기대하는 모양으로 변환한다
문제하는 일은 같은데 인터페이스가 달라 못 붙인다 (남의 코드라 못 고침)
고침내 인터페이스를 구현하고 속으로 대상을 부르는 어댑터를 끼운다
변환 위치이름·인자·단위 변환이 어댑터 한 곳에 모인다
두 방식객체 어댑터(조합, 자바 기본) · 클래스 어댑터(상속)
정체성기능은 안 더한다. 모양만 번역한다

바꿀 수 없는 것을 바꾸려 하지 않고, 사이에 번역기를 끼워 붙게 만드는 것. 내 코드와 남의 코드의 이음매를 한 곳에 모으는 것 - 그게 어댑터다.

관련 글