어댑터란 특정 클래스의 인터페이스를, 클라이언트에서 요구하는 다른 인터페이스로 변환하는 중개자 클래스를 의미한다. 서로 다른 인터페이스여서 같이 사용할 수 없었던 클래스를 사용할 수 있게 도와준다.
즉, 서로 호환되지 않는 두개 인터페이스를 동일하게 변환하여 연결한다!
어댑터는 실제 타깃 인터페이스를 구현하고, 어댑터 내부에는 적응하려는 다른 객체를 구성하고 있어(필드로 가진다) 해당 객체에 호출을 위임하는 형태이다.
Target
: 클라이언트에서 요구하는 인터페이스이다.Adapter
: Target
을 상속받아 형식을 맞추고, 필드로 Adaptee
를 가지고 있어서 해당 클래스의 함수를 호출시킨다.Adaptee
: 기존 변환하기 이전 클래스이다.예를 들어, Turkey 를 Duck 처럼 사용하고 싶다 하자. 그러면 다음과 같은 어댑터를 만들기만 하면 끝이다.
public class TurkeyAdapter implements Duck {
Turkey turkey; // 구성
public TurkeyAdapter(Turkey turkey) {
this.turkey = turkey;
}
public void quack(){
turkey.gobble(); // 독립적으로 작동한다.
}
public void fly() {
for(int i=0; i < 5; i++) {
turkey.fly();
}
}
}
List<Duck> ducks = new ArrayList<>();
Duck duck = new MallardDuck();
Duck turkeyAdapter = new TurkeyAdapter(new WildTurkey());
ducks.add(duck);
ducks.add(turkeyAdapter);
for(Duck duck: ducks) {
duck.quack(); // Duck과 같이 동작한다.
duck.fly();
}
클라이언트와 구현된 인터페이스를 분리하여 변경 내역이 어댑터에 캡슐화되기에 , 나중에 인터페이스가 바뀌어도 클라이언트 코드는 변경되지 않는다.
어댑터는 객체 구성(컴포지션)을 활용하는 디자인 패턴이다. 상속이 아닌 컴포지션을 사용하므로, 어댑티의 모든 하위 클래스들에도 어댑터를 써서 인터페이스를 변환시켜줄 수 있다는 장점이 있다.
만약 아래와 같이 어댑티가 어댑터를 확장하는 상속 형태였다면? 오버라이딩으로 인해 어댑터 코드가 달라졌을 때 그것을 상속받고 있는 어댑티에까지 영향이 가는 문제가 발생한다.
// Duck 변경 -> 어댑터 변경(인터페이스 구현) -> 어댑티 변경..
public class Turkey extends TurkeyAdapter {
@override
public void quack(){
..
}
}
🔖 데코레이터 패턴과 어댑터 패턴
둘다 컴포지션을 활용하므로 겉모습은 비슷해 보일 수 있지만 둘은 아예 다르다. 데코레이터는 감싸고 있는 객체의 행동과 책임을 확장하고, 어댑터는 감싸고 있는 인터페이스를 적응시키고자 하는 인터페이스로 변환시킨다.
Enumeration
은, Iterator
가 나오기 이전에 Vector
, Stack
, HashTable
과 같은 초기 컬렉션 형식을 순회할 수 있는 인터페이스이다. 아래와 같은 방식으로 사용한다.
Vector v = new Vector();
Enumeration e = v.elements();
Iterator it = v.iterator();
while (e.hasMoreElements()) {
System.out.println("" + e.nextElement());
}
while (it.hasNext()) {
System.out.println("" + it.next());
}
하지만 만약, iterator
은 제공하지만 enumeration
을 지원하지 않는 환경에서 enumeration
을 사용하고 싶다면, 어댑터를 통해 변환시켜주면 된다.
바꾸고자 하는 인터페이스에만 있고 자신에게는 없는 기능이 있다면, 런타임 예외를 던지는 것이 가장 좋다.
public class EnumerationIterator implements Iterator {
Enumeration enumeration; // composition
public EnumerationIterator(Enumeration enmt) {
this.enumeration = enmt;
}
public boolean hasNext() {
return enumeration.hasMoreElements();
}
public Object next() {
return enumeration.nextElement();
}
public void remove() {
throw new UnsupportedOperationException();
}
}
클라이언트는, 라이브러리 내부 구현이 바뀌어도 코드를 변경하지 않아도 된다.
Arrays.asList()
는 자바에서 배열을 고정 크기의 리스트로 변환하며 마치 배열을 List Interface
처럼 사용할 수 있도록, 배열을 감싸서 리스트 인터페이스인것처럼 보여지게 하는 하나의 어댑터이다.
단 리스트로 변환하더라도, 고정 크기이므로 add()
, remove()
는 사용 할 수 없다.
public class Arrays {
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
}
public class ArraysAdapter {
public static void main(String[] args) {
String[] cities = { "Seoul", "Incheon", "Busan", "Sejong" };
List<String> cityList = Arrays.asList(cities);
System.out.printf("cities.length = %d\n", cities.length); //List의 기능 사용
System.out.printf("cityList.size = %d\n", cityList.size());
}
}
스프링 MVC에서 HandlerAdapter
는, 다양한 종류의 핸들러중에 처리 가능한 핸들러를 찾아서 해당 핸들러에게 처리를 위임한다. 이에 DispatcherServlet
이 여러 종류의 핸들러들을 컨트롤 할 수 있게 되고, 무한으로 확장 가능한 구조가 되었다.
public interface HandlerAdapter {
boolean supports(Object handler);
ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;
}
이런 구조는 기존 코드를 변경하지 않고 원하는 인터페이스 구현체를 만들어 재사용할 수 있으며, 기존 코드가 하던 일과 특정 인터페이스 구현체로 변환하는 작업을 각자 다른 클래스로 분리하여 관리할 수 있다는 점이다. 즉 HandlerAdapter 덕분에 OCP(Open Closed Principal)가 지켜진다.
사실 어댑터 패턴의 규격화된 구조에 집중을 하는게 아니라, 어댑터 패턴 도입으로 인해 얻는 효과에 집중하여 유연하게 구조는 바꿔나갈 수 있을 것 같다.
서브시스템이 너무 많고 복잡할 때, 클라이언트가 쉽게 사용할 수 있도록 인터페이스들을 한 개의 고급 수준의 인터페이스로 통합하여 단순하게 변환시키는 패턴을 의미한다.
public class HomeTheaterFacade {
Amplifer amp;
Player player;
...
public void watchMovie() {
amp.on();
amp.setVolume(5);
player.play();
...
}
}
public class HomeTheaterTestDrive {
public static void main(String[] args) {
HomeTheaterFacade homeTheater = new HomeTheaterFacade(amp, tuner, dvd, cd, projector, screen, lights, popper);
homeTheater.watchMovie("Raiders of the Lost Ark");
homeTheater.endMovie();
}
}
클라이언트와 서브 시스템의 의존성을 최소화한다.
클라이언트는 더 간단한 인터페이스를 사용할 수 있으며, 구현과 서브시스템을 분리하여 클라이언트 코드를 변경하지 않아도 된다.
단, 서브시스템이 많아질수록 최소 지식 원칙을 준수해야 한다.
🔖 최소 지식 원칙
객체 지향 구현 시 각 모듈 간의 결합도를 최소화하여 설계한다는 원칙이다.
객체 사이 의존성을 최소화하면 여러 클래스가 얽혀있더라도, 한 부분에 변경이 일어났을 때 다른 부분들까지 줄줄히 고쳐야 하는 상황을 방지하여 유지보수가 편해진다. 하지만, 당연히 메소드 호출을 대신 처리하는 래퍼 클래스가 생성되면서 시스템 구조 자체는 복잡해질수도 있다.
여러 클래스를 걸쳐 메서드를 호출한다던지 다음과 같은 경우를 제외하는 최소 지식 원칙을 위반한다고 판단한다.
public float getTemp() { // 최소 지식 원칙 위반
Thermometer thermoeter = station.getThermometer();
return theromometor.getTemperature()
}
public float getTemp() { // 최소 지식 원칙 O
return station.getTemperature();
}