어댑터 패턴(Adapter Pattern)
특정 클래스의 인터페이스를 다른 인터페이스로 변환해주는 패턴입니다. 두 인터페이스가 서로 호환되지 않을 때 유용합니다.
퍼사드 패턴(Facade Pattern)
서브 시스템에 있는 각 인터페이스를 통합 인터페이스로 묶어줌으로써, 일련의 과정을 단순화시키는 패턴입니다.
🦆 점점 커지는 오리 시뮬레이션 게임
당신은 1인 개발자로서 오리 시뮬레이션 게임을 만들어서 운영하고 있습니다. 그런데 이 게임이 게이머들 사이에서 입소문을 타게 되면서 점점 인기가 많아지게 됐고, 게임의 규모를 더 키우기 위해 경쟁사 게임이었던 칠면조 시뮬레이션 게임과 병합하기로 결정합니다.
그런데 두 게임이 합쳐져서 동작하려면 두 클래스가 서로 호환이 되어야 하겠네요.
우선 기존의 오리 클래스와 호환되도록 변환해야 하는 칠면조 클래스는 아래와 같습니다.
public interface Turkey {
public void gobble();
public void fly();
}
class WildTurkey implements Turkey {
public void gobble() {
System.out.println("골골..");
}
public void fly() {
System.out.println("짧게 날고 있어요!"); // 칠면조는 멀리 날지 못합니다..
}
}
그리고 오리 클래스는 아래와 같습니다.
public interface Duck {
public void quack();
public void fly();
}
class MallardDuck implements Duck {
public void quack() {
System.out.println("꽥!");
}
public void fly() {
System.out.println("멀리 날고 있어요!"); // 칠면조와는 다르게 멀리 날 수 있어요!
}
}
코드를 합치려고 보니, 기존의 오리 클래스와 칠면조 클래스는 구조도 다르고 동작하는 방식도 제각각이라서, 어느 한 쪽이 두 클래스가 호환되도록 클래스를 수정해야 하는 상황이네요. 두 사람 모두 기존 클래스를 수정하면 그로 인한 side effect가 크기 때문에 부담이 되는 상황인 것이죠.
그래서 당신은 두 클래스가 서로 호환될 수 있도록 변환해주는 클래스를 생성하기로 결정합니다.
public class TurkeyAdapter implements Duck {
public Turkey turkey;
public TurkeyAdapter(Turkey turkey) {
this.turkey = turkey;
}
public void quack() {
turkey.gobble();
}
public void fly() {
turkey.fly();
}
}
이 어댑터 클래스는 오리 인터페이스를 구현하고, 칠면조 클래스를 인자로 받아서 오리 클래스와 호환되도록 변환해주고 있습니다.
이제 동작을 테스트 해보면 아래와 같이 할 수 있겠죠?
public class Main {
public static void main(String[] args) {
WildTurkey wildTurkey = new WildTurkey();
TurkeyAdapter turkeyAdapter = new TurkeyAdapter(wildTurkey);
testDuck(duckAdaptor);
}
// testDuck은 인자(duck)가 오리인지 칠면조인지 구분하지 못합니다!
static void testDuck(Duck duck) {
duck.quack();
duck.fly();
}
}
위처럼 한 클래스가 다른 클래스와 호환되도록 변환해주는 방식을 어댑터 패턴이라고 합니다. 위 예제에선 코드가 얼마 안 돼서 그냥 오리 또는 칠면조 클래스를 직접 수정해도 무리가 없지만, 클래스 규모가 커질수록 기존 클래스를 수정하기가 어렵기 때문에, 변경되는 부분을 별도의 클래스로 캡슐화해서 어댑터로 만드는 방법이 더 효율적일 수 있습니다.
어댑터 패턴에는 객체 어댑터, 클래스 어댑터 두 가지 종류가 있는데, 위와 같은 어댑터 패턴은 구성을 활용하는 객체 어댑터 패턴으로 불리고, 다중 상속이 가능한 언어에서는 상속을 활용하는 클래스 어댑터로 어댑터 패턴을 구현할 수 있습니다.
🎦 나만의 영화관 만들기
영화를 좋아하는 사람이라면 방을 영화관으로 직접 꾸며보는 것이 로망일 수 있습니다. 그런데 편하게 영화만 보면 좋겠지만 영화를 보기 전 준비하는 과정들이 있을 수 있겠네요.
- 팝콘 기계를 켠다.
- 팝콘을 튀긴다.
- 조명을 어둡게 조절한다.
- 스크린을 내린다.
- 프로젝터를 켠다.
- ....
- 영화를 재생한다.
생각보다 영화를 보기 전에 해야 할 일들이 많네요. 각 기계들을 클래스로 구현해본다면 아래처럼 만들 수도 있겠네요.
그럼 위 준비 과정을 실제로 구현해볼까요?
popper.on();
popper.pop();
lights.dim(10);
screen.down();
projector.on();
projector.setInput(player);
...
player.on();
player.play(movie);
- 영화를 보기 위해 클래스가 5개 이상으로, 많은 클래스들이 필요합니다.
- 영화가 끝나면 어떻게 꺼야 할까요? 위 과정을 역순으로 처리해야 하지 않을까요?
- 라디오를 들을 때도 이렇게 복잡할까요?
- 시스템을 업그레이드하면 작동 방법을 또 배워야 하지 않을까요?
위와 같이 영화 한 편을 보려고 스트레스를 엄청 많이 받을 것 같네요. 두 번째부턴 그냥 휴대폰으로 보게 될 것 같습니다.
그럼 위와 같은 문제를 퍼사드 패턴으로 해결할 수 있는지 알아봅시다.
사용하기 쉬운 메소드 등을 제공하는 퍼사드 클래스를 구현함으로써, 위와 같은 과정을 단순화시킬 수 있습니다.
위 그림에서 HomeTheaterFacade 클래스에 watchMovie(), endMovie() 메소드에 준비 과정과 같은 일련의 과정을 미리 구현해놓으면, 그 안에서 정확히 어떻게 처리되고 있는지는 몰라도 일련의 과정을 쉽게 실행하고 관리할 수 있습니다.
이러한 패턴을 퍼사드 패턴이라고 하며, 퍼사드 클래스나 요소의 개수는 제한 없이 다양하게 활용할 수 있습니다.
✅ 최소 지식 원칙(Principle of Least Knowledge, 데메테르 법칙)
위와 같이 어댑터 패턴, 퍼사드 패턴으로 클래스를 만들다 보면 다양한 클래스의 메소드를 사용하게 되기 때문에 결합도가 높아질 수 있습니다. 예를 들어, 사용하고 있는 클래스가 변경되면 관련된 어댑터, 퍼사드 클래스를 전부 수정해야 하는 상황이 생길 수 있겠죠.
그래서 객체지향 디자인 원칙 중 최소 지식 원칙을 활용할 필요가 있습니다. 최소 지식 원칙은 객체 사이의 결합도를 낮추기 위해 상호작용은 되도록 줄이는 것이 목표이며, 가이드 라인은 아래와 같습니다.
- 객체 자신의 메소드 사용 가능
- 메소드에 매개변수로 전달된 객체 사용 가능
- new 키워드로 인스턴스를 만든 객체 사용 가능
- 객체 자신에 속해 있는 인스턴스 등 구성 요소 사용 가능
// 원칙을 따르지 않은 경우
public float getTemp() {
Thermometer thermometer = station.getTermometer();
return thermometer.getTemperature(); // 다른 객체의 메소드를 통해 생성된 인스턴스의 메소드를 사용하면 안 됩니다!
}
// 원칙을 따르는 경우
public float getTemp() {
return station.getTemperature(); // 객체 자신에 속하는 인스턴스의 메소드는 그냥 사용해도 됩니다.
}
이번엔 다른 예제를 볼까요?
public class Car {
Engine engine;
// 기타 인스턴스 변수
public Car() {
// engine 초기화 등 처리
}
public void start(Key key) {
Doors doors = new Doors();
boolean authorized = key.turns(); // 매개변수로 받은 인스턴스의 메소드는 사용해도 됩니다.
if (authorized) {
engine.start(); // 객체 자신에 속하는 인스턴스의 메소드는 사용해도 됩니다.
updateDashboardDisplay(); // 객체 자신의 메소드는 사용해도 됩니다.
doors.lock(); // new 키워드로 만든 인스턴스의 메소드는 사용해도 됩니다.
}
}
public void updateDashboardDisplay() {
// 디스플레이 갱신
}
}
그럼 아래 예제에선 어떤 부분이 원칙을 위반하고 있는지 살펴보겠습니다.
public class House {
WeatherStation station;
public float getTemp() {
// 객체에 속한 메소드를 사용하고 있지만, 메소드로 만든 인스턴스의 메소드를 호출하면 안 됩니다!
return station.getTermometer().getTemperature();
}
}
public class House {
WeatherStation station;
public float getTemp() {
Thermometer termometer = station.getTermometer();
// 원칙을 위반..하는 것처럼 보였지만 인스턴스를 자신의 메소드로 전달해서 사용했으므로 아슬아슬하게 세이프입니다!
return getTempHelper(thermometer);
}
public float getTempHelper(Thermometer thermometer) {
return termometer.getTemperature();
}
}
- 논리적으로는 원칙을 위반하진 않았지만.. 교묘하게 다른 메소드에게 떠넘겼다고 해서 정말로 뭔가 달라지는 걸까요?
어댑터 패턴, 퍼사드 패턴에서 배울 수 있는 객체지향 원칙
- 바뀌는 부분은 캡슐화한다.
- 상속보다는 구성을 활용한다.
- 구현보다는 인터페이스에 맞춰서 프로그래밍한다.
- 상호작용하는 개체 사이에서는 가능하면 느슨한 결합을 사용해야 한다.
- 클래스는 확장에는 열려 있어야 하지만 변경에는 닫혀 있어야 한다(OCP).
- 추상화된 것에 의존하게 만들고 구상 클래스에 의존하지 않게 만든다.
- 진짜 절친에게만 이야기해야 한다(최소 지식 원칙).
어댑터 패턴(Adapter Pattern)
특정 클래스의 인터페이스를 다른 인터페이스로 변환해주는 패턴입니다. 두 인터페이스가 서로 호환되지 않을 때 유용합니다.
퍼사드 패턴(Facade Pattern)
서브 시스템에 있는 각 인터페이스를 통합 인터페이스로 묶어줌으로써, 일련의 과정을 단순화시키는 패턴입니다.
'Fundamental > Design Pattern' 카테고리의 다른 글
[디자인 패턴] 9. 반복자 패턴, 컴포지트 패턴 (0) | 2024.11.03 |
---|---|
[디자인 패턴] 8. 템플릿 메소드 패턴 (1) | 2024.10.26 |
[디자인 패턴] 6. 커맨드 패턴(Command Pattern) (0) | 2024.09.29 |
[디자인 패턴] 5. 싱글턴 패턴(Singleton Pattern) (0) | 2024.09.20 |
[디자인 패턴] 4. 팩토리 패턴(Factory Pattern) (0) | 2024.09.10 |