Post

어댑터(Adapter) 패턴

클래스 기반 어댑터 패턴 이해

어댑터(Adapter) 패턴

어댑터 패턴은 서로 다른 인터페이스를 가진 클래스들을 호환되도록 변환하는 디자인 패턴이다.

주로 기존 코드와 새로운 코드 간의 호환성을 유지하면서 변경 없이 사용할 수 있도록 도와준다.

주요 개념

  • 클라이언트(Client)
    • 타겟 인터페이스를 사용하려고 하는 코드
  • 타겟(Target)
    • 클라이언트가 기대하는 인터페이스
    • 클라이언트가 기대하는 추상 인터페이스 또는 기본 클래스가 될 수도 있음
  • 어댑터(Adapter)
    • 타겟 인터페이스를 구현하고, 실제 사용하려는 클래스를 감싸서 변환 역할을 수행하는 클래스
    • 인터페이스를 단순히 변환하는 것 외에도, 데이터 변환, 추가 로직 수행 등의 역할도 할 수 있음
  • 어댑티(Adaptee)
    • 기존에 존재하는 클래스지만 클라이언트가 요구하는 인터페이스를 제공하지 않는 클래스
    • 레거시 코드, 서드파티 라이브러리도 포함될 수 있음
sequenceDiagram
    participant Client
    participant Adapter
    participant Adaptee

    Client->>Adapter: targetRequest()
    Adapter->>Adaptee: adapteeRequest()
    Adaptee-->>Adapter: adapteeResponse()
    Adapter-->>Client: targetResponse() 

장점

  • 기존 코드 재사용
    • 기존 클래스를 변경하지 않고도 새로운 인터페이스에 맞출 수 있어 레거시 코드 활용 가능
    • 외부 라이브러리나 오래된 시스템과 통합할 때 유용
  • 유연성과 확장성 증가
    • 클라이언트는 어댑터 인터페이스만 사용하므로, 내부 구현이 변경되더라도 영향을 받지 않음
    • OCP(Open-Closed Principle, 개방-폐쇄 원칙)을 준수하여 코드 변경 없이 새로운 어댑터를 추가가능
  • 서로 다른 인터페이스를 가진 클래스 간의 호환성 제공
    • 기존 시스템과 새로운 시스템을 통합할 때, 다양한 인터페이스를 단일한 방식으로 사용가능
  • 의존성 감소
    • 클라이언트는 어댑터를 통해 대상 클래스(Adaptee)와 간접적으로 연결되므로, 인터페이스만 유지되면 내부 구현이 변경되어도 클라이언트 코드에 영향을 주지 않음

주의점

  • 성능 오버헤드
    • 어댑터 객체를 생성하고, 인터페이스 변환을 수행해야 하므로 직접 호출하는 것보다 성능이 다소 저하될 수 있음
    • 빈번한 호출이 발생하는 시스템에서는 성능고려 필요
  • 유지보수성 확인
    • 클래스가 많아질수록 코드가 복잡해지고 유지보수가 어려워질 수 있음
    • 인터페이스가 너무 많이 변환되면 코드 가독성이 떨어지고 유지보수가 어려워짐
    • 여러 개의 어댑터가 존재하면 어댑터-클래스 연관성을 파악하기 어려울 수 있음
  • 한계점 검토
    • 기존 클래스의 동작 방식이 클라이언트 요구사항과 너무 다를 경우, 어댑터로 해결하기 어려울 수 있음
    • 어댑터 패턴을 사용하기 전에, 기존 코드를 직접 수정하는 것이 더 좋은 해결책인지 검토필요

예시

C++을 이용하여 예시 코드를 작성하였다. 아래 링크에서 전체를 확인할 수 있다.

https://github.com/grade-e/adapter-cpp-container

  • 클라이언트는 IPrinter 인터페이스를 요구
  • 기존 클래스(OldPrinter)는 해당 인터페이스와 호환되지 않음
  • Adapter 패턴을 이용하여 OldPrinter를 IPrinter와 호환시켜야 함

Class diagram

classDiagram
    class IPrinter {
        <<interface>>
        +printDocument(text: string)
    }

    class ModernPrinter {
        +printDocument(text: string)
    }

    class OldPrinter {
        +legacyPrint(text: string)
    }

    class PrinterAdapter {
        +printDocument(text: string)
    }

    IPrinter <|.. ModernPrinter
    IPrinter <|.. PrinterAdapter
    PrinterAdapter --> OldPrinter

코드

IPrinter(Target)

  • 클라이언트는 IPrinter 인터페이스를 사용하여 프린터를 조작
  • printDocument() 메서드를 정의하는 추상 인터페이스
1
2
3
4
5
class IPrinter {
  public:
    virtual void printDocument(const std::string& text) = 0;
    virtual ~IPrinter() = default;
};

ModernPrinter

  • 최신 프린터, IPrinter 구현되어있음
1
2
3
4
5
6
class ModernPrinter : public IPrinter {
   public:
    void printDocument(const std::string& text) override {
        std::cout << "Modern Printer: " << text << std::endl;
    }
};

OldPrinter(Adaptee)

  • legacyPrint()라는 메서드만 제공하며, IPrinter와 호환되지 않음
1
2
3
4
5
6
class OldPrinter {
   public:
    void legacyPrint(const std::string& text) {
        std::cout << "Old Printer: " << text << std::endl;
    }
};

PrinterAdapter(Adapter)

  • IPrinter를 구현하고, OldPrinter를 포장하여 변환 역할을 수행
  • printDocument()를 호출하면 legacyPrint()가 실행되도록 변환됨
1
2
3
4
5
6
7
8
9
10
class PrinterAdapter : public IPrinter {
   private:
    std::shared_ptr<OldPrinter> oldPrinter;
   public:
    PrinterAdapter(std::shared_ptr<OldPrinter> oldPrinter)
        : oldPrinter(oldPrinter) {}
    void printDocument(const std::string& text) override {
        oldPrinter->legacyPrint(text);
    }
};

main(client)

  • class 형태에 상관없이 printDocument 메서드만 호출하면 됨
1
2
3
4
5
6
7
8
9
int main() {
    shared_ptr<IPrinter> modernPrinter = make_shared<ModernPrinter>();
    modernPrinter->printDocument("Hello from Modern Printer!");

    shared_ptr<OldPrinter> oldPrinter = make_shared<OldPrinter>();
    shared_ptr<IPrinter> printer = make_shared<PrinterAdapter>(oldPrinter);
    printer->printDocument("Hello, from Old Printer via Adapter!");
    return 0;
}

실행 결과

1
2
Modern Printer: Hello from Modern Printer!
Old Printer: Hello, from Old Printer via Adapter!
This post is licensed under CC BY 4.0 by the author.