Table of contents
지난 포스팅에선 메소드 호출을 캡슐화하는 커맨드 패턴에 대해 알아보았다. 이번엔 호환되지 않는 인터페이스를 변환하여 클라이언트에게 제공하는 어댑터 패턴에 대해 복습한다.
정의
한 클래스의 인터페이스를 클라이언트에서 사용하고자 하는 다른 인터페이스로 변환한다. 이를 이용하여 인터페이스 호환성 문제 때문에 같이 쓸 수 없는 클래스들을 연결해서 쓸 수 있다. 예를 들어 아이폰과 안드로이드 폰의 충전기는 규격이 달라 서로 호환되지 않는데, 안드로이드 폰 충전기에 변환젠더를 연결하면 아이폰을 충전할 수 있게 되는 것과 비슷하다. 변환젠더와 같이 호환성 문제 때문에 사용할 수 없는 객체를 중간에서 변환하여 사용가능하게 만들어주는 녀석이 어댑터라고 생각하면 된다.
다이어그램
Target은 클라이언트에게 제공하는 인터페이스이다. Adapter 클래스는 타겟 인터페이스를 구현하며 어댑티를 구성요소로 사용, 타겟 인터페이스의 요청을 어댑티에게 위임한다. 마지막으로 Adaptee는 요청을 위임받아 실제로 호출되는 기능을 갖고 있는 인터페이스이며 Adaptee를 새로 바뀐 인터페이스로 감쌀 때는 객체 구성(Composition) 을 이용하여 감싼다.
상속이 아닌 구성을 이용하면 Adaptee의 어떤 서브클래스에 대해서도 Adapter를 쓸 수 있는 장점이 생긴다. 그리고 세번째 원칙을 적용하여 클라이언트를 특정 구현이 아닌 타겟 인터페이스에 연결시킨다. 구현이 아닌 인터페이스를 사용하면 각각 서로 다른 백엔드 클래스들로 변환시키는 여러 어댑터를 사용할 수도 있다. Target 인터페이스만 제대로 지킨다면 나중에 다른 구현을 추가하기가 쉬워 유연성이 높아진다. 어댑터 패턴을 사용하면 기존의 인터페이스와 새로운 인터페이스의 코드를 전혀 수정하지 않고도 호환성 문제를 해결할 수 있게 된다.
예시
스타크래프트 테란과 저그의 유닛을 예로 들어보자. 저그를 플레이하는 클라이언트가 퀸을 이용해서 커멘트 센터를 감염시킨 상황을 가정한다. 감염된 커멘드센터는 100미네랄과 50의 가스를 지불하고 인페스티드 테란이라는 유닛을 만들 수 있다. 인페스티드 테란은 자폭 무기를 사용해서 상대유닛에게 500의 데미지를 준다. 인페스티드 테란은 원래 테란의 유닛이라고 생각해보자. 클라이언트는 저그유닛의 zergAttack()만 사용할 수 있다. 그러나 인페스티드 테란은 weaponAttack()을 호출해서 공격 명령을 내려야 한다. 결국 클라이언트는 저그유닛인척 하는 테란유닛에게 zergAttack() 메소드를 호출하라고 명령을 내려야하는데 메소드가 존재하지 않으므로 오류가 날 것이다.
그럼 인페스티드 테란을 수정해서 zergAttack()이라는 메소드를 또 만들어줘야 하는가? 그렇게 되면 테란 유닛 중에 감염될 수 있는 유닛이 여러 가지 존재한다거나 감염될 수 있는 유닛을 추가하는 경우에 일일이 수정해야 한다. OCP원칙을 생각해보면 코드는 변화에 닫혀있어야 한다. 어댑터 패턴을 사용하면 테란유닛의 코드를 수정하지 않고도 weaponAttack()을 호출할 수 있다.
ZergUnit이 클라이언트가 사용하는 유닛으로 타겟 인터페이스, TerranUnit이 요청을 위임받을 Adaptee, TerranUnitAdepter가 변환 작업을 수행할 Adapter이다. Adapter에게 InfestedTerran 유닛을 넘겨줘서 감싼다. 그러면 마치 저그의 유닛처럼 다룰 수 있게된다. 클라이언트는 Adaptee의 zergAttack()메소드만 호출하면 된다.
의문점
대형 타겟 인터페이스를 구현해야 하는 경우는 어떻게 해야하는가?
어댑터를 구현하는 일은 타겟 인터페이스로 지원해야 하는 인터페이스의 크기에 비례해서 복잡해지게 된다. 양쪽의 코드를 수정하지 않고도 호환성을 제공하는 다른 대안이 마땅치 않다. 모든 코드 변경 사항을 캡슐화시킨 클래스 한 개만 제공하는 방법이 깔끔하다.
어댑터는 하나의 클래스만 감싸야 하는가?
보통 어댑터 패턴은 한 인터페이스를 다른 인터페이스로 변환하기 위해 사용한다. 하지만 한 어댑터에서 타겟 인터페이스를 구현하기 위해 두 개 이상의 어댑티를 감싸야할 수도 있다.
호환되는 부분(신형)과 호환되지 않는 부분(구형)을 섞어서 사용한 시스템이 있으면 어떻게 해야하나?
이런 상황에서는 호환되는 부분, 호환되지 않는 부분의 두 인터페이스를 모두 지원하는 어댑터(Two Way Adapter)를 만들 수도 있다. 필요한 인터페이스를 둘 다 구현해서 어댑터가 기존 인터페이스와 새로운 인터페이스 역할을 모두 맡을 수 있도록 다중 어댑터로 구현할 수 있다.
클래스 어댑터
이제까지는 Adapter가 Target 인터페이스를 구현하고 Adaptee를 구성요소로 감싸는 객체 어댑터였다. 이와 반대로 클래스 어댑터는 타겟 인터페이스와 어댑티를 모두 상속한다. Adaptee를 적용시키는 데 있어서 구성을 사용하지 않고 다중 상속을 이용해 Adaptee와 Target 인터페이스 모두의 서브클래스를 만든다.
객체 어댑터 VS 클래스 어댑터
객체 어댑터는 구성을 사용해서 어댑티 클래스 뿐 아니라 그 서브 클래스에 대해서도 어댑터 역할이 가능하다.
대신, 객체 어댑터는 어댑티 전체를 다시 구현해야 한다.
클래스 어댑터는 상속을 사용해서 특정 어댑터 클래스에만 사용이 가능하다.
클래스 어댑터는 어댑티 전체를 다시 구현하지 않아도 된다. 어댑티의 서브클래스이기 때문에 어댑티의 행동을 오버라이드 할 수 있다.
클래스 어댑터는 상속을 이용하여 중복되는 코드를 줄일 수 있지만 객체 어댑터에 비해 유연성이 낮다.
객체 어댑터는 상속보다는 구성을 이용하라는 원칙을 지켰기 때문에 클래스 어댑터에 비해 유연성이 높다.
객체 어댑터는 어댑티한테 필요한 일을 시키기 위한 코드만을 만들면 되기 때문에 딱히 많은 코드가 필요하지 않다.
객체 어댑터는 어댑터 코드에 어떤 행동을 추가하면 그 어댑터 코드는 어댑티 클래스 뿐 아니라 모든 서브클래스에 대해서도 적용된다.
데코레이터 패턴과 어댑터 패턴
데코레이터가 적용된다는 것은 새로운 책임 또는 행동이 추가된다는 것을 의마한다.
반면 어댑터 패턴을 이용하면 변화를 클라이언트로 부터 분리할 수 있다.
데코레이터 패턴이 복잡하게 구성된 경우 메소드 호출이 얼마나 많은 데코레이터를 거쳐갔는지 얼마나 겹겹이 싸여있는지, 요청을 처리한 것에 대해 어떠한 연락을 받게 될지와 같은 추적이 어렵다.
겉으로 보기엔 객체를 감싸서 다른 행동을 하거나 변환하기 때문에 비슷해보이지만, 각각의 패턴의 목적, 용도에 확연한 차이가 있다.