디자인패턴 - 어댑터 패턴

디자인패턴 - 어댑터 패턴

개요

  • 어댑터 패턴에 대해서 알아본다.
  • Coursera 의 디자인패턴 강의 를 기반으로 작성했다.
  • Rectangle vs LegacyRectangle, Duck vs Turkey 같은 터무니 없는 예제가 아닌 조금 더 실질적인 예제를 사용해본다.
  • JDK 에 사용된 어댑터 패턴을 살펴본다.


어댑터 패턴

어댑터 패턴은 이름대로 어댑터처럼 사용되는 패턴이다. 220V 를 사용하는 한국에서 쓰던 기기들을, 어댑터를 사용하면 110V 를 쓰는곳에 가서도 그대로 쓸 수 있다. 이처럼, 호환성이 없는 인터페이스 때문에 함께 동작할 수 없는 클래스들이 함께 작동하도록 해주는 패턴이 어댑터 패턴이라고 할 수 있겠다. 이를 위해 어댑터 역할을 하는 클래스를 새로 만들어야 한다.

기존에 있는 시스템에 새로운 써드파티 라이브러리가 추가된다던지, 레거시 인터페이스를 새로운 인터페이스로 교체하는 경우에 코드의 재사용성을 높일 수 있는 방법이 어댑터 패턴을 사용하는 것이다.

구조를 보면 아래와 같다.

Adapter Pattern
[그림1] Coursera Design Pattern 강의 중

Client
써드파티 라이브러리나 외부시스템을 사용하려는 쪽이다.

Adaptee
써드파티 라이브러리나 외부시스템을 의미한다.

Target Interface
Adapter 가 구현(implements) 하는 인터페이스이다. 클라이언트는 Target Interface 를 통해 Adaptee 인 써드파티 라이브러리를 사용하게 된다.

Adapter
Client 와 Adaptee 중간에서 호환성이 없는 둘을 연결시켜주는 역할을 담당한다. Target Interface 를 구현하며, 클라이언트는 Target Interface 를 통해 어댑터에 요청을 보낸다. 어댑터는 클라이언트의 요청을 Adaptee 가 이해할 수 있는 방법으로 전달하고, 처리는 Adaptee 에서 이루어진다.


어댑터 패턴 호출 과정

Adapter Pattern
[그림2]Adapter Pattern Sequence Diagram

클라이언트에서는 Target Interface 를 호출하는 것 처럼 보인다. 하지만 클라이언트의 요청을 전달받은 (Target Interface 를 구현한) Adapter 는 자신이 감싸고 있는 Adaptee 에게 실질적인 처리를 위임한다. Adapter 가 Adaptee 를 감싸고 있는 것 때문에 Wrapper 패턴이라고도 불린다.


어댑터 패턴 사용예제

Coursera 의 디자인 패턴 강의 중 어댑터 패턴에 나오는 예제를 조금 수정해서 구현해 보려한다. 이해를 돕기위해 어댑터 패턴의 설명에 불필요하다고 생각하는 메소드와 코드는 제거했다.

위에서 Client, Target Interface, Adapter, Adaptee 가 나오는 다이어그램을 그대로 구현한 코드다. UML 을 그려보면 아래와 같다.

Coursera Design Pattern 강의 중
[그림3] Coursera Design Pattern 강의 중

헷갈리지 않도록 위 그림과 코드에 사용되는 클래스들을 간단히 설명하면,

그림1 그림3 코드
Client WebClient AdapterDemo
Target Interface WebRequester WebRequester
Adapter WebAdapter WebAdapter
Adaptee WebService FancyRequester

시나리오는 다음과 같다.

기존에는 WebClient 에서는 요청에 대한 처리로 doWork() 메소드를 호출하는데, 이 처리는 WebRequester 인터페이스를 구현한 OldWebRequester 에게 위임하도록 되어있다. 이때, WebRequester 인터페이스를 구현한 OldWebRequester 의 requestHandler() 를 호출한다.

하지만 이 OldWebRequester 를 써드파티 라이브러리인 FancyRequester 로 변경해야하는 상황이 생겼다고 가정하자. 이때 어댑터 패턴을 적용하여 기존의 코드와 써드파티 라이브러리 어느쪽도 수정하지 않고 FancyRequester 를 적용할 수 있다.

코드를 보자.

WebRequester «Interface»
public interface WebRequester {
    void requestHandler();
}

Target Interface 이다. 구현체를 가지지 않고 requestHandler() 메소드에 대한 정의만 되어있다.


WebClient
public class WebClient {
    private WebRequester webRequester;

    public WebClient(WebRequester webRequester) {
        this.webRequester = webRequester;
    }

    public void doWork() {
        webRequester.requestHandler();
    }
}

doWork() 는 WebRequester 인터페이스를 구현한 클래스의 requestHandler() 메소드를 호출하여 동작한다.


OldWebRequester
public class OldWebRequester implements WebRequester {
    @Override
    public void requestHandler() {
        System.out.println("OldWebRequester is working");
    }
}

시나리오상에서, 기존에 사용하고 있던 WebRequester 의 구현클래스로써, WebClient 에서 doWork() 를 호출하면 내부에서 호출되던 녀석이다. 설명의 편의를 위해 작성했을 뿐, 이 예제의 Client 에서는 사용되지 않는다.


FancyRequester
public class FancyRequester {
    public void fancyRequestHandler() {
        System.out.println("Yay! fancyRequestHandler is called!");
    }
}

사용할 써드파티 라이브러리인 FancyRequester 이다. Adaptee 가 되겠다.


WebAdapter
public class WebAdapter implements WebRequester {
    private FancyRequester fancyRequester;

    public WebAdapter(FancyRequester fancyRequester) {
        this.fancyRequester = fancyRequester;
    }

    @Override
    public void requestHandler() {
        fancyRequester.fancyRequestHandler();
    }
}

어댑터를 위와같이 작성해준다.

WebAdapter 는 Target Interface 인 WebRequester 인터페이스를 구현하고, 인스턴스 생성시 FancyRequester 클래스를 주입한다. (FancyRequester 는 보통 또다른 어떤 인터페이스를 구현한 클래스이겠지만, 굳이 또 만들면 이해하기 복잡해지니까 그냥 구현 클래스만 언급했다)

그리고 Target Interface 인 WebRequester 인터페이스의 requestHandler() 를 구현하는데, 이때 주입시킨 FancyRequester 의 fancyRequestHandler() 메소드를 호출하도록 만든다.

이렇게 하면 WebAdapterWebRequester 인터페이스를 구현했으므로, WebRequester 인터페이스의 구현체를 받아 동작하던 WebClientWebAdapter 를 넘겨줄 수 있고, 기존에 WebClient 에서 requesterHandler() 메소드를 호출하던 코드는 그대로 두면서도, WebAdapter 의 requestHandler() 를 통해 써드파티 라이브러리인 FancyRequester 를 사용할 수 있게 된다.


실행
public class AdapterDemo {
    public static void main(String[] args) {
        WebAdapter adapter = new WebAdapter(new FancyRequester());
        WebClient client = new WebClient(adapter);
        client.doWork();
    }
}

위 예제 코드로 실행해보면 FancyRequester 가 동작하는 것을 확인할 수 있다.


WHY

애초에 두개의 인터페이스가 달라서 호환이 안된다면, 하나를 바꿔서 되게 하던지, 아니면 둘다 바꾸면 되지 않나?

예제에서는 이해를 돕기위해 FancyRequester 를 인터페이스를 만들지도 않았고 클래스 내부도 훤히 들여다 볼 수 있다. 하지만, FancyRequester 가 오픈소스가 아니라 미리 컴파일된 클래스 바이너리 파일만을 제공받은 써드파티 라이브러리라면 직접적인 접근이 불가능 할 수 있다. 직접적으로 접근할 수 있는 경우라 하더라도 Adaptee 쪽에서 우리가 변경한 코드로 인해 라이브러리나 벤더쪽 시스템 전체가 깨질 수도 있다.

그러면 우리쪽 인터페이스를 수정하면 되지 않나?

가능할 수 있다. 하지만 바꾸려는 우리쪽 인터페이스를 우리 시스템의 다른 어딘가에서 사용하고 있다면? 그 부분도 수정해줘야 한다. 우리쪽 인터페이스를 수정하고, 이에 영향을 받는 부분들을 수정하다가 예기치 못한 오류가 발생할 가능성이 매우 크다.


JDK 에 사용된 어댑터 패턴

어댑터 패턴이 적용된 가장 대표적인 예가 자바의 InputStreamReader 이다. 콘솔에서 입력을 받을 때 아래와 같이 사용하는 것을 본 적이 있을 것이다. (알고리즘 문제풀때 자바로 하면 Scanner 클래스를 사용하지 않고 이렇게 많이 했던 것 같다)

BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

BufferedReader 클래스를 까서 위 구문이 실행될때 사용되는 생성자를 보면 아래와 같이 Reader 타입을 받는다.

public BufferedReader(Reader in) {
    this(in, defaultCharBufferSize);
}

하지만 System.in 은 InputStream 타입을 반환한다.

public final static InputStream in = null;

자바의 InputStream 은 바이트 스트림을 읽어들인다. 하지만, BufferedReader 는 캐릭터인풋 스트림을 읽어들인다. 둘은 호환되지 않는다. 하지만, 이 둘을 연결시켜 주는 어댑터가 InputStreamReader 클래스이다. UML 로 보면 아래와 같은 구조다.

Adapter Pattern used in JDK
Adapter Pattern used in JDK

BufferedReader 클래스는 Reader 클래스를 상속받는다. (Reader 클래스는 Readable 인터페이스를 구현한 추상클래스이다), InputStreamReader 클래스도 Reader 클래스를 상속받는다. 둘 다 Reader 클래스의 서브클래스 이므로 Reader 타입으로 레퍼런스 할 수 있다.

그리고 InputStreamReader 클래스는 InputStream 타입을 받을 수 있는 생성자를 가지고 있으므로, System.in 을 InputStreamReader 인스턴스 생성시 넘겨주는 방식이다.

InputStreamReader 클래스를 Adapter, System.in 을 Adaptee, Reader 를 Target Interface 라고 할 수 있겠다.


어댑터 패턴 정리

  • Adaptee 를 감싸고, Target Interface 만을 클라이언트에게 드러낸다.
  • Target Interface 를 구현하여 클라이언트가 예상하는 인터페이스가 되도록 Adaptee 의 인터페이스를 간접적으로 변경한다.
  • Adaptee 가 기대하는 방식으로 클라이언트의 요청을 간접적으로 변경한다.
  • 호환되지 않는 우리의 인터페이스와 Adaptee 를 함께 사용할 수 있다.


결론

사용해야하는 인터페이스가 현재의 시스템과 호환되지 않는다고 해서 현재의 시스템을 변경을 해야하는 것은 아니다.


참고한 자료

Comments

Yaboong's Picture

Yaboong

오스카 쉰들러는 흔해빠진 기회주의자요 부패한 사업가였다. 그러나 거대한 악이 세상을 점령하는 것처럼 보일 때 그 악에 대항해서 사람의 생명을 구한 것은 귀족도 지식인도 종교인도 아닌 부패한 기회주의자 오스카 쉰들러였다.

Seoul, South Korea https://github.com/yaboong