지난번 어댑터 패턴에 대해서 알아보았다. 어댑터 패턴과 유사하지만 다른 역할을 하는 패턴에 대해 정리해보고자 한다.

구조패턴 이란 것은 구조를 효율적으로 유지하면서 객체들과 클래스 들을 더 큰 구조로 조립하는 방법을 의미한다.

퍼사드 패턴 또한 이런 분류로 속하게 되는 이유에 대해 생각해 보면서 글을 작성해보고자 한다.

 

퍼사드 패턴 이란 ?

퍼사드는 클래스 라이브러리 같은 어떤 소프트웨어의 다른 커다란 코드 부부에 대해 간략화된 인터페이스를 제공하는 객체이다.
- 퍼사드는 소프트웨어 라이브러리를 쉽게 사용할 수 있게 해 준다. 퍼사드는 공통적인 작업에 대해 간편한 메서드들을 제공해 준다.
래퍼가 특정 인터페이스를 준수해야 하며, 폴리모픽 기능을 지원해야 할 경우에는 어댑터 패턴을 쓴다. 단지 쉽고 단순 한 인터페이스를 이용하고 싶을 경우에는 퍼사드를 쓴다. - wiki 퍼사드패턴 - 

퍼사드 패턴은 라이브러리에 대한, 프레임워크에 대한 또는 다른 클래스들의 복잡한 집합에 대한 단순화된 인터페이스를 제공하는 구조적인 디자인 패턴입니다. - guru -

퍼사드패턴 은 서브시스템에 있는 일련의 인터페이스를 통합 인터페이스로 묶어줍니다. 또한 고수준 인터페이스도 정의하므로 서브시스템을 더 편리하게 사용할 수 있습니다. - HeadFirst -

각 사이트 혹은 책에서 읽은 내용의 정의를 종합해 보자면 퍼사드 패턴은 무언가 묶어주 는 인터페이스 역할을 하게 되고 클라이언트는 이 인터페이스와 통신한다 정도로 이해된다. 이에 클라이언트는 오로지 퍼사드를 통해서 통신하게 되어 의존관계가 줄어든다.

 

왜 사용하는가?

 

1. 단순한 인터페이스 

- 클라이언트 와 복잡한 서브시스템 사이에 단순한 인터페이스를 제공한다. 이로 인해 클라이언트 코드는 간결해지고, 사용법을 쉽게 이해할 수 있다.

2. 복잡성 감소

- 복잡한 서브시스템을 단순한 인터페이스로 감싸기 때문에, 클라이언트는 서브시스템의 내부 동작에 대해 걱정할 필요가 없다. 

3. 결합도 감소 

- 서브시스템과 클라이언트 사이의 결합도를 감소시킨다. 서브시스템의 내부 변경이 발생해도 퍼사드의 인터페이스를 유지하기만 하면 클라이언트는 코드를 수정하지 않아도 된다.

4. 클라이언트 편의성

- 퍼사드 패턴은 서브시스템의 기능을 논리적으로 그룹화할 수 있으며, 이는 클라이언트가 필요로 하는 기능을 쉽게 찾을 수 있게 도와준다.

 

퍼사드 패턴 또한 SOLID 원칙 중 개방폐쇄 원칙을 잘 지키고 있다고 볼 수 있다. 왜? 새로운 기능을 추가하거나, 기존 기능을 변경할 때 클라이언트는 코드를 수정하지 않아도 된다. 

 

구조

말 그대로 다양한 구조체에 대해 통합해서 무언가를 한다는 느낌이 그림으로만 봐도 느껴진다. 

 

퍼사드 패턴의 적용

- 퍼사드 패턴은 복잡한 하위 시스템에 대한 제한적이지만 간단한 인터페이스가 필요할 때 사용한다.

- 퍼사드 패턴은 하위시스템을 계층들로 구성하려는 경우 사용한다.

 

package facade

import "fmt"

/**
다른 이유로 어댑터를 변경하는 방법
인터페이스를 단순하게 변가ㅕㅇ하기 위해 사용
"퍼사드 패턴"
*/

type Screen struct{}

func (s *Screen) up() {
	fmt.Println("[Screen] up")
}
func (s *Screen) down() {
	fmt.Println("[Screen] down")
}

type PopcornPopper struct{}

func (p *PopcornPopper) on() {
	fmt.Println("[PopcornPopper] on")
}
func (p *PopcornPopper) off() {
	fmt.Println("[PopcornPopper] off")
}
func (p *PopcornPopper) pop() {
	fmt.Println("[PopcornPopper] pop")
}

type TheaterLights struct{}

func (t *TheaterLights) on() {
	fmt.Println("[TheaterLights] on")
}
func (t *TheaterLights) off() {
	fmt.Println("[TheaterLights] off")
}
func (t *TheaterLights) dim(){
	fmt.Println("[TheaterLights] dim")
}

type StreamingPlayer struct{}

func (s *StreamingPlayer) on() {
	fmt.Println("[StreamingPlayer] on")
}
func (s *StreamingPlayer) off() {
	fmt.Printf("[StreamingPlayer] off\n")
}
func (s *StreamingPlayer) pause() {
	fmt.Printf("[StreamingPlayer] pause\n")
}
func (s *StreamingPlayer) play(target string) {
	fmt.Printf("[StreamingPlayer %s] play\n", target)
}
func (s *StreamingPlayer) setSurroundAudio() {
	fmt.Printf("[StreamingPlayer] setSurroundAudio\n")
}
func (s *StreamingPlayer) setTwoChannelAudio() {
	fmt.Printf("[StreamingPlayer] setTwoChannelAudio\n")
}
func (s *StreamingPlayer) stop() {
	fmt.Printf("[StreamingPlayer] stop\n")
}
func (s *StreamingPlayer) String() string {
	return "StreamingPlayer"
}

type Projector struct {
	player StreamingPlayer
}

func (p *Projector) on() {
	fmt.Printf("[%s Projector] on\n", p.player)
}
func (p *Projector) off() {
	fmt.Printf("[%s Projector] off\n", p.player)
}
func (p *Projector) tvMode() {
	fmt.Printf("[%s Projector] tvMode\n", p.player)
}
func (p *Projector) wideScreenMode() {
	fmt.Printf("[%s Projector] wideScreenMode\n", p.player)
}

type Tuner struct{}

func (t *Tuner) on() {
	fmt.Println("[Tuner] on")
}
func (t *Tuner) off() {
	fmt.Println("[Tuner] off")
}
func (t *Tuner) setAm() {
	fmt.Println("[Tuner] setAm")
}
func (t *Tuner) setFm() {
	fmt.Println("[Tuner] setFm")
}
func (t *Tuner) setFrequency() {
	fmt.Println("[Tuner] setFrequency")
}

type Amplifier struct {
	tuner *Tuner
	player *StreamingPlayer
}

func (a *Amplifier) on() {
	fmt.Printf("[%s][%s][Amplifier] on\n", a.tuner, a.player)
}
func (a *Amplifier) off() {
	fmt.Printf("[%s][%s][Amplifier] off\n", a.tuner, a.player)
}
func (a *Amplifier) setStreamingPlayer() {
	fmt.Printf("[%s][%s][Amplifier] setStreamingPlayer\n", a.tuner, a.player)
}
func (a *Amplifier)setSurroundSound() {
	fmt.Printf("[%s][%s][Amplifier] setSurroundSound\n", a.tuner, a.player)
}
func (a *Amplifier)setVolume() {
	fmt.Printf("[%s][%s][Amplifier] setVolume\n", a.tuner, a.player)
}
func (a *Amplifier) String() string {
	return fmt.Sprintf("[%s][%s][Amplifier]", a.tuner, a.player)
}

type HomeTheaterFacade struct {
	amp *Amplifier
	tuner *Tuner
	player *StreamingPlayer
	projector *Projector
	lights *TheaterLights
	screen *Screen
	popcornPopper *PopcornPopper
}
func (h *HomeTheaterFacade)watchMovie(movie string){
	fmt.Printf("Get ready to watch a movie...\n")
	h.popcornPopper.on()
	h.popcornPopper.pop()
	h.lights.dim()
	h.screen.down()
	h.projector.on()
	h.projector.wideScreenMode()
	h.amp.on()
	h.amp.setStreamingPlayer()
	h.amp.setSurroundSound()
	h.amp.setVolume()
	h.player.on()
	h.player.play(movie)	
}

영화 하나 보기 위해서 퍼사드가 없다면 클라이언트에서는 watchmovie를 전부 구현하고, 각 구조체를 들고 있어야 한다. 즉 모든 구조체들과 결합하게 된다는 의미이다. 이걸 중간 인터페이스로 추상화를 시킨다면 

클라이언트는 퍼사드와 만 통신을 하면 된다.

func TestFacade(t *testing.T) {
	homeTheater := NewHomeTheaterFacade(
		NewAmplifier(),
		NewTuner(),
		NewStreamingPlayer(),
		NewProjector(),
		NewTheaterLights(),
		NewScreen(),
		NewPopcornPopper(),
	)

	homeTheater.watchMovie("Inception")
}


Get ready to watch a movie...
[PopcornPopper] on
[PopcornPopper] pop
[TheaterLights] dim
[Screen] down
[{} Projector] on
[{} Projector] wideScreenMode
[Tuner][StreamingPlayer][Amplifier] on
[Tuner][StreamingPlayer][Amplifier] setStreamingPlayer
[Tuner][StreamingPlayer][Amplifier] setSurroundSound
[Tuner][StreamingPlayer][Amplifier] setVolume
[StreamingPlayer] on
[StreamingPlayer Inception] play
--- PASS: TestFacade (0.00s)
PASS

이런 식의 코드 구현이 된다. 막상 구현하다 보면 익숙한 느낌이 든다 왜? 

우리는 함수를 구현할 때 도 이와 유사하게 작성한다. 함수 하나당 한의 기능별로 구분을 하고 그 기능을 통합으로 호출하는 함수를 만드는 경우도 있다.

우리는 알게 모르게 디자인 패턴에서 사용되는 개념들을 사용하면서 코딩을 하고 있었다 왜? 그게 더 편하고 유지보수가 편하니깐 함수가 작은 단위로 나눠지면서 재사용성을 가져가고 유지보수하기 가 편해지기 때문에 이렇게 코딩해 왔다.

 

아 위에서 퍼사드패턴에 대해서 설명할 때 인터페이스를 계속 말했었는데 저기서 언급한 인터페이스는 프로그래밍 언어의 인터페이스가 아닌 객체의 추상화를 의미한다. 즉 상위개념으로 캡슐화한다?라고 이해하면 될 것이다.

인터페이스 라고 해서 꼭 인터페이스 와 구조체의 구조를 가져갈 필요는 없는 것이다.

 

어댑터 패턴, 퍼사드 패턴, 데코레이터 패턴 뭔가 다 비슷하게 감싸고 변환하는 등의 구조적인 변환이 있어 비슷한데 정리를 하고 가자.

 

데코레이터 패턴 => 객체에 추가 요소를 동적으로 더할 수 있다. 데코레이터를 사용하면 서브클래스를 만들 때 보다 훨씬 유연하게 기능을 확장할 수 있다.

 

어댑터 패턴 => 특정 클래스 인터페이스를 클라이언트에서 요구하는 다른 인터페이스로 변환한다. 인터페이스가 호환되지 않아 같이 쓸 수 없었던 클래스를 사용할 수 있게 도와준다.

 

퍼사드 패턴 => 서브시스템에 있는 일련의 인터페이스를 통합 인터페이스로 묶어 준다. 또한 고수준 인터페이스도 정의하므로 서브시스템을 더 편리하게 사용할 수 있다.

 

 

+ Recent posts