어댑터 패턴

주상돈·2025년 3월 14일

TIL

목록 보기
39/53

어댑터(Adapter) 패턴


어댑터(Adapter) 패턴호환되지 않는 인터페이스를 가진 객체를, 클라이언트(사용자)가 원하는 인터페이스맞춰주는 역할을 하는 구조(Structural) 디자인 패턴이다.

이름 그대로, 맞지 않는(호환되지 않는) 인터페이스에 어댑터(변환기)를 연결해 원하는 형태로 사용할 수 있게 해준다.

예를 들어 “220V 전압 전기기기”를 “110V 콘센트”에 연결하기 위해, 전압 변환 어댑터를 사용하는 것과 비슷한 개념이다.

문제 상황 (Why Adapter?)

호환되지 않는 인터페이스

이미 잘 동작하던 클래스(또는 라이브러리)가 있지만, 새로운 요구사항(또는 외부 라이브러리, 프레임워크)에서는 다른 인터페이스를 요구하는 경우가 있다.

예를 들어, 클라이언트는 ITarget 인터페이스에서는 Request() 메서드를 호출하기 기대하지만, 기존 코드의 Adaptee클래스는 SpecificRequest() 메서드만 제공할 수 있다.

이 경우, 메서드 이름이나 매개변수, 반환 타입 등이 달라서 직접 연결하기 어려워 진다.

그렇다고 새로운 인터페이스에 맞추어 아예 새 클래스를 만드는 것은 비용이나 리스크 면에서 부담스러울 수 있다.

사실 이미 잘 만들어진 “레거시(기존)” 클래스(또는 외부 라이브러리)를 완전히 폐기하고 새로 작성하기에는 비효율적일 수 있다.

직접 레거시 클래스 안에 if문이나 오버로드 등을 추가하여 억지로 요구사항을 맞추면, 코드가 복잡해지고 결합도가 높아진다.

“호환성”만 해결한다면, 레거시 코드를 재활용하여 빠른 개발이 가능할까?

어댑터 패턴을 사용하면, 기존 코드의 “핵심 로직”은 그대로 두고, 인터페이스만 조정해줄 수 있다.

“인터페이스 변환 로직”을 별도 클래스로 격리하여, 단일 책임 원칙을 지키기 수월해진다.

어댑터(Adapter) 패턴의 핵심 아이디어

클라이언트가 기대하는 인터페이스(ITarget)와 기존에 있는 인터페이스(Adaptee) 간의 불일치를, 어댑터(Adapter) 객체가 중간에서 해결해 준다.

어댑터클라이언트가 사용하는 인터페이스(ITarget)를 구현하고, 내부적으로 Adaptee(기존 기능)를 참조하여 필요한 메서드를 호출하거나, 필요한 변환 로직을 수행한다.

어댑터 패턴을 구현하는 방식은 크게 두 가지가 있다.

  1. 객체 어댑터 방식(Object Adapter)
    • 어댑터가 Adaptee 인스턴스필드(또는 속성)로 가지고 있으며, 클라이언트의 호출을 위임(Delegation) 방식으로 연결해 준다.
    • 다중 상속을 쓸 필요가 없으므로 구현이 간편하다.
  2. 클래스 어댑터 방식(Class Adapter)
    • 어댑터가 Adaptee상속하고, 동시에 ITarget구현하는 방식.
    • 다중 상속을 지원하지 않는 언어의 경우 활용하기가 제한적이다.

무엇이 되었든 결국 어댑터가 “중간 번역가” 역할을 맡는다.

그 결과 클라이언트Adaptee의 내부 구현이나 원래 인터페이스를 신경 쓰지 않고도, 익숙한 인터페이스를 통해 그대로 기능을 사용할 수 있다.

간단 예시

새로운 인터페이스 ITarget, 기존 클래스 Adaptee

// 클라이언트가 'Request()' 메서드를 호출하기를 기대합니다.
public interface ITarget
{
    string Request();
}

// 기존 클래스(호환 안 되는 인터페이스)
public class Adaptee
{
    public string SpecificRequest()
    {
        return "Adaptee: SpecificRequest() 결과입니다.";
    }
}
  • ITarget은 클라이언트가 사용하고자 하는 “타겟 인터페이스”이며,Request() 메서드를 호출하기로 가정.
  • Adaptee는 기존 구현 클래스이며, SpecificRequest()라는 이름이 달라서 직접 사용하기 어렵습니다(많은 경우 시그니처가 달라진다).

Adapter

public class Adapter : ITarget
{
    private readonly Adaptee _adaptee;

    public Adapter(Adaptee adaptee)
    {
        _adaptee = adaptee;
    }

    public string Request()
    {
        // 클라이언트가 기대하는 "Request()" 호출을
        // 내부적으로 adaptee의 "SpecificRequest()"로 연결해 줍니다.
        return $"Adapter 변환 → {_adaptee.SpecificRequest()}";
    }
}
  • AdapterITarget구현하여, Request() 메서드를 제공.
  • 내부에서 Adaptee 인스턴스를 참조(_adaptee)하고, Request()가 호출되면 _adaptee.SpecificRequest()를 호출하여 결과를 가공(변환)해 반환.
  • 이처럼 중간 변환이 이루어지므로, 클라이언트 입장에서는 ITarget.Request()만 알면 된다.

Main

class Program
{
    static void Main()
    {
        // 1) 클라이언트는 'ITarget' 인터페이스를 기대합니다.
        ITarget target = new ClientTarget();
        Console.WriteLine(target.Request());

        // 2) 그러나 우리가 쓰고 싶은 기존 클래스(Adaptee)는 인터페이스가 달라서 바로 사용할 수 없습니다.
        Adaptee adaptee = new Adaptee();
        // adaptee.SpecificRequest() -> 클라이언트가 원하는 형태가 아닙니다.

        // 3) Adapter를 통해 Adaptee를 감싸(래핑) ITarget 형태로 변환합니다.
        ITarget adapter = new Adapter(adaptee);
        Console.WriteLine(adapter.Request());
    }
}

public class ClientTarget : ITarget
{
    public string Request()
    {
        return "ClientTarget: ITarget.Request() 동작";
    }
}

실행 결과

ClientTarget: ITarget.Request() 동작
Adapter 변환 → Adaptee: SpecificRequest() 결과입니다.
  • 첫 번째 출력은 이미 ITarget을 구현하는 ClientTarget이므로 문제 없다.
  • 두 번째는 Adaptee직접 사용하면 인터페이스가 달라서 호환이 불가능하기에, Adapter를 통해 ITarget으로 변환하여 사용한 모습.

0개의 댓글