참고: Head First Design Patterns
일상 생활에서 어댑터라는 용어를 흔히 접해보았을 것이다. 어댑터가 하는 역할이 정확히 무엇일까?
객체지향에서의 어댑터도 크게 다르지 않다. 기존 프로그램에 적용시켜야 할 새로운 클래스가 있는데, 인터페이스가 달라 서로 호환이 되지 않는다고 생각해보자. 이럴 때, 기존 클래스의 요청을 새로운 클래스에게 적용시킬 어댑터 클래스를 만들어 새로운 클래스에게 전달한다면 기존 프로그램과 새로운 클래스 모두 코드의 수정 없이 호환되도록 만들어 줄 수 있다.
어댑터가 무엇을 하는지 알아보았으니, 간단한 코드 예제를 통해 더 자세히 살펴보자.
public interface Duck {
public void quack();
public void fly();
}
public class MallardDuck implements Duck {
public void quack() {
// 꽥꽥 울기
}
public void fly() {
// 날기
}
}
[디자인 패턴] 1. the Strategy Pattern 의 Duck
을 오랜만에 데려와보았다. Duck
인터페이스와 그 구현체 MallardDuck
이 있다.
public interface Turkey {
public void gobble();
public void fly();
}
public class WildTurkey implements Turkey {
public void gobble() {
// 꾸륵거리기 (칠면조 울음소리..)
}
public void fly() {
// 날기
}
}
위와 같이 Turkey
를 만들었다. 칠면조는 오리와는 다른 인터페이스를 구현하며 당연히 메소드명도 다를 수 있다.
그런데, 어떠한 이유로 Duck
객체를 다루는 코드가 Turkey
도 다룰 수 있게 하기로 결정했다.
void testDuck(Duck duck) {
duck.quack();
duck.fly();
}
위와 같은 메소드에 Turkey
객체를 넣으면 어떻게 될까? 당연히 에러가 난다. 매개변수의 타입 자체가 Turkey
가 아니기 때문이다. 그렇다면, 위 메소드가 Turkey
를 매개변수로 받을 수 있도록 어댑터 클래스를 만들어보자!
public class TurkeyAdapter implements Duck {
Turkey turkey;
public TurkeyAdapter(Turkey turkey) {
this.turkey = turkey;
}
public void quack() { turkey.gobble(); }
public void fly() { turkey.fly(); }
}
이제 어댑터를 적용해보자.
Turkey turkey = new WildTurkey();
Duck turkeyAdapter = new TurkeyAdapter(turkey);
testDuck(turkeyAdapter);
이제 위 코드는 오류가 나지 않을 것이다. Turkey
를 TurkeyAdapter
로 감싸주면서 testDuck()
에게 칠면조를 오리로 분장시켜 속여넘기는데 성공했기 때문이다.
어댑터 패턴은 아래와 같이 정의할 수 있다.
클래스의 인터페이스를 클라이언트의 요구사항에 맞는 다른 인터페이스로 변환해주는 디자인 패턴.
이를 클래스 다이어그램으로 살펴보면 아래와 같다.
타겟(Target)은 변환의 결과물, 어댑티(Adaptee)는 변환의 대상이라고 보면 좋을 것 같다. 우리의 예시에서는 Duck
이 타겟, Turkey
가 어댑티가 되겠다.
다이어그램을 통해 우리는 어댑터가 구성(Composition)을 이용해 어댑티를 감싸주고 있는데, 이를 통해 알 수 있는 점은 어댑티의 그 어떤 서브클래스에도 어댑터를 사용할 수 있다.
또한, 클라이언트가 구현체 대신 인터페이스와 연결된 것 또한 꾸준히 강조되는 디자인 원칙을 잘 지키고 있다는 점을 알 수 있다.
우리가 지금까지 살펴본 종류의 어댑터를 객체 어댑터(Object Adapter)라고 부른다. 이 외에 클래스 어댑터(Class Adapter)라는 것이 존재한다. 그러나, 클래스 어댑터는 다중 상속을 이용해서 구현하는데 알다시피 Java는 이를 지원하지 않는다.
클래스 다이어그램을 보면, 구성을 사용하는 객체 어댑터와 달리 어댑터 클래스가 타겟과 어댑티 모두를 상속함으로써 구현된다. 객체 어댑터가 어댑티를 타겟으로 감싸주는 느낌이라면, 클래스 어댑터는 타겟과 어댑터 사이를 조율하는 느낌이다.
현재 Java의 Collections
는 Iterator
통해 구현되어있다. 그러나, 초창기에는 Enumemration
으로 구현되어있었다.
Enumeration
을 Iterator
로 변환해주는 어댑터를 구현해보자.우선, 다이어그램을 먼저 그려보면서 방향을 잡아보자.
잘 살펴보니, 같은 역할을 하는 대응되는 메소드들이 있다
hasMoreElements()
와 hasNext()
: 컬렉션에 요소가 더 있는지를 알려준다.nextElement()
와 next()
: 컬렉션의 다음 요소를 리턴한다.그런데, Enumeration
에게는 remove()
메소드가 없다. Enumeration
은 Read-Only 인터페이스로 remove()
메소드를 지원하지 않으므로, 이 기능이 완벽히 동작하도록 어댑터를 구현할 수 없다. 우리가 할 수 있는 최선은 런타임 예외를 throw
해주는 것이다.
public class EnumerationIterator implements Iterator {
Enumeration enum;
public EnumerationIterator(Enumeration enum) {
this.enum = enum;
}
public boolean hasNext() { return enum.hasMoreElements(); }
public Object next() { return enum.nextElement(); }
public void remove() { throw new UnsupportedOperationException(); }
}
따라서, 완성한다면 위와 같은 코드가 된다.
우린 인터페이스를 다른 인터페이스로 변환해주는 어댑터 패턴에 대해 살펴보았다. 여기, 인터페이스를 다른 인터페이스로 대체해주는, 그러나 어댑터 패턴과는 다른 목적을 갖는 디자인 패턴이 있다.
facade [명사]
1. (건물의) 정면[앞면]
2. (실제와는 다른) 표면, 허울
영단어 facade(파사드)의 사전적 정의이다. 2번 정의에서 짐작할 수 있듯, 파사드 패턴이 인터페이스를 대체하는 목적은 잘 만든 깔끔한 허울 뒤로 하나 혹은 그 이상의 클래스들의 복잡성을 숨기기 위함이다.
기술이 많이 발전한 현재 빔프로젝터만 있다면 집에 홈시어터(Home Theater)를 만드는 것은 크게 복잡하지 않다. 그러나, 10년 혹은 20년 전 홈시어터를 만드는 것은 대단히 손이 많이 가는 일이었을 것이다.
빔프로젝터, DVD 플레이어, 스크린, 조명, 앰프(Amplifier, 음향 기기), 심지어 팝콘 기계까지 준비할 것이 너무 많다!
이를 모두 코드로 구현한다면,
popper.on(); // 팝콘 기계 켜기
popper.pop(); // 팝콘 만들기
lights.dim(10); // 조명 어둡게 하기 (밝기 10%)
screen.down(); // 스크린 내리기
projector.on(); // 프로젝터 켜기
projector.setInput(dvd); // 프로젝터 인풋을 DVD로 설정하기
projector.wideScreenMode(); // 프로젝터를 와이드 스크린 모드로 설정하기
amp.on(); // 앰프 켜기
amp.setDvd(dvd); //앰프 인풋을 DVD로 설정하기
amp.setSurroundSound(); // 앰프를 서라운드 사운드 모드로 설정하기
amp.setVolume(5); // 앰프 볼륨을 5로 설정하기
dvd.on(); // DVD 플레이어 켜기
dvd.play(movie); // DVD 플레이어 재생하기
위와 같을 것이다. 그런데 이것 만이 문제가 아니다. 영화가 끝나면 이 모든 작업을 역순으로 똑같이 해야한다. 또한, DVD 플레이어 뿐만 아니라 라디오 혹은 CD를 들을 때도 이처럼 복잡한 작업을 거쳐야한다.
public class HomeTheaterFacade {
Amplifier amp;
Tuner tuner;
DvdPlayer dvd;
CdPlayer cd;
Projector projector;
TheaterLights lights;
Screen screen;
PopcornPopper popper;
public HomeTheaterFacade(
Amplifier amp,
Tuner tuner,
DvdPlayer dvd,
CdPlayer cd,
Projector projector,
Screen screen,
TheaterLights lights,
PopcornPopper popper) {
this.amp = amp;
this.tuner = tuner;
this.dvd = dvd;
this.cd = cd;
this.projector = projector;
this.screen = screen;
this.lights = lights;
this.popper = popper;
}
...
}
파사드 클래스는 홈시어터에 필요한 모든 장비들을 생성자를 통해 클라이언트로부터 넘겨받는다. 즉, 컴포지션(구성)을 이용하고 있다.
public void watchMovie(String movie) {
popper.on();
popper.pop();
lights.dim(10);
screen.down();
projector.on();
projector.wideScreenMode();
amp.on();
amp.setDvd(dvd);
amp.setSurroundSound();
amp.setVolume(5);
dvd.on();
dvd.play(movie);
}
public void endMovie() {
popper.off();
lights.on();
screen.up();
projector.off();
amp.off();
dvd.stop();
dvd.eject();
dvd.off();
}
위에서 클라이언트가 직접 하나하나 장비들을 세팅해야만 했던 것에 비해 파사드는 편리하고 직관적인 메소드 하나로 모든 로직을 감싸주었다. 따라서 영화를 종료하는 기능 역시 endMovie()
의 호출 한 번으로 간단히 실행할 수 있을 것이다.
파사드 패턴은 아래와 같이 정의한다
서브시스템을 더 편리하게 사용하도록 하는, 서브시스템 인터페이스들의 상위 인터페이스를 제공하는 디자인 패턴.
우리의 HomeTheaterFacade
를 살펴보면 이것이 무슨 의미인지 이해하기 크게 어렵지 않다.
그러나, 한 가지 의문이 들 수 있다.
homeTheaterFacade.getDvd.on();
DVD 전원을 수동으로 켜지 못하고, 어딘가에 굴러다니는 파사드라는 리모콘을 찾아야지만 켤 수 있는게 아닌가?.
만약 서브시스템들이 파사드 클래스에 선언, 즉 캡슐화 되어있다면 클라이언트는 위와 같이 파사드를 거쳐서 서브시스템에 접근해야만 할 것이다. 그러나 파사드는 서브시스템을 캡슐화 하지 않는다. 단지, 편리한 인터페이스를 제공할 뿐이다.
Amplifier amp = new Amplifier();
Tuner tuner = new Tuner();
... // 필요한 장비들의 인스턴스 선언
HomeTheaterFacade homeTheater = new HomeTheaterFacade(
amp, tuner, dvd, cd, projector, screen, lights, popper
);
homeTheater.watchMovie("Begin Again");
homeTheater.endMovie();
파사드는 서브시스템을 넘겨 받을 뿐, 서브시스템의 선언은 클라이언트에서 이루어진다. 따라서, 클라이언트는 얼마든지 서브시스템을 직접 접근할 수 있다.
watchMovie()
, endMovie()
를 가져도 되는 것 아닌가? 파사드라는 클래스로 분리하는 것의 의의가 무엇인가?!바로, 클라이언트와 서브시스템의 결합을 느슨하게 해줄 수 있다는 점이다. watchMovie()
등 서브시스템을 직접적으로 다루는 메소드가 클라이언트에게 있다면, 서브시스템의 변경은 곧 클라이언트의 코드 수정으로 이어진다.
파사드 클래스로 분리함으로써, 이를 테면 DVD 플레이어와 앰프 대신 스마트폰과 블루투스 스피커로 교체한다던지, 서브시스템이 바뀌더라도 클라이언트의 코드 대신 파사드의 코드만 수정해주면 된다.
친한 친구들과만 상호작용하라.
오늘의 디자인 원칙이다. 이는 클래스들 사이의 연결에 유의하면서 프로그래밍하라는 의미이다.
이 원칙은 다음과 같은 가이드라인을 제시한다.
아래에 해당하는 객체들의 메소드만을 호출하라
public float getTemp() {
Thermometer thermometer = station.getThermometer();
return thermometer.getTemparature();
}
위 코드에서는 station
으로부터 Thermometer
객체를 불러오고, 또 getTemperature()
을 실행하고 있다. 즉, 아래와 같은 모양새이다.
station.getThermometer.getTemperature();
가이드라인에 어긋나는 메소드 호출을 하고있다.
만약 Thermometer
를 불러와 Temperature
를 받는 내용을 Station
클래스에 추가한다면, 우리 클래스의 의존성에서 Thermometer
를 덜어낼 수 있다.
public float getTemp() {
return station.getTemperature();
}
따라서, 위와 같이 원칙을 만족하는 메소드가 된다.
public float getTemp() {
Thermometer thermometer = station.getThermometer();
return getTempHelper(thermometer);
}
public float getTempHelper(Thermometer thermometer) {
return thermometer.getTemperature();
}
의외로 위 코드는 원칙을 어기지 않는다! 아니, Thermometer
로부터 TempHelper
을 호출하는 것은 사실,
station.getThermometer().getTemperature();
과 다를게 없지 않은가?
그러나 놀랍게도, getTemp()
에서는 인스턴스 변수의, getTempHelper()
에서는 파라미터로 넘겨 받은 객체의 메소드를 호출하기 때문에 원칙에 어긋나지 않는다. 원칙의 맹점이자 일종의 꼼수(Hack)이라고 할 수 있겠다.