읽은 책 정리/Head Firstr Design Pattern

CHAPTER 07.어댑터 패턴과 퍼사드 패턴

어댑터 패턴 이해를 위한 간단한 문제 제안

  • 해외 직구를 통해 전자 제품을 구매했다고 가정하겠습니다.
  • 각 나라별로 사용되는 어뎁터 소켓이 다르기 때문에 변환기가 필요할 것입니다.
    • 일본은 110V를 사용하고 우리나라는 220V를 사용하는것이 예시가 될 수 있습니다.
  • 객체지향 코드에서도 위와 같은 비슷한 문제가 발생할 때가 있습니다.
    • 객체 인터페이스가 달라서 사용하지 못하는 경우가 예시가 될 수 있습니다.
    • 오리 인터페이스에 비슷한 칠면조 객체를 동작하도록 하고 싶은 문제 상황이라 가정해보겠습니다.
    • 이럴 경우 어댑터 패턴을 사용해 어떻게 해결하는지 살펴보겠습니다.

어댑터 패턴을 사용하여 문제 해결해보기

public interface Duck {
	public void quack();
	public void fly();
}
  • 먼저 오리 인터페이스를 정의해줍니다.
public class MallardDuck implements Duck {
	public void quack() {
		System.out.println("Quack");
	}
 
	public void fly() {
		System.out.println("I'm flying");
	}
}
  • 오리 인터페이스를 구현한 MallardDuck 객체입니다.
public interface Turkey {
	public void gobble();
	public void fly();
}
  • 칠면조 인터페이스입니다.
public class WildTurkey implements Turkey {
	public void gobble() {
		System.out.println("Gobble gobble");
	}
 
	public void fly() {
		System.out.println("I'm flying a short distance");
	}
}
  • 칠면조 인터페이스를 정의한 wildTurkey 객체입니다. WildTurkey 객체를 TukeyAAdpater를 사용해서 동작하도록 할 것입니다.
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();
		}
	}
}
  • TurkeyAdpater에서는 Duck 인터페이스를 구현해서 Turkey를 참조하고 Turkey의 행동들을 오리 인터페이스의 행동으로 정의해서 사용하도록 합니다.
public class DuckAdapter implements Turkey {
	Duck duck;
	Random rand;
 
	public DuckAdapter(Duck duck) {
		this.duck = duck;
		rand = new Random();
	}
    
	public void gobble() {
		duck.quack();
	}
  
	public void fly() {
		if (rand.nextInt(5)  == 0) {
		     duck.fly();
		}
	}
}
  • 반대로 오리의 행동을 칠면조의 행동으로 재정의해서 사용할 수 도 있습니다.

어댑터 패턴 살펴보기

  • 어댑터의 흐름은 위와 같습니다.
  • 클라이언트에서 어댑터를 사용하는 방법은 아래의 절차로 이루어집니다.
    • 먼저 클라이언트가 인터페이스로 메소드를 호출해서 어댑터에 요청을 보냅니다.
    • 어댑터는 내부에서 참조하고 있는 객체의 행동을 정의한 인터페이스안의 메소드들에서 대리로 호출합니다.
    • 클라이언트는 호출 결과를 받지만 중간에 어댑터가 있는 사실은 모릅니다.

어댑터 패턴의 정의

  • 어댑터 패턴은 특정 클래스 인터페이스를 클라이언트에서 요구하는 다른 인터페이스로 변환하는것이 핵식 목적인 디자인패턴입니다.
  • 인터페이스가 호환되지 않아 같이 사용할 수 없었던 클래스를 사용할 수 있게 도와줍니다.
  • 어댑터를 새로 바뀐 인터페이스로 감쌀 때는 객체 구성을 사용합니다. 이러한 접근 방법은 모든 서브클래스에 어댑터를 사용할 수 있다는 점입니다.

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

  • 어댑터 패턴은 객체 어댑터와 클래스 어댑터 두 가지 종류가 존재합니다.
  • 이번 단원에서 진행했던 내용들은 객체 어댑터 방식으로 설명이 진행되었습니다.
  • 클래스 어댑터에 대한 자세한 설명은 진행하지 않습니다.
    • 클래스 어댑터를 사용하기 위해서는 다중 상속이 가능해야 하는데 자바에서는 다중 상속이 불가능하기 때문입니다.
  • 간단하게 다이어그램만 확인하고 넘어가겠습니다.
    • 클래스 어댑터는 Target과 어댑터 모두 서브클래스로 만들어서 사용하고 객체 어댑터는 구성으로 어댑터에 요청을 전달하는 점을 제외하고는 다른게 없습니다.

데코레이터 패턴과 어댑터 패턴의 차이점

  • 두 패턴 모두 클라이언트에서 코드를 고치지 않고 새로운 라이브러리를 사용할 수 있다는 장점이 존재합니다. 알아서 변환해주기 때문이죠.
  • 하지만 두 패턴은 확실한 차이점이 존재합니다. 데코레이터 패턴은 기능 확장을 위해 사용한다는 점에 포커스가 맞춰져있는 디자인패턴이고 어댑터패턴은 특정 클래스의 인터페이스를 변경해서 사용하는것이 목적인 디자인패턴이라는 점에서 차이점이 존재합니다.

퍼사드 패턴 이해를 위한 간단한 문제 제안

  • 집에서 미디어 매체를 매니아처럼 시청하기 위해 소프트웨어를 설계할려고 합니다.
  • 간단하게 생각만 해봐도 영화를 키기 위한 리모컨, 영화가 보여져야할 Screen 설정 등 많은 유형의 클래스들이 필요합니다.
  • 위와 같은 클래스를 구현하고 미디어를 시청하기 위해서 동작시킬 메소드를 확인해보니 위와 같습니다.
  • 구현할 클래스는 6개나 있어야하고 수행해야 할 메소드는 13개나 존재합니다.
    • 심지어 영화가 끝나고 나서는 종료를 위한 행동도 수행해야합니다.
  • 위처럼 다양한 클래스를 사용해야하는 상황이 너무 복잡한 경우에는 퍼사드 패턴으로 해결할 수 있습니다.

퍼사드 패턴이란

  • 퍼사드 패턴은 서브시스템에 있는 일련의 인터페이스를 통합 인터페이스로 묶어줍니다.
  • 사용자가 수행할 행동에 적합한 고수준 인터페이스를 정의하여 서브시스템을 더 편리하게 사용할 수 있습니다.
  • 퍼서드 패턴은 인터페이스를 간단하게 변경하기 위해 사용하는 디자인 패턴입니다.
💡
퍼사드 패턴 VS 데코레이터 패턴 VS 퍼사드 패턴
데코레이터어댑터퍼사드
용도하나의 인터페이스를 다른 인터페이스로 변환하나의 인터페이스를 다른 인터페이스로 변환인터페이스를 간단하게 변경

퍼사드 패턴 적용해보기

  • 구현할 예시의 전체 그림입니다.
  • 위의 그림을 퍼서트 패턴을 도입해 다양한 서브클래스의 기능을 간편화한 공통 인터페이스인 HomeTheaterFacade를 구현해보도록 하겠습니다.
public class HomeTheaterFacade {
	Amplifier amp;
	Tuner tuner;
	StreamingPlayer player;
	CdPlayer cd;
	Projector projector;
	TheaterLights lights;
	Screen screen;
	PopcornPopper popper;
 
//퍼사트 패턴의 핵심은 생성자에서 사용할 클래스를 정의하고 아래 기타메소드에서 수행할 행동들을 정의해서 사용하는것이다.
	public HomeTheaterFacade(Amplifier amp,
				 Tuner tuner,
				 StreamingPlayer player,
				 Projector projector,
				 Screen screen,
				 TheaterLights lights,
				 PopcornPopper popper) {
 
		this.amp = amp;
		this.tuner = tuner;
		this.player = player;
		this.projector = projector;
		this.screen = screen;
		this.lights = lights;
		this.popper = popper;
	}
 
//기타 메소드
	public void watchMovie(String movie) {
		System.out.println("Get ready to watch a movie...");
		popper.on();
		popper.pop();
		lights.dim(10);
		screen.down();
		projector.on();
		projector.wideScreenMode();
		amp.on();
		amp.setStreamingPlayer(player);
		amp.setSurroundSound();
		amp.setVolume(5);
		player.on();
		player.play(movie);
	}
 
 
	public void endMovie() {
		System.out.println("Shutting movie theater down...");
		popper.off();
		lights.on();
		screen.up();
		projector.off();
		amp.off();
		player.stop();
		player.off();
	}

	public void listenToRadio(double frequency) {
		System.out.println("Tuning in the airwaves...");
		tuner.on();
		tuner.setFrequency(frequency);
		amp.on();
		amp.setVolume(5);
		amp.setTuner(tuner);
	}

	public void endRadio() {
		System.out.println("Shutting down the tuner...");
		tuner.off();
		amp.off();
	}
}
  • 코드 부분을 보면 사용할 서브시트템의 인스턴수 병수가 선언된것을 확인할 수 있습니다.
  • 생성자에는 서브시스템의 구성요소가 레퍼런스로 전달되게 됩니다.
  • 수행되어야 하는 행동들은 메소드에서 관리하게 됩니다.
SubClass Code
public class Amplifier {
	String description;
	Tuner tuner;
	StreamingPlayer player;
	
	public Amplifier(String description) {
		this.description = description;
	}
 
	public void on() {
		System.out.println(description + " on");
	}
 
	public void off() {
		System.out.println(description + " off");
	}
 
	public void setStereoSound() {
		System.out.println(description + " stereo mode on");
	}
 
	public void setSurroundSound() {
		System.out.println(description + " surround sound on (5 speakers, 1 subwoofer)");
	}
 
	public void setVolume(int level) {
		System.out.println(description + " setting volume to " + level);
	}

	public void setTuner(Tuner tuner) {
		System.out.println(description + " setting tuner to " + player);
		this.tuner = tuner;
	}
  
	public void setStreamingPlayer(StreamingPlayer player) {
		System.out.println(description + " setting Streaming player to " + player);
		this.player = player;
	}
 
	public String toString() {
		return description;
	}
}
public class Tuner {
	String description;
	Amplifier amplifier;
	double frequency;

	public Tuner(String description, Amplifier amplifier) {
		this.description = description;
	}

	public void on() {
		System.out.println(description + " on");
	}

	public void off() {
		System.out.println(description + " off");
	}

	public void setFrequency(double frequency) {
		System.out.println(description + " setting frequency to " + frequency);
		this.frequency = frequency;
	}

	public void setAm() {
		System.out.println(description + " setting AM mode");
	}

	public void setFm() {
		System.out.println(description + " setting FM mode");
	}

	public String toString() {
		return description;
	}
}
public class StreamingPlayer {
	String description;
	int currentChapter;
	Amplifier amplifier;
	String movie;
	
	public StreamingPlayer(String description, Amplifier amplifier) {
		this.description = description;
		this.amplifier = amplifier;
	}
 
	public void on() {
		System.out.println(description + " on");
	}
 
	public void off() {
		System.out.println(description + " off");
	}
 
	public void play(String movie) {
		this.movie = movie;
		currentChapter = 0;
		System.out.println(description + " playing \"" + movie + "\"");
	}

	public void play(int chapter) {
		if (movie == null) {
			System.out.println(description + " can't play chapter " + chapter + " no movie selected");
		} else {
			currentChapter = chapter;
			System.out.println(description + " playing chapter " + currentChapter + " of \"" + movie + "\"");
		}
	}

	public void stop() {
		currentChapter = 0;
		System.out.println(description + " stopped \"" + movie + "\"");
	}
 
	public void pause() {
		System.out.println(description + " paused \"" + movie + "\"");
	}

	public void setTwoChannelAudio() {
		System.out.println(description + " set two channel audio");
	}
 
	public void setSurroundAudio() {
		System.out.println(description + " set surround audio");
	}
 
	public String toString() {
		return description;
	}
}
public class CdPlayer {
	String description;
	int currentTrack;
	Amplifier amplifier;
	String title;
	
	public CdPlayer(String description, Amplifier amplifier) {
		this.description = description;
		this.amplifier = amplifier;
	}
 
	public void on() {
		System.out.println(description + " on");
	}
 
	public void off() {
		System.out.println(description + " off");
	}

	public void eject() {
		title = null;
		System.out.println(description + " eject");
	}
 
	public void play(String title) {
		this.title = title;
		currentTrack = 0;
		System.out.println(description + " playing \"" + title + "\"");
	}

	public void play(int track) {
		if (title == null) {
			System.out.println(description + " can't play track " + currentTrack + 
					", no cd inserted");
		} else {
			currentTrack = track;
			System.out.println(description + " playing track " + currentTrack);
		}
	}

	public void stop() {
		currentTrack = 0;
		System.out.println(description + " stopped");
	}
 
	public void pause() {
		System.out.println(description + " paused \"" + title + "\"");
	}
 
	public String toString() {
		return description;
	}
}
public class Projector {
	String description;
	StreamingPlayer player;
	
	public Projector(String description, StreamingPlayer player) {
		this.description = description;
		this.player = player;
	}
 
	public void on() {
		System.out.println(description + " on");
	}
 
	public void off() {
		System.out.println(description + " off");
	}

	public void wideScreenMode() {
		System.out.println(description + " in widescreen mode (16x9 aspect ratio)");
	}

	public void tvMode() {
		System.out.println(description + " in tv mode (4x3 aspect ratio)");
	}
  
        public String toString() {
                return description;
        }
}
public class TheaterLights {
	String description;

	public TheaterLights(String description) {
		this.description = description;
	}

	public void on() {
		System.out.println(description + " on");
	}

	public void off() {
		System.out.println(description + " off");
	}

	public void dim(int level) {
		System.out.println(description + " dimming to " + level  + "%");
	}

	public String toString() {
		return description;
	}
}
public class Screen {
	String description;

	public Screen(String description) {
		this.description = description;
	}

	public void up() {
		System.out.println(description + " going up");
	}

	public void down() {
		System.out.println(description + " going down");
	}


	public String toString() {
		return description;
	}
}
public class PopcornPopper {

    String description;

    public PopcornPopper(String description) {
        this.description = description;
    }

    public void on() {
        System.out.println(description + " on");
    }

    public void off() {
        System.out.println(description + " off");
    }

    public void pop() {
        System.out.println(description + " popping popcorn!");
    }
  

    public String toString() {
        return description;
    }
}
public class HomeTheaterTestDrive {
	public static void main(String[] args) {
		//사용되어질 서브 클래스
		Amplifier amp = new Amplifier("Amplifier");
		Tuner tuner = new Tuner("AM/FM Tuner", amp);
		StreamingPlayer player = new StreamingPlayer("Streaming Player", amp);
		CdPlayer cd = new CdPlayer("CD Player", amp);
		Projector projector = new Projector("Projector", player);
		TheaterLights lights = new TheaterLights("Theater Ceiling Lights");
		Screen screen = new Screen("Theater Screen");
		PopcornPopper popper = new PopcornPopper("Popcorn Popper");
 
	//서브 클래스를 사용하여 퍼사드로 단순화한 인스턴스 생성.
		HomeTheaterFacade homeTheater =
				new HomeTheaterFacade(amp, tuner, player,
						projector, screen, lights, popper);
 
	//단순화한 인터페이스를 사용하여 기능 수행
		homeTheater.watchMovie("Raiders of the Lost Ark");
		homeTheater.endMovie();
	}
}

수행결과

최소 지식 원칙

  • 퍼사드 패턴을 사용하면서 주의해야할 점이자 배우게 될 객체지향 사고는 최소 지식 원칙입니다.
  • 최소지식 원칙은 객체 사이의 상호작용은 긴밀한 사이의 객체 사에서만 관계를 가지는것입니다.
    • 개발할 때 객체는 다른 객체와 상호작용을 하게 되는데 이때 상호작용 하는 방식과 개수들에 주의하며 타당성이 있는지를 검토해야 한다는 의미입니다.
  • 최소 지식 원칙을 따르게 되면 여러 클래스가 복잡하게 얽혀 있어서 변경이 발생할 경우 줄줄이 바꿔야하는 상황을 방지할 수 있습니다.

최소 지식 원칙을 지키기 위한 가이드라인

  1. 객체 자체
  1. 메소드에 매개변수로 전달된 객체
  1. 메소드를 생성하거나 인스턴스를 만든 객체
  1. 객체에 속하는 구성 요소
  • 최소 지식 원칙을 지키기 위해 위와 같이 4개의 가이드라인을 제공하고 있습니다.
  • 1,2,3번 규칙을 따르게 되면 다른 메소드를 호출해서 리턴받은 객체의 메소드를 호출하는 일도 바람직하지 않습니다.
  • 4번 규칙의 ‘구성 요소’는 인스턴스 변수에 의해 참조되는 객체를 의미합니다. 즉 ‘A’에는 ‘B’가 있다라는 관계에 있는 객체를 생각하면 됩니다.
  • 위와 같은 상황에서 최소 지식 원칙을 따르기 위해서는 객체가 대신 요청하도록 만들어야 합니다.
//안 좋은 예시
public float getTemp(){
	Theromometer thermometer = station.getThermometer();
	return thermometer.getTemperature();
}
//좋은 예시
public float getTemp(){
	return station.getTemperature();
}
  • 예시로서 원칙을 따른 좋은 예시와 안 좋은 예시의 코드를 위에 첨부했습니다.
  • 안 좋은 경우는 station 객체를 통해 thermometer 객체를 받은 다음 getTempreature() 메소드를 호출합니다. 이럴 경우 1,2,3번 조건을 위반하게 됩니다.
  • 반면에 원칙을 따르는 좋은 예시는 최소 원칙을 적용해서 thermometer에게 요청을 전달하는 메소드를 station 클래스에 추가하였습니다. 이를 통해 의존하는 클래스를 줄일 수 잇습니다.

최소 지식 원칙을 잘 따르는 예시

public class Car{
	Engine engine // 해당 클래스의 구성요소로 engine의 어떠한 메소드를 호출해도 상관없다.
	
	public Car(){
	//엔진 초기화 작업
	}

	public void start(Key key){ // 매개 변수로 전달된 객체의 메소드는 호출해도 좋다.
		Doors doors = new Doors(); // 새로운 객체로 생성한 메소드는 호출해도 좋다.
		Boolean authorized = key.turns(); // 매개 변수로 전달된 객체의 메소드는 호출해도 좋다.
		if (authorized) {	
		engine.start(); // 객체 내에 있는 메소드는 호출해도 좋다
		updateDashboardDispaly();  // 객체 내에 있는 메소드는 호출해도 좋다
		doors.lock() // 직접 생성하거나 만든 인스턴 객체의 메소드는 호출해도 좋다.
		}
	}

	public void updateDashboardDisplay(){
		//디스프렐이 갱신
	}
}
💡
최소 지식 원칙의 단점은 없나요?
  • 메소드 호출을 처리하는 Wrapper 클래스를 더 만들어야 할 수 있는 단점이 존재합니다. 이로 인해 시스템이 복잡해지고 개발 시간이 늘어나며 성능도 떨어질 수 있는 위험이 존재합니다.
  • 개인적인 의견으로 최소 지식 원칙을 지키기 위한 가장 중요한 문제는 시간이라고 생각합니다. 그렇기 때문에 “최소 지식 원칙을 지킬 경우 의존성을 줄일 수 있고 SW 관리가 편해지는 장점이 존재하기에 자신의 개발 시간과 여러 상황을 고려해서 해당 규칙을 수행하면 좋지 않을까?”라는게 저의 의견입니다.

정리

  • 어댑터 패턴은 특정 클래스 인터페이스를 클라이언트에서 요구하는 다른 인터페이스로 변환하는것입니다. 이를 통해 인터페스가 호환되지 않아 같이 사용할 수 없던 클래스를 사용할 수 있습니다.
  • 퍼사드 패턴은 서브시스템에 있는 일련의 인터페이스를 통합 인터페이스로 묶어 사용하는것입니다. 이를 통해 고수준 인터페이스를 정의하여 객체를 편리하게 사용할 수 있습니다.
  • 퍼사드 패턴은 클라이언트를 복잡한 서브시스템과 분리하는 역할을 합니다.
  • 어뎁터 패턴의 구현 양은 Target 인터페이스의 크기와 구조에 따라 구현양이 결정됩니다.
  • 퍼사드 패턴은 퍼사드를 만들고 진짜 작업은 서브클래스에서 수행하는것입니다.
  • 한 개의 서브시스템에 여러 퍼사드를 만들어도 상관없습니다.
  • 아래 3개의 디자인 패턴은 유사하나 차이점이 존재하며 목적에 맞게 사용해야 합니다.
    • 어댑터 패턴은 객체를 감싸서 인터페이스를 바꾸는 용도로 사용합니다.
    • 데코레이터는 패턴은 객체를 감싸서 새로운 행동을 추가하는 용도로 사용합니다.
    • 파사드 패턴은 일련의 객체를 감싸 단순하게 만드는 용도로 사용합니다.