[디자인 패턴] 7.(1) 어댑터 패턴 (Adapter Pattern)
이 글은 헤드퍼스트 디자인패턴과 GoF 디자인패턴을 읽고 정리한 글입니다.
1. 어댑터 패턴(Adapter Pattern)
특정 클래스 인터페이스를 클라이언트에서 요구하는 다른 인터페이스로 변환합니다. 인터페이스가 호환되지 않아 같이 쓸 수 없었던 클래스를 사용할 수 있습니다.
다중상속을 이용한 클래스 어댑터 (자바에서는 다중상속의 미지원으로 사용 불가능)
구성을 이용한 오브젝트 어댑터
1.1. 구성 요소
Target
- Client 가 사용할 인터페이스입니다.
Client
- 클라이언트로 Target 인터페이스를 따르는 객체를 사용합니다.
Adaptee
- 적응(adapting)이 필요한 인터페이스입니다.
Adaptor
- Adaptee 를 Target 인터페이스로 적응시킨 객체입니다.
- Target 인터페이스를 따르기에 클라이언트가 사용할 수 있습니다.
1.2. 적용 방법
- 수정이 어려우며 호환되지 않는 인터페이스를 가진 클래스를 찾습니다. 이는 Adaptee 클래스가 됩니다.
- Adaptor 클래스를 생성합니다. 호환되었으면 하는 인터페이스를 Target 이라 할 때, Adaptor가 Target을 구현하도록 합니다.
- Adaptor에 Adaptee의 참조를 갖도록 하며, 생성자나 세터를 통해 사용할 Adaptee를 전달받을 수 있도록 합니다.
- Client 에서 사용하는 Adaptor의 모든 메서드를 구현합니다. 실제 처리는 Adaptee 를 통해 이루어지며 Adaptor 는 인터페이스 타입에 맞춰서 데이터 형식 반환만 처리하도록 합니다.
- Client 가 Target 인터페이스를 통해 Adaptor 를 사용하도록 합니다.
1.3. 적용 시기
- 기존 클래스를 사용하고 싶으나 필요한 인터페이스 타입과 호환되지 않을 때 어댑터 패턴을 사용할 수 있습니다.
1.4. 정리
어댑터 패턴의 원리는 위임입니다. Adaptor가 Adaptee를 가지며 Client 요청에 대하여 처리할 때 Adaptor 는 Adaptee를 통해 요청을 처리합니다. Adaptor 클래스는 Target 인터페이스 타입을 준수하기 위한 하나의 래퍼(Wrapper)로써의 역할을 합니다.
어댑터 패턴은 구조에 따라 클래스 어댑터와 오브젝트 어댑터로 구분됩니다. 클래스 어댑터는 Target과 Adaptee를 모두 상속하며, 오브젝트 어댑터는 Target 를 상속하며 Adaptee를 갖습니다. 이 두 방식은 각 특성에 따라 장단점이 있습니다.
클래스 어댑터는 어댑터 자제가 Adaptee 이므로 Adaptee에 대한 참조를 갖지 않아도 됩니다. 하지만 Adaptee타입이 아닌 Adaptee의 subclass 타입을 갖도록 하기 위해서는 해당 subclass 타입으로 상속을 받아 구현하여야만 합니다.
오브젝트 어댑터는 하나 이상의 Adaptee를 갖도록 구현할 수 있습니다. 그러나 직접 Adaptee 의 구현을 확장 하기엔 어려움이 있습니다. 확장된 Adaptee를 구현 후 이를 setter로 지정하는 방식으로 구현해야 합니다.
추가적으로, 어댑터를 구현 시 어느 수준의 어댑팅까지 지원하여야 할지, 플러그인이나 양방향 어댑터로 지원을 할지 등등에 대하여 더 생각할 필요가 있습니다. 이는 아래의 구현 및 이슈에 대하여 다루는 곳에서 더 자세히 정리 해보겠습니다.
2. 어댑터 패턴 구현 시 이슈 및 대응 방법
2.1. Pluggable adapters - 플러거블 어댑터
pluggable adapter를 구현하는 방법에 대하여 정리하기 전 pluggable 어댑터와 일반적인 어댑터가 어떠한 차이가 있는지 알 필요가 있었습니다. 책에서는 class with built-in interface adaptation로 라는 문장으로 설명하고 있었는데요, 저는 이 표현이 이해가 되지 않아 구글링한 결과를 정리하였습니다.
bulit-in interface는 어댑터가 상속할 Target API의 인터페이스로, 일반 어댑터와 다른 점으로 미래에 추가 및 변경될 수 있는 Adaptee를 고려해 최소한의 기능만을 제공하는 인터페이스를 의미합니다.
즉 pluggable adapter는 변경될 수 있는 Adaptee를 고려하여 설계된 Target API 인터페이스를 상속하는 Adapter입니다.
명확하게 정의하지 어렵지만, 플러거블 어댑터는 Target 인터페이스를 구현하며 최소한의 기능만 제공하여 재사용성을 높은 어댑터입니다.
플러거블 어댑터를 만들기 위해서는 세가지 방법이 있습니다. 이 세가지 방법은 모두 narrow 인터페이스를 찾는것으로 시작됩니다. narrow 인터페이스는 어댑터 기능을 위한 가장 작은 부분집합입니다. 플러거블 어댑터를 구현하는 세가지 방법은 narrow 인터페이스를 다르게 사용합니다.
(a) Using abstract operation
narrow 인터페이스를 Target 클래스에 추상 메서드로 등록합니다. Adapter 클래스는 Target 클래스를 상속할 때 이 추상 메서드만 구현합니다.
(b) Using delegate objects
delegate 방식도 abstraction 방식과 동일하게 Target 이 narrow 인터페이스를 추상 메서드로 갖습니다. 차이점은 Client가 Target 구현 클래스를 setter를 통해 수정할 수 있다는 것입니다. 이는 전략패턴과 유사한데요, Client가 Target 인터페이스의 참조를 갖도록 합니다. Client의 SetDelegate 메서드를 통해 사용할 어댑터를 동적으로 수정할 수 있으므로 다르게 동작하는 여러 어댑터를 사용할 수 있다는 장점이 있습니다.
(c) Parameterized adapters
책의 예시는 SmallTalk 언어에서 block을 사용하여 파라미터화된 어댑터 구현을 보여주고 있습니다. block은 node의 getSubdirectories, createGraphicNode 메서드를 호출하는 것입니다. 솔직히 이 방법이 위의 delegation과 어떠한 차이가 있는지는 잘 모르겠습니다. 위임을 통한 구현도 Target 인터페이스를 따르는 어댑터에 대하여 Target API 에 맞게 메서드를 호출하도록 하면 어떤점이 다른가 싶습니다. 그러나 이 방법은 subclassing의 대안으로 소개됩니다. 자바에서는 어떻게 이 방법을 사용할 수 있을지 고민해보았습니다.
자바에서는 람다, 함수형 인터페이스를 사용하는 것이 방법이 될것 같습니다. Client는 narrow 인터페이스의 메서드 구현을 람다로 등록할 수 있도록 합니다. 하지만 이렇게 구현한다면 어댑터를 만들 필요가 있을까요? Client람다에 Adaptee를 이용한 모든 구현 내용을 전달한다면 어댑터의 존재 이유가 없을것 같습니다.
파라미터화된 어댑터에 대한 내용을 추가적으로 학습 후 정리하도록 하겠습니다.