어댑터(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.