어댑터 패턴
의도
어댑터는 호환되지 않는 인터페이스를 가진 객체들이 협업할 수 있도록 하는 구조적 디자인 패턴입니다.
문제
주식 시장 모니터링 앱을 만들고 있고, 이 앱은 여러 소스에서 주식 데이터를 XML 형식으로 다운로드한 후 사용자에게 보기 좋은 차트들과 다이어그램들을 표시한다고 상상해 봅시다.
어느 시점에 당신은 타사의 스마트 분석 라이브러리를 통합하여 당신의 앱을 개선하기로 결정했습니다. 그런데 함정이 있습니다: 이 분석 라이브러리는 JSON 형식의 데이터로만 작동한다는 것입니다.
당신은 이 라이브러리를 XML과 작동하도록 변경할 수 있으나, 그러면 라이브러리에 의존하는 일부 기존 코드가 손상될 수 있습니다. 또 처음부터 타사의 라이브러리 소스 코드에 접근하는 것이 불가능하여 위의 해결 방식을 사용하지 못할 수도 있습니다.
해결책
당신은 어댑터를 만들 수 있습니다. 어댑터는 한 객체의 인터페이스를 다른 객체가 이해할 수 있도록 변환하는 특별한 객체입니다.
어댑터는 변환의 복잡성을 숨기기 위하여 객체 중 하나를 래핑(포장)합니다. 래핑된 객체는 어댑터를 인식하지도 못합니다. 예를 들어 미터 및 킬로미터 단위로 작동하는 객체를 모든 데이터를 피트 및 마일과 같은 영국식 단위로 변환하는 어댑터로 래핑할 수 있습니다.
어댑터는 데이터를 다양한 형식으로 변환할 수 있을 뿐만 아니라 다른 인터페이스를 가진 객체들이 협업하는 데에도 도움을 줄 수 있으며, 대략 다음과 같이 작동합니다:
- 어댑터는 기존에 있던 객체 중 하나와 호환되는 인터페이스를 받습니다.
- 이 인터페이스를 사용하면 기존 객체는 어댑터의 메서드들을 안전하게 호출할 수 있습니다.
- 호출을 수신하면 어댑터는 이 요청을 두 번째 객체에 해당 객체가 예상하는 형식과 순서대로 전달합니다.
때로는 양방향으로 호출을 변환할 수 있는 양방향 어댑터를 만드는 것도 가능합니다.
다시 당신의 주식 시장 앱을 살펴봅시다. 당신은 형식이 호환되지 않는 문제를 해결하기 위해 당신의 코드와 직접 작동하는 분석 라이브러리의 모든 클래스에 대한 XML->JSON 변환 어댑터를 만듭니다. 그 후 이러한 어댑터들을 통해서만 해당 라이브러리와 통신하도록 코드를 조정합니다. 어댑터는 호출을 받으면 들어오는 XML 데이터를 JSON 구조로 변환한 후 해당 호출을 래핑된 분석 객체의 적절한 메서드들에 전달합니다.
실제상황 적용
미국에서 유럽으로 처음 여행을 가서 노트북을 충전하려고 하면 깜짝 놀랄지도 모릅니다. 전원 플러그와 소켓은 국가마다 표준이 달라 미국 플러그가 독일 소켓에 맞지 않을 수 있기 때문입니다. 이 문제는 미국식 소켓과 유럽식 플러그가 있는 전원 플러그 어댑터를 사용하면 해결할 수 있습니다.
구조
객체 어댑터
이 구현은 객체 합성 원칙을 사용합니다. 어댑터는 한 객체의 인터페이스를 구현하고 다른 객체는 래핑합니다. 위 합성은 모든 인기 있는 프로그래밍 언어로 구현할 수 있습니다.
-
클라이언트는 프로그램의 기존 비즈니스 로직을 포함하는 클래스입니다.
-
클라이언트 인터페이스는 다른 클래스들이 클라이언트 코드와 공동 작업할 수 있도록 따라야 하는 프로토콜을 뜻합니다.
-
서비스는 일반적으로 타사 또는 레거시의 유용한 클래스를 뜻합니다. 클라이언트는 서비스 클래스를 직접 사용할 수 없습니다. 왜냐하면 서비스 클래스는 호환되지 않는 인터페이스를 가지고 있기 때문입니다.
-
어댑터는 클라이언트와 서비스 양쪽에서 작동할 수 있는 클래스로, 서비스 객체를 래핑하는 동안 클라이언트 인터페이스를 구현합니다. 어댑터는 어댑터 인터페이스를 통해 클라이언트로부터 호출들을 수신한 후 이 호출을 래핑된 서비스 객체가 이해할 수 있는 형식의 호출들로 변환합니다.
-
클라이언트 코드는 클라이언트 인터페이스를 통해 어댑터와 작동하는 한 구상 어댑터 클래스와 결합하지 않습니다. 덕분에 기존 클라이언트 코드를 손상하지 않고 새로운 유형의 어댑터들을 프로그램에 도입할 수 있습니다. 이것은 서비스 클래스의 인터페이스가 변경되거나 교체될 때 유용할 수 있습니다: 클라이언트 코드를 변경하지 않은 채 새 어댑터 클래스를 생성할 수 있으니까요.
클래스 어댑터
이 구현은 상속을 사용하며, 어댑터는 동시에 두 객체의 인터페이스를 상속합니다. 이 방식은 C++ 와 같이 다중 상속을 지원하는 프로그래밍 언어에서만 구현할 수 있습니다.
-
클래스 어댑터는 객체를 래핑할 필요가 없습니다. 그 이유는 클라이언트와 서비스 양쪽에서 행동들을 상속받기 때문입니다. 위의 어댑테이션(적용)은 오버라이딩된 메서드 내에서 발생합니다. 위 어댑터는 기존 클라이언트 클래스 대신 사용할 수 있습니다.
의사코드
이 어댑터 패턴은 서로 맞지 않는 정사각형 못과 둥근 구멍이라는 고전적인 예시를 기초로 합니다.
어댑터는 정사각형 지름의 절반(즉, 사각형 못을 수용할 수 있는 가장 작은 원의 반지름)을 반지름으로 가진 둥근 못인 척 합니다.
적용
어댑터 클래스는 기존 클래스를 사용하고 싶지만 그 인터페이스가 나머지 코드와 호환되지 않을 때 사용하세요.
어댑터 패턴은 당신의 코드와 레거시 클래스, 타사 클래스 또는 특이한 인터페이스가 있는 다른 클래스 간의 변환기 역할을 하는 중간 레이어 클래스를 만들 수 있도록 합니다.
이 패턴은 부모 클래스에 추가할 수 없는 어떤 공통 기능들이 없는 여러 기존 자식 클래스들을 재사용하려는 경우에 사용하세요.
각 자식 클래스를 확장한 후 누락된 기능들을 새 자식 클래스들에 넣을 수 있습니다. 하지만 해당 코드를 모든 새 클래스들에 복제해야 하며, 그건 정말 나쁜 냄새가 나는 코드일 것입니다.
이보다 훨씬 더 깔끔한 해결책은 누락된 기능을 어댑터 클래스에 넣는 것입니다. 그 후 어댑터 내부에 누락된 기능이 있는 객체들을 래핑하면 필요한 기능들을 동적으로 얻을 것입니다. 이 해결책이 작동하려면 대상 클래스들에는 반드시 공통 인터페이스가 있어야 하며 어댑터의 필드는 해당 인터페이스를 따라야 합니다. 위 접근 방식은 데코레이터 패턴과 매우 유사합니다.
구현방법
-
호환되지 않는 인터페이스가 있는 클래스가 최소 두 개 이상 있는지 확인하세요:
- 당신이 변경할 수 없는 유용한 서비스 클래스가 있습니다. (종종 타사 코드, 레거시 코드 또는 기존 의존성이 많은 코드).
- 위 서비스 클래스를 사용하여 이득을 얻을 수 있는 하나 또는 여러 개의 클라이언트 클래스들이 있습니다.
-
클라이언트 인터페이스를 선언하고 클라이언트들이 서비스와 통신하는 방법을 기술하세요.
-
어댑터 클래스를 생성한 후 클라이언트 인터페이스를 따르게 하세요. 일단은 모든 메서드들을 비워 두세요.
-
서비스 객체에 참조를 저장하기 위하여 어댑터 클래스에 필드를 추가하세요. 일반적으로 사용되는 방법은 생성자를 통해 이 필드를 초기화하는 것이지만, 때때로 어댑터의 메서드들을 호출할 때는 이 필드를 어댑터에 전달하는 것이 더 편리하기도 합니다.
-
클라이언트 인터페이스의 모든 메서드를 어댑터 클래스에서 하나씩 구현하세요. 어댑터는 인터페이스 또는 데이터 형식 변환만 처리해야 하며, 실제 작업의 대부분을 서비스 객체에 위임해야 합니다.
-
클라이언트들은 클라이언트 인터페이스를 통해 어댑터를 사용해야 합니다. 이렇게 하면 클라이언트 코드에 영향을 주지 않고 어댑터들을 변경하거나 확장할 수 있습니다.
장단점
- 단일 책임 원칙. 프로그램의 기본 비즈니스 로직에서 인터페이스 또는 데이터 변환 코드를 분리할 수 있습니다.
- 개방/폐쇄 원칙. 클라이언트 코드가 클라이언트 인터페이스를 통해 어댑터와 작동하는 한, 기존의 클라이언트 코드를 손상시키지 않고 새로운 유형의 어댑터들을 프로그램에 도입할 수 있습니다.
- 다수의 새로운 인터페이스와 클래스들을 도입해야 하므로 코드의 전반적인 복잡성이 증가합니다. 때로는 코드의 나머지 부분과 작동하도록 서비스 클래스를 변경하는 것이 더 간단합니다.
다른 패턴과의 관계
-
브리지는 일반적으로 사전에 설계되며, 앱의 다양한 부분을 독립적으로 개발할 수 있도록 합니다. 반면에 어댑터는 일반적으로 기존 앱과 사용되어 원래 호환되지 않던 일부 클래스들이 서로 잘 작동하도록 합니다.
-
어댑터는 기존 객체의 인터페이스를 변경하는 반면 데코레이터는 객체를 해당 객체의 인터페이스를 변경하지 않고 향상합니다. 또한 데코레이터는 어댑터를 사용할 때는 불가능한 재귀적 합성을 지원합니다.
-
어댑터는 다른 인터페이스를, 프록시는 같은 인터페이스를, 데코레이터는 향상된 인터페이스를 래핑된 객체에 제공합니다.
-
퍼사드는 기존 객체들을 위한 새 인터페이스를 정의하는 반면 어댑터는 기존의 인터페이스를 사용할 수 있게 만들려고 노력합니다. 또 어댑터는 일반적으로 하나의 객체만 래핑하는 반면 퍼사드는 많은 객체의 하위시스템과 함께 작동합니다.
-
브리지, 상태, 전략 패턴은 매우 유사한 구조로 되어 있으며, 어댑터 패턴도 이들과 어느 정도 유사한 구조로 되어 있습니다. 위 모든 패턴은 다른 객체에 작업을 위임하는 합성을 기반으로 합니다. 하지만 이 패턴들은 모두 다른 문제들을 해결합니다. 패턴은 특정 방식으로 코드의 구조를 짜는 레시피에 불과하지 않습니다. 왜냐하면 패턴은 해결하는 문제를 다른 개발자들에게 전달할 수도 있기 때문입니다.