본문 바로가기

OOP, FP/디자인패턴

헤드퍼스트 디자인 패턴: 7. 어댑터 패턴


지난 포스팅에선 메소드 호출을 캡슐화하는 커맨드 패턴에 대해 알아보았다.


이번엔 호환되지 않는 인터페이스를 변환하여 클라이언트에게 제공하는 어댑터 패턴에 대해 복습한다.


어댑터 패턴

한 클래스의 인터페이스를 클라이언트에서 사용하고자 하는 다른 인터페이스로 변환한다.

이를 이용하여 인터페이스 호환성 문제 때문에 같이 쓸 수 없는 클래스들을 연결해서 쓸 수 있다.


아이폰과 안드로이드 폰의 충전기는 규격이 달라 서로 호환되지 않는다.

안드로이드 폰 충전기에 변환젠더를 연결하면 아이폰을 충전할 수 있게 된다.


여기서 변환젠더와 같이 호환성 문제 때문에 사용할 수 없는 객체를

중간에서 변환하여 사용가능하게 만들어주는 녀석이 어댑터라고 생각하면 될 것 같다.



(1) 타겟: 클라이언트에게 제공하는 인터페이스 

(2) 어댑터: 타겟 인터페이스를 구현하며 어댑티를 구성요소로 사용, 타겟 인터페이스의 요청을 어댑티에게 위임한다.

(3) 어댑티: 요청을 위임받아 실제로 호출되는 기능을 갖고 있는 인터페이스


어댑티를 새로 바뀐 인터페이스로 감쌀 때는 두번째 원칙 객체 구성(Composition)을 사용해서 감싼다.

이렇게 상속이 아닌 구성을 이용하면 어댑티의 어떤 서브클래스에 대해서도 어댑터를 쓸 수 있는 장점이 생긴다.


그리고 세번째 원칙을 적용하여 클라이언트를 특정 구현이 아닌 타겟 인터페이스에 연결시킨다.

이렇게 구현이 아닌 인터페이스를 사용하면 각각 서로 다른 백엔드 클래스들로 변환시키는 여러 어댑터를 사용할 수도 있다.

타겟 인터페이스만 제대로 지킨다면 나중에 다른 구현을 추가하기가 쉬운 것과 같이 유연셩이 높아진다.


어댑터 패턴을 사용하면 기존의 인터페이스와 새로운 인터페이스의 코드를 전혀 수정하지 않고도 호환성 문제를 해결할 수 있게 된다.




테란과 저그의 유닛이 있다.

저그를 플레이하는 클라이언트가 퀸을 이용해서 커멘트 센터를 감염시킨 상황이 생겼다고 가정하자.

감염된 커멘드센터는 100미네랄과 50의 가스를 지불하고 인페스티드 테란이라는 유닛을 만들 수 있다.

인페스티드 테란은 자폭 무기를 사용해서 상대유닛에게 500의 데미지를 준다.


여기선 인페스티드 테란은 원래 테란의 유닛이라고 생각하자. 

클라이언트는 저그유닛의 zergAttack()만 사용할 수 있다.

그런데 인페스티드 테란은 weaponAttack()을 호출해서 공격 명령을 내려야 한다.

결국 클라이언트는 저그유닛인척 하는 테란유닛에게 zergAttack() 메소드를 호출하라고 명령을 내려야하는데

메소드가 존재하지 않으므로 오류가 날 것이다.


그럼 인페스티드 테란을 수정해서 zergAttack()이라는 메소드를 또 만들어줘야 할까?

그렇게 되면 테란 유닛 중에 감염될 수 있는 유닛이 여러 가지 존재한다거나 감염될 수 있는 유닛을 추가하는 경우에 일일이 수정해야 한다.


OCP에서 코드는 변화에 닫혀있어야 한다고 했다.

어댑터 패턴을 사용하면 테란유닛의 코드를 수정하지 않고도 weaponAttack()을 호출할 수 있다.




ZergUnit이 클라이언트가 사용하는 유닛으로 타겟 인터페이스,

TerranUnit이 요청을 위임받을 어댑티,

TerranUnitAdepter가 변환 작업을 수행할 어댑터이다.


어댑터에게 InfestedTerran 유닛을 넘겨줘서 감싼다. 그러면 마치 저그의 유닛처럼 다룰 수 있게된다.

클라이언트는 어댑터의 zergAttack()메소드만 호출하면 된다.


의문점

(1) 대형 타겟 인터페이스를 구현해야 하는 경우

- 어댑터를 구현하는 일은 타겟 인터페이스로 지원해야 하는 인터페이스의 크기에 비례해서 복잡해지게 된다.

  하지만 양쪽의 코드를 수정하지 않고 호환성을 제공하는 다른 대안이 마땅치 않다.

  모든 코드 변경 사항을 캡슐화시킨 클래스 한 개만 제공하는 방법이 깔끔하다.


(2) 어댑터는 하나의 클래스만 감싸야 하는가?

- 보통 어댑터 패턴은 한 인터페이스를 다른 인터페이스로 변환하기 위해 사용한다.

  하지만 한 어댑터에서 타겟 인터페이스를 구현하기 위해 두 개 이상의 어댑티를 감싸야할 수도 있다.


(3) 호환되는 부분(신형)과 호환되지 않는 부분(구형)을 섞어서 사용한 시스템이 있으면 어떻게 해야하나?

- 이런 상황에서는 두 인터페이스(호환되는 부분, 호환되지 않는 부분)를 모두 지원하는 어댑터(Two Way Adapter)를 만들 수도 있다.

  필요한 인터페이스를 둘 다 구현해서 어댑터가 기존 인터페이스와 새로운 인터페이스 역할을 모두 맡을 수 있게

  다중 어댑터로 구현할 수 있다.



클래스 어댑터

이제까지는 어댑터가 타겟인터페이스를 구현하고 어댑티를 구성요소로 감싸는 객체 어댑터였다.

이와 반대로 클래스 어댑터는 타겟 인터페이스와 어댑티를 모두 상속한다.

어댑티를 적응시키는 데 있어서 구성을 사용하지 않고 다중 상속을 이용해 어댑티와 타겟 인터페이스 모두의 서브클래스를 만든다.





앞에서는 편의상 타겟 인터페이스와 어댑티를 인터페이스로 설계했지만

실제 상황에서 Unit을 인터페이스로 만들지는 않을 것이다.

타겟인터페이스와 어댑티가 모두 추상 클래스가 될 것이며,

이런 상황에서 클래스 어댑터를 사용하려면 다중 상속이 필요하다.



객체 어댑터와 클래스 어댑터

(1) 객체 어댑터는 구성을 사용해서 어댑티 클래스 뿐 아니라 그 서브 클래스에 대해서도 어댑터 역할이 가능하다.

- 대신 어댑티 전체를 다시 구현해야 한다.


(2) 클래스 어댑터는 상속을 사용해서 특정 어댑터 클래스에만 사용이 가능하다.

- 대신 어댑티 전체를 다시 구현하지 않아도 된다.

- 어댑티의 서브클래스이기 때문에 어댑티의 행동을 오버라이드 할 수 있다.


(3) 클래스 어댑터는 상속을 이용하여 중복되는 코드를 줄일 수 있지만 객체 어댑터에 비해 유연성이 낮다.


(4) 객체 어댑터는 상속보다는 구성을 이용하라는 원칙을 지켰기 때문에 클래스 어댑터에 비해 유연성이 높다.

- 어댑티한테 필요한 일을 시키기 위한 코드만을 만들면 되기 때문에 딱히 많은 코드가 필요하지 않다.


(5) 객체 어댑터는 어댑터 코드에 어떤 행동을 추가하면 그 어댑터 코드는

    어댑티 클래스 뿐 아니라 모든 서브클래스에 대해서도 적용된다.



데코레이터 패턴과 어댑터 패턴

(1) 데코레이터가 적용된다는 것은 새로운 책임 또는 행동이 추가된다는 것을 의마한다.


(2) 어댑터 패턴을 이용하면 변화를 클라이언트로 부터 분리할 수 있다.


(3) 데코레이터 패턴이 복잡하게 구성된 경우 메소드 호출이 얼마나 많은 데코레이터를 거쳐갔는지,

    얼마나 겹겹이 싸여있는지, 요청을 처리한 것에 대해 어떠한 연락을 받게 될지와 같은 추적이 어렵다.


* 겉으로 보기엔 객체를 감싸서 다른 행동을 하거나 변환하기 때문에 비슷해보이지만,

  각각의 패턴의 목적, 용도에 확연한 차이가 있다.




호환되지 않는 인터페이스를 변환하여 클라이언트에게 제공하는 어댑터 패턴에 대한 복습을 마치겠다.


1. 스트래티지 패턴

2. 옵저버 패턴

3. 데코레이터 패턴

4-1. 팩토리 메서드 패턴

4-2. 추상 팩토리 패턴

5. 싱글턴 패턴

6. 커맨드 패턴

7. 어댑터 패턴