어댑터 패턴 (Adapter Pattern)

한눈에 보기

호환되지 않는 인터페이스를 클라이언트가 기대하는 형태로 변환해 함께 동작하게 만드는 구조 패턴이다. 타깃, 어댑티, 어댑터, 클라이언트의 협력 구조와 장단점, 유사 패턴과의 차이를 정리한다.

최종 업데이트 2026.09.17

어댑터 패턴(Adapter Pattern)은 서로 호환되지 않는 인터페이스를 가진 클래스를 클라이언트가 기대하는 인터페이스로 변환해 함께 동작하게 만드는 구조 디자인 패턴이다. 어댑터 패턴을 적용하면 기존 클래스나 외부 라이브러리의 코드를 한 줄도 수정하지 않고 새로운 시스템에 연결할 수 있다. 해외여행에서 콘센트 규격이 다를 때 변환 플러그를 끼워 쓰는 상황에 자주 비유된다.

패턴의 의도

소프트웨어를 개발하다 보면 이미 검증된 클래스를 재사용하고 싶은데 그 클래스의 메서드 시그니처가 현재 시스템이 요구하는 인터페이스와 맞지 않는 상황을 자주 만난다. 외부 라이브러리는 소스 코드를 고칠 수 없고, 레거시 코드는 수정했다가 기존 기능이 깨질 위험이 크다. 이때 두 인터페이스 사이에 변환 계층을 하나 두면 양쪽 모두 손대지 않고 연결할 수 있다.

어댑터 패턴의 핵심 의도는 "인터페이스 불일치 때문에 함께 쓸 수 없던 클래스들을 협력하게 만든다"는 것이다. 어댑터는 클라이언트가 기대하는 인터페이스를 구현하고, 내부에서는 기존 객체의 메서드를 호출하면서 필요한 형 변환이나 파라미터 매핑을 수행한다. 즉 어댑터는 새로운 기능을 추가하는 존재가 아니라 이미 있는 기능의 호출 방식을 바꿔 주는 번역자 역할을 한다.

구조와 참여 객체

어댑터 패턴에는 네 종류의 참여자가 등장한다. 각 참여자의 역할을 정확히 구분해서 설명할 수 있어야 면접에서도 흔들리지 않는다.

  • 타깃(Target): 클라이언트가 사용하려는 인터페이스다. 시스템이 요구하는 표준 규격에 해당한다.
  • 어댑티(Adaptee): 재사용하고 싶지만 인터페이스가 맞지 않는 기존 클래스다. 레거시 코드나 외부 라이브러리가 여기에 해당한다.
  • 어댑터(Adapter): 타깃 인터페이스를 구현하면서 내부에서 어댑티를 호출하는 변환 계층이다. 두 세계를 잇는 다리 역할을 한다.
  • 클라이언트(Client): 타깃 인터페이스에만 의존해서 동작하는 코드다. 어댑티의 존재를 전혀 모른다.

호출 흐름은 단순하다. 클라이언트가 타깃 인터페이스의 메서드를 호출하면 어댑터가 그 요청을 받아 어댑티가 이해할 수 있는 형태로 바꿔 전달하고, 어댑티의 결과를 다시 타깃이 약속한 형태로 돌려준다. 클라이언트 입장에서는 어댑터도 그저 타깃의 구현체 중 하나일 뿐이다.

Java 코드 예시

새 결제 시스템은 원화 금액을 정수로 받는 인터페이스를 요구하는데, 사내 레거시 과금 모듈은 문자열 형태의 금액만 받는 상황을 가정한다.

// 타깃: 새 시스템이 요구하는 인터페이스
public interface PaymentProcessor {
    void pay(int amountWon);
}

// 어댑티: 수정할 수 없는 레거시 과금 모듈
public class LegacyBillingSystem {
    public void charge(String formattedAmount) {
        System.out.println(formattedAmount + " 청구 완료");
    }
}

// 어댑터: 타깃을 구현하고 내부에서 어댑티를 호출
public class BillingAdapter implements PaymentProcessor {
    private final LegacyBillingSystem legacy;

    public BillingAdapter(LegacyBillingSystem legacy) {
        this.legacy = legacy;
    }

    @Override
    public void pay(int amountWon) {
        // 정수 금액을 레거시가 요구하는 문자열 형식으로 변환
        legacy.charge(amountWon + "원");
    }
}

// 클라이언트: 타깃 인터페이스에만 의존
PaymentProcessor processor = new BillingAdapter(new LegacyBillingSystem());
processor.pay(15000);

클라이언트 코드는 PaymentProcessor만 알고 있으므로, 나중에 레거시 모듈을 완전히 걷어내고 새 구현체로 교체해도 클라이언트는 전혀 바뀌지 않는다. 어댑터가 변경의 파급 범위를 한 클래스 안으로 가둔 것이다.

장단점과 사용 시점

가장 큰 장점은 단일 책임 원칙과 개방 폐쇄 원칙을 자연스럽게 지킬 수 있다는 점이다. 인터페이스 변환 로직이 어댑터 한 곳에 모이므로 비즈니스 로직과 변환 로직이 분리되고, 기존 코드를 수정하지 않고 새 어댑터를 추가하는 방식으로 확장할 수 있다. 또한 테스트 시에 타깃 인터페이스의 가짜 구현을 주입하기 쉬워져 테스트 용이성도 좋아진다.

단점은 클래스 수가 늘어나 코드의 전체 복잡도가 올라간다는 것이다. 변환할 인터페이스가 한두 개뿐이고 어댑티를 직접 수정할 수 있는 상황이라면, 어댑터를 만들기보다 어댑티 자체를 고치는 편이 단순할 수 있다. 어댑터를 여러 겹으로 겹쳐 쓰면 호출 경로 추적이 어려워지므로 남용은 피해야 한다.

사용 시점은 명확하다. 수정할 수 없는 외부 라이브러리를 도입할 때, 레거시 시스템을 점진적으로 교체할 때, 서로 다른 벤더의 SDK를 하나의 내부 인터페이스로 통일하고 싶을 때 어댑터 패턴이 잘 맞는다.

설계 시 한 가지 원칙을 지키는 것이 좋다. 어댑터 안에는 형식 변환과 위임 로직만 두고 비즈니스 규칙을 넣지 않는 것이다. 어댑터에 도메인 판단이 섞이기 시작하면 변환 계층이라는 정체성이 흐려지고, 나중에 어댑티를 교체할 때 함께 옮겨야 할 로직이 생겨 버린다. 또한 변환 과정에서 손실이 생기는 경우, 예를 들어 어댑티가 제공하지 않는 기능을 타깃이 요구한다면, 그 메서드에서 명시적으로 예외를 던질지 기본 동작을 제공할지 팀 차원의 규칙을 정해 두어야 호출자가 혼란을 겪지 않는다.

유사 패턴과 비교

어댑터는 다른 래퍼(Wrapper) 계열 구조 패턴과 자주 혼동된다. 무엇을 위해 감싸는지가 결정적인 차이다.

패턴목적인터페이스 변화
어댑터(Adapter)호환되지 않는 인터페이스를 변환해 연결한다다른 인터페이스로 바꾼다
데코레이터(Decorator)객체에 책임과 기능을 동적으로 덧붙인다같은 인터페이스를 유지한다
퍼사드(Facade)복잡한 서브시스템을 단순한 창구 하나로 감춘다새로운 단순 인터페이스를 만든다
프록시(Proxy)접근 제어, 지연 로딩 등 대리 역할을 수행한다같은 인터페이스를 유지한다

정리하면 어댑터는 "이미 있는 것을 다른 규격에 맞춘다", 데코레이터는 "기능을 더한다", 퍼사드는 "복잡함을 숨긴다", 프록시는 "접근을 통제한다"로 기억하면 된다.

면접에서 이렇게 물어본다

  • 어댑터 패턴이 필요한 상황을 실제 프로젝트 경험이나 구체적인 시나리오로 설명해 보라.
  • 어댑터 패턴의 네 가지 참여 객체(타깃, 어댑티, 어댑터, 클라이언트)와 호출 흐름을 그림 없이 말로 설명할 수 있는가.
  • 어댑터 패턴과 데코레이터 패턴은 둘 다 객체를 감싸는데, 목적이 어떻게 다른가.
  • 어댑터 패턴이 개방 폐쇄 원칙(OCP)과 어떤 관련이 있는지 설명해 보라.
  • 퍼사드 패턴 대신 어댑터 패턴을 선택해야 하는 기준은 무엇인가.
디자인 패턴 문서 전체 보기

이 개념, 내 코드에서는 어떻게 물어볼까요

데브핏(DevFit)에 채용공고 URL을 붙여 넣고 레포지토리를 연결하면, 내가 쓴 커밋에서 예상 질문과 어필 포인트를 만듭니다. 하루 10회까지 무료입니다. 합불 판정이나 지원자 랭킹은 만들지 않고, 최종 판단은 사람이 합니다.

공고 넣고 시작하기