Adapter 패턴은 이미 구현 되어 있는 객체를 이용하여 다른 기능을 구현하는 패턴이다.
코드는 가능한 한 적은 것이 좋다. 이미 구현되어 있는 코드가 있다면 최대한 그 코드를 이용하는 것이 좋다. 중복은 코드를 만들어 내고 관리에 필요한 비용을 만들어 낸다. 그렇다고 해서 아무렇게나 기존의 코드를 이용하는 것은 문제가 있다.
객체지향에서 대표적으로 기존에 구현된 코드를 이용하는 방식에는 상속이 있다. 하지만 조금만 공부해 보면 상속을 통해서 상위 클래스의 기능을 물려 받은 후 하위 클래스로 다른 기능을 구현하는 것은 객체지향에서 금기시 되고 있다. 이는 다형성을 위배하고, 불필요한 인터페이스를 전파하며, 클래스 계층관계를 이해하는데 혼돈을 준다.
말로만 해서는 잘 이해가 안될 수도 있으니 하지 말라는 것을 한번 해보도록 하겠다. Stack을 구현하는 과정을 예로 들어 보겠다. Stack은 데이터를 push()와 pop()으로 넣었다 뺐다 하는 자료 구조이다. 익히 알다시피 Stack 뿐만 아니라 다른 대부분의 컬렉션들도 비슷한 기능은 이미 지원하고 있다. 그렇다면 다른 컬렉션 중 하나를 상속 받고, Stack이 지닌 고유한 특성, 즉 push()와 pop() 기능을 구현해 주면 될 것이다.
그러면 간단히 Vector를 상속 받아서 Stack을 구현해 보자.
상속을 통해 구현한 Stack(안티 패턴)
class Stack<T> extends Vector<T>{
public void push(T t){ add(t); }
public T pop(){
if(size() == 0) return null;
T t = get(size() - 1);
remove(size() - 1);
return t;
}
}
그리고 테스트 코드를 작성해서 실행을 시켜보자.
테스트 코드
public static void main(String[] args) {
Stack<Integer> stack = new Stack<Integer>();
stack.push(100);
stack.push(200);
stack.push(300);
System.out.println(stack.pop());
System.out.println(stack.pop());
System.out.println(stack.pop());
}실행 결과
300
200
100
원하는 대로 매우 잘 동작하는 것을 알 수 있다. 하지만 여기에 아무 문제도 없을까?
이제 테스트 코드에서 선언한 Stack 인스턴스에 인터페이스를 살펴 보자. 이클립스라면 stack 옆에 "."을 찍으면 이와 같은 화면이 뜰 것이다.
위의 그림에 보면 우리가 직접 선언한 push()와 pop() 조차 보이지 않을 만큼 많은 API가 선언되어 있음을 알 수 있다. 이것들은 대체 어디서 온 것일까? 그렇다. 우리는 Vector를 상속 받았다. 이 메소드들은 모두 Vector에서 온 것들이다. 우리는 Vector 구체 클래스를 상속 받았기 때문에 그 API를 그대로 물려 받은 것이다.
이것은 우리의 의도와는 다른 결과를 만들어 낸다. 우리는 분명 이 Stack 클래스를 Stack으로 사용할 것을 기대하고 배포하였다. 하지만 이 코드를 받아 본 다른 개발자는 이 클래스가 가진 다른 API들을 보고 자신에게 필요한 용도로 활용할 수 있을 것이라 생각했다. 그래서 add() 함수를 통해서 Stack에 저장되는 순서와 다른 형태로 저장도 하고, remove() 함수를 통해서 역시 순서와 상관 없이 데이터를 제거하는 코드를 만들었다. 이렇게 만들어진 코드에 대해서 Vector를 상속 받아 Stack을 만든 사람은 책임을 피하기 어려울 것이다.
문제는 또 있다. Vector 클래스에 대한 설계가 변경되어 기존의 API들을 고쳐야 하는 상황이 온 것이다. Stack 클래스에서 새로 구현한 API들은 그대로 사용해도 문제가 없겠지만 Vector 클래스의 API를 사용해버린 경우 Vector 클래스의 수정에 영향을 받게 된 것이다. 단지 기존의 기능을 좀 이용하려 했을 뿐인데 상위 클래스의 변경에도 취약한 코드가 되어 버린 것이다.
자 이런 경우에 활용할 수 있는 것이 바로 Adapter 패턴이다. 일단 Adapter 패턴은 상속을 받아 구현하지 않는다. 따라서 상위 클래스의 변경 문제로부터 자유로울 수 있다. 대신에 Adapter 패턴에서는 기존의 클래스를 맴버 객체로 가지고 있다가 자신이 해야 할 일을 맴버 객체에게 시키는 방식으로 동작한다. 자기가 해야 할 일을 맴버 객체에게 시킴으로써 목적을 달성하는 것을 위임이라고 한다. 이를 통해 기존의 코드를 이용하면서도 상속 문제와 상위 클래스의 API 문제를 해결할 수 있다.
그러면 이제 Adapter 패턴으로 구현된 Stack 코드를 보자.
Adapter 패턴으로 구현한 Stack 클래스
class Stack<T>{
private Vector<T> vector = new Vector<T>();
public void push(T t){ vector.add(t); }
public T pop(){
if(vector.size() == 0) return null;
T t = vector.get(vector.size() - 1);
vector.remove(vector.size() - 1);
return t;
}
}
우선 우리가 이용하고자 하던 기존 코드, 즉 Vector는 내부 맴버 객체로 선언이 되었다.(더 좋은 코드라면 강한 의존성 문제를 해결하기 위해서 의존성 주입 형태를 사용해야 한다.) 그리고 마찬가지로 push()와 pop()을 구현하는데, 이 때 기존 코드에서는 상속을 받았기 때문에 직접 호출했었던 API들을 이제는 vector 맴버 객체의 API를 호출하고 있다.
이렇게 구현된 경우, Stack 클래스는 Vector가 가지고 있는 API들은 하나도 노출시키지 않고 Vector의 기능을 이용할 수 있다. 만약 Vector 클래스의 API가 변경된 경우라면 Stack 클래스를 사용하는 코드에는 수정이 가해질 필요가 없고 단지 Stack 클래스 내부 코드만 수정하면 된다.
'5.디자인패턴' 카테고리의 다른 글
Builder 패턴 (0) | 2016.09.10 |
---|---|
Holder 패턴 (0) | 2016.08.28 |
Decorator 패턴(synchronizedList의 구현 패턴) (0) | 2016.08.23 |
Pluggable Selector 패턴 (1) | 2016.08.21 |
State 패턴 (0) | 2016.08.15 |