38-2.어댑터

38-2-가.어댑터

어댑터(Adapter)란 이미 만들어진 컴포넌트의 구현은 그대로 활용하고 인터페이스만 조금 변경하여 컴포넌트를 일부 변형시키는 것이다. 어댑터는 컴포넌트를 조금씩 변형함으로써 활용도를 높인다. 새로 만들고자 하는 부품이 이미 만들어진 부품의 기능 중 일부만을 필요로 할 경우 처음부터 새로 만들 필요없이 기존 부품을 변형해서 사용하면 훨씬 더 빠르고 간편하다. 없는 기능을 만들 수는 없지만 있는 기능의 일부를 막아 버린다거나 고정하는 것은 가능하다.

용어가 굉장히 어려운 것 같지만 일상 생활에서도 어댑터의 예는 많이 찾아 볼 수 있다. 예를 들어 바닥에 엎드려 책을 읽고 있는데 갑자기 졸음이 마구 쏟아진다고 하자. 잠을 자기 위해서는 베게라는 것이 필요한데 장롱까지 갔다 오면 이 달콤한 잠이 달아나 버릴 것 같다. 이럴 때는 읽던 책을 베게삼아 잠을 청할 수도 있다. 책이라는 것은 지식이나 감동을 전달하는 미디어이지만 원래 목적은 잠시 접어 두고 큼직하고 넓은 표면을 활용하여 베게로 쓸 수도 있다.

책이 일시적으로 베게가 될 수 있는 이유는 두 사물이 어느 정도의 유사함이 있기 때문이다. 아무리 급하다고 해도 볼펜이나 식칼을 베게로 쓸 수는 없는 노릇이다. 핸드폰은 원래 전화를 걸고 받는 것이 목적인 기계이다. 그러나 아무도 전화를 걸어 주지 않는 캔디폰이라면 게임기로 활용할 수도 있다. 전화가 안 오고 걸 데도 없는 왕따라서 핸드폰의 본래 기능을 포기하고 부가 기능 중 하나인 게임기로만 사용한다면 이것도 어댑터의 한 예이다. 이 외에도 일상 생활에서 사물의 용도를 잠시 전용하는 경우는 많이 들 수 있다.

STL 컴포넌트의 어댑터도 유사한 방식이다. 이러쿵 저러쿵 동작하는 컴포넌트가 있는데 이러쿵 기능은 쓸 일이 전혀 없고 저러쿵 기능만 필요하다면 이 컴포넌트의 이러쿵 기능을 막아 버리고 용도를 바꿔 쓰는 것이 어댑터이다. 이 편이 저러쿵 기능만 가진 컴포넌트를 처음부터 다시 만드는 것보다 훨씬 더 경제적이고 신뢰할만하다. 이러쿵 저러쿵 컴포넌트는 두 기능 다 잘 동작하는 것으로 이미 증명되어 있기 때문이다.

비록 STL이 제공하는 컴포넌트의 수가 많고 일반화되어 있기는 하지만 그래도 특수한 프로그래밍 환경에 두로 사용되기에는 결코 충분하지 않다. 그렇다고 컴포넌트의 수를 무한정 늘리기만 할 수는 없으므로 기존 컴포넌트를 변형할 수 있는 어댑터라는 방법을 제공한다. 어댑터는 일반화된 컴포넌트의 용도를 더욱 확장하는 역할을 한다. 어댑터는 컴포넌트, 반복자, 함수 객체에 대해 적용되며 다음과 같이 분류할 수 있다.

컴포넌트 어댑터와 반복자 어댑터는 관련 장에서 논하기로 하고 여기서는 함수 객체에 대한 어댑터만 우선적으로 연구해 보자. 문법이 복잡해서 다소 어려우므로 어떤 식으로 동작하는지 소스를 잘 관찰해 봐야 하며 때로는 앞부분으로 돌아가 복습을 하고 와야 하는 경우도 있을 것이다. STL 문법 중 가장 어렵고 복잡하다.

함수 객체의 기능을 조금이라도 변경하려면 어댑터를 적용할 수 있도록 만들어야 하는데 이런 함수 객체를 어댑터블(Adaptable) 함수 객체라고 한다. 기능을 변경하는 어댑터는 대상 함수 객체가 취하는 인수의 타입은 무엇인지, 리턴 타입은 무엇인지 등 함수 객체에 대한 충분한 정보를 얻을 수 있어야 한다. 만드는 방법은 아주 쉬운데 functional 헤더 파일에 정의되어 있는 다음 두 템플릿 클래스 중 하나를 상속받으면 된다.

 

template<class Arg, class Result>

struct unary_function {

     typedef Arg argument_type;

     typedef Result result_type;

};

template<class Arg1, class Arg2, class Result>

struct binary_function {

     typedef Arg1 first_argument_type;

     typedef Arg2 second_argument_type;

     typedef Result result_type;

};

 

인수의 개수에 따라 단항 함수 객체는 unary_function을 상속받고 이항 함수 객체는 binary_function을 상속받는다. 이 클래스의 내용을 보면 멤버 변수나 멤버 함수는 전혀 없고 인수와 리턴값에 대한 타입 정의(typedef)만을 가진다. 클래스는 주로 멤버 변수나 멤버 함수들로 구성되지만 타입이나 상수, 내부 클래스, 가상 함수 테이블 등의 다른 여러 가지 것들도 같이 캡슐화된다는 것을 잊지 말자.

인수나 리턴 타입은 함수 객체와 관련이 있는 중요한 정보인데 이 정보들을 템플릿 인수로 전달받아 argument_type, result_type 등의 이름으로 획일화하여 타입 정의한다. 이항 함수 객체는 두 개의 인수를 가지므로 first, second 인수의 타입을 각각 따로 정의한다. 이 두 클래스로부터 상속받으면 타입들이 미리 약속된 이름으로 정의되므로 어댑터는 약속된 이름으로 해당 정보를 쉽게 얻을 수 있다.

어댑터들은 함수 객체의 기능을 변형하기 위해 이 정보들이 필요한데 함수 객체가 약속된 이름으로 직접 이 타입들을 정의해도 상관없다. 그러나 아무래도 직접 정의하는 것은 번거로우므로 상기 두 클래스로부터 상속을 받는 것이 편리하다. 어댑터를 적용할 필요가 없다면 굳이 이 타입들을 정의할 필요는 없다. functor 예제의 print 함수 객체는 단독으로 사용되므로 이 타입들을 정의하지 않았는데 어댑터로 사용하려면 다음과 같이 정의하는 것이 원칙이다.

 

#include <functional>

struct print : public unary_function<int,void> {

     void operator()(int a) const {

          printf("%d\n",a);

     }

};

 

unary_function으로부터 상속받되 인수는 int 타입이고 리턴 타입은 void임을 템플릿 인수로 지정했다. 이 상속에 의해 두 개의 타입이 약속된 이름으로 정의되며 print 함수 객체를 다음처럼 선언하는 것과 같다. argument_type은 int가 되고 result_type은 void가 된다.

 

struct print {

    typedef int argument_type;

    typedef void result_type;

     void operator()(int a) const {

          printf("%d\n",a);

     }

};

 

이렇게 타입을 직접 정의해도 마찬가지이지만 귀찮기도 하고 오타가 발생할 위험도 있으므로 STL은 타입 정의를 도와주는 unary_function, binary_function 기반 클래스를 제공하는 것이다. 어차피 이 클래스들은 크기가 0이므로 상속을 받는다 하여 용량상의 불이익이 발생하는 것도 아니다. 그래서 어댑터를 적용할 계획이든 아니든 함수 객체 클래스를 정의할 때는 일단 상속을 받는 것이 좋다. plus, greater 등의 미리 제공되는 함수 객체들은 모두 이 클래스들로부터 상속받으므로 어댑터를 항상 적용할 수 있다.