[헤드퍼스트 디자인패턴] Chapter 07. 어댑터 패턴과 퍼사드 패턴

뚱이·2023년 6월 20일
0
post-thumbnail

1. 어댑터 패턴

어댑터 패턴 (Adapter Pattern)

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

(1) 이름에서 알 수 있듯이 멀티 어댑터와 비슷하다

여행 갈 때 필수품 중 하나인 멀티 어댑터.

우리는 220V를 사용하는데, 110V를 사용하는 나라에 가면 변압기를 사용해줘야 한다.
안 그러면 사용할 수 없으니까 !

이처럼, 어댑터 패턴은 어떤 인터페이스를 클라이언트에서 요구하는 형태로 적응시키는 역할을 한다.
다음과 같이 말이다.

새로운 업체에서 제공한 클래스 라이브러리를 사용하는데 필요한 인터페이스기존에 사용하던 인터페이스 와 다를 때, 어댑터를 사용하면 코드 변화 없이 그대로 사용할 수 있다.


(2) 어댑터 패턴이란?

어댑터 페턴을 사용하면 호환되지 않는 인터페이스를 사용하는 클라이언트 를 그대로 활용할 수 있다 !

어댑터가 인터페이스를 변환해 주는 역할을 하기 때문이다.

어댑터를 사용하면 일단 다음과 같은 장점이 있다.
1. 클라이언트와 구현된 인터페이스를 분리할 수 있다.
2. 변경 내역이 어댑터에 캡슐화되기 때문에 나중에 인터페이스가 바뀌더라도 클라이언트를 바꿀 필요가 없다.

클래스 다이어그램

클래스 다이어그램에서도 알 수 있듯이, 어댑터 패턴은 구성(composition)을 사용한다.
정확히는, 어댑티새로 바뀐 인터페이스 로 감쌀 때 객체 구성을 사용한다.

이렇게 구성을 사용하면, 어댑티의 모든 서브클래스에 어댑터를 쓸 수 있다는 장점이 있다.

그리고 이 패턴은 클라이언트를 특정 구현이 아닌 인터페이스 에 연결하기 때문에,
서로 다른 백엔드 클래스로 변환시키는 여러 어댑터를 사용할 수도 있다.

이렇듯 인터페이스가 기준이 되기 때문에, 타깃 인터페이스만 제대로 유지하면 나중에 다른 구현을 추가하는 것도 가능하다.


(3) 어댑터 패턴 작동 원리

어댑터 패턴은 위 그림과 같이 작동한다.

클라이언트, 어댑터, 어댑티 - 총 3가지 역할이 필요한데, 각 역할은 다음과 같다.

  • 클라이언트
    말그대로 클라이언트로, 타깃 인터페이스에 맞게 구현되어 있다.

  • 어댑터
    클라이언트와 어댑티를 연결해주는 중개 역할을 한다.
    클라이언트가 사용하는 타킷 인터페이스를 구현하며, 어댑티 인스턴스가 포함되어 있다.

  • 어댑티
    어댑터를 통해 변환되어 사용할 대상이다.


클라이언트에서 어댑터를 사용하는 방법

  1. 클라이언트에서 타깃 인터페이스로 메소드를 호출한다.
    즉, 어댑터에 요청을 보낸다.

  2. 어댑터는 어댑터 인터페이스를 통해 해당 요청을 어댑티에 관한 (하나 이상의) 메소드 호출로 변환한다.

  3. 클라이언트가 호출 결과를 받는다.
    이 때 클라이언트는 중간에 어댑터가 존재한다는 사실을 모르고, 어댑티와도 분리되어 있어 서로를 전혀 알지 못 한다.


(4) 어댑터 사용 방법 예시

1장에서 나왔던 오리가 기억나나요?

그 때 장난감 오리, 오리 인형을 비롯해 각종 오리가 나왔었다.
그리고 이 오리들은 나는 방법, 울음 소리 등이 각기 달라 이에 대한 패턴을 배웠었다.

근데 이 때, 만약 오리인 척 하는 칠면조가 있다면?
은근슬쩍 오리인 마냥 있었다면?

칠면조가 오리인 척 하고 있기 때문에 클라이언트는 칠면조의 존재를 전혀 모른다 !
😯
그럼 클라이언트는 평소처럼 오리를 다루는 방식대로 진행할텐데,
칠면조는 실제로 오리가 아니기 때문에 그냥은 사용할 수 없을 것이다.

이 때 어댑터 패턴을 적용하면 이 친구도 동작 가능하게 할 수 있다 !

[ Duck (인터페이스) ]

public interface Duck {
	public void quack();
    public void fly();
}

[ MallardDuck ]

public class MallardDuck implements Duck {
	public void quack() {
    	System.out.println("꽥");
    }
    public void fly() {
    	System.out.println("오리 날다");
    }
}

[ Turkey (인터페이스) ]

public interface Turkey {
	public void gobble();
    public void fly();
}

[ WildTurkey ]

public class WildTurkey implements Turkey {
	public void gobble() {
    	System.out.println("골골);
    }
    public void fly() {
    	System.out.println("칠면조 날다");
    }
}

[ TurkeyAdapter ]

// 자, 클라이언트는 오리에 맞는 인터페이스를 사용할 것이다.
// 그렇기 때문에 우리는 오리에 맞는 인터페이스를 만들어야 한다.
public class TurkeyAdapter implements Duck {
	Turkey turkey;
    
    // 기존 형식 객체의 레퍼런스가 필요하다.
    // -> 생성자에서 레퍼런스를 받아오는 작업을 처리한다.
    public TurkeyAdapter(Turkey turkey) {
    	this.turkey = turkey;
    }
    
    // 상황에 맞게 내부 코드를 짜면 된다.
    public void quack() {
    	turkey.gobble();
    }
    
    public void fly() {
    	turkey.fly();
    }
}

[ Test ]

public class DuckTestDrive {
	public static void main(String[] args) }
    	Duck duck = new MallardDuck();
        Turkey turkey = new WildTurkey();
        
        Duck turkeyAdapter = new TurkeyAdapter(turkey);
        
        System.out.println("칠면조가 말하길");
        turkey.gobble();
        turkey.fly();
        
        System.out.println("오리가 말하길");
        testDuck(duck);
        
        System.out.println("칠면조 어댑터가 말하길");
        testDuck(turkeyAdapter);
    }
    
    static void testDuck(Duck duck) {
    	duck.quack();
        duck.fly();
    }
}

(5) 어댑터의 종류

사실 어댑터에는 두 종류가 있다.
하나는 객체 어댑터 이고, 다른 하나는 클래스 어댑터 이다.

우리가 앞에서 계속 본 어댑터는 객체 어댑터 이다.

클래스 어댑터 의 클래스 다이어그램은 다음과 같다.

구성(composition)으로 어댑티에 요청을 전달하는 객체 어댑터 와 달리,
클래스 어댑터 는 타깃과 어댑티 모두 서브클래스로 만들어서 사용한다.
다시 말해, 어댑터를 어댑티와 타깃 클래스의 서브클래스로 만든다.

그러나, 클래스 어댑터 는 우리가 배우지 않는다 .. 😥
why ?

클래스 어댑터 패턴 을 사용하려면 다중 상속이 필요하다.
그러나 Java에서는 다중 상속이 불가능하기 때문에, Java를 사용하는 우리들은 이 패턴을 사용하고 싶어도 사용할 수 없다.
클래스 어댑터 패턴 을 사용하고 싶으면, 다중 상속이 가능한 언어를 찾아보자.



2. 퍼사드 패턴

퍼사드 패턴 (Facade Pattern)

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

(1) facade = 겉모양, 외관

facade 단어 뜻에서 알 수 있듯이, 퍼사드 패턴은 단순하게 인터페이스의 겉모양을 변경하는 패턴이다.

아니, 멀쩡한 걸 왜 변경해?
바꿔야 할 필요가 있어?

이렇게 생각할 수도 있는데, 이 패턴의 핵심은 인터페이스를 단순하게 바꾸는 것이다.


(2) 퍼사드 패턴의 예시 - 홈시어터

여기 하나의 홈시어터가 있다.
이 홈시어터에는 스트리밍 플레이어, 프로젝터, 자동 스크린, 서라운드 음향, 팝콘 기계 의 기능이 있는, 아주 집순이를 위한 최적의 장치이다.

기능이 많은 만큼 클래스 다이어그램도 복잡하다 !
클래스들이 정말 정말 많이 필요하다.

영화를 한 번 보려면

영화를 보려면 정 ~ 말 수고로운 사전 작업을 거쳐야 한다.

  1. 팝콘 기계를 켜고,
  2. 팝콘을 튀긴다.
  3. 조명을 어둡게 조절한다.
  4. 스크린을 내린다.
  5. 프로젝터를 켜고,
  6. 프로젝터 입력을 스트리밍 플레이어로 설정한 후,
  7. 프로젝터를 와이드 스크린 모드로 전환한다.
  8. 앰프를 켜고,
  9. 앰프 입력을 스트리밍 플레이어로 설정한 후,
  10. 앰프를 서라운드 음향 모드로 전환하고,
  11. 앰프 볼륨을 5로 설정한다.
  12. 스트리밍 플레이어를 켠다.
  13. 영화를 재생한다.

어우 귀찮아.
영화 한 번 보려면 정말 여러 작업을 해야한다.

이 작업들을 처리하는 데에 또 많은 클래스와 메소드가 필요하다.

근데 영화를 보기만 하면 끝인가?
영화를 다 보면 꺼야 되고,
다른 기능들도 사용할 때고 있을 것이고,
시스템이 업그레이드 돼서 다른 기능이 추가되면?

정말 정말 정말 복잡하다.
그래서 우리는 인터페이스를 단순화 할 필요가 있다.

퍼사드 작동 원리

업로드중..

먼저, 홈시어터 시스템용 퍼사드, HomeTheaterFacade 클래스를 만든다.
이 클래스에는 watchMovie() 와 같이 몇 가지 간단한 메소드만 들어 있다.

이 때,
퍼사드 클래스는
홈시어터 구성 요소를 하나의 서브시스템으로 간주하고,
watchMovie() 메소드는 서브시스템의 메소드를 호출해서 필요한 작업을 처리한다.

그럼 클라이언트는 퍼사드에 있는 watchMovie() 메소드 하나만 호출하면 영화를 볼 수 있다 !
작업이 훨씬 간결해졌다.


(3) 퍼사드 패턴이란?

퍼사드 패턴은 복잡한 추상화 같은 게 필요 없기 때문에 상당히 단순한 편이다.

그렇다고 퍼사드 패턴이 별 거 아닌 패턴은 아니다.

퍼사드 패턴을 사용하면,

  • 클라이언트와 서브시트템이 서로 긴밀하게 연결되어 있지 않아도 되고,

  • 최소 지식 원칙을 준수하는 데도 도움이 된다.

클래스 다이어그램

업로드중..

퍼사드 패턴의 클래스 다이어그램은 이와 같은데,
이처럼 단순화된 인터페이스로 서브시스템을 더 편리하게 사용할 수 있다.


(4) 퍼사드 패턴 QnA

❔ 퍼사드로 서브시스템 클래스를 캡슐화하면, 저수준 기능을 원하는 클라이언트는 어떻게 서브시스템 클래스에 접근할 수 있지?

✔️ 여기서 중요한 점. 퍼사드 클래스는 서브시스템 클래스를 캡슐화하지 않는다.
서브시트메의 기능을 편하게 사용할 수 있는 간단한 인터페이스를 제공할 뿐, 클라이언트에서 특정 인터페이스가 필요하면 서브시스템 클래스를 그냥 사용하면 된다.

❔ 그럼, 어댑터는 한 클래스만 감싸고 퍼사드는 여러 클래스를 감쌀 수 있는 건가?

✔️ 결론만 말하자면 아니다. 퍼사드와 어댑터 모두 여러 개의 클래스를 감쌀 수 있다.

❔ 그럼 어댑터와 퍼사드의 차이점은?
✔️ 어댑터와 퍼사드는 그 용도가 다른 게 가장 큰 차이점이다.
어댑터는 인터페이스를 변경해서 클라이언트에서 필요로 하는 인터페이스로 적응시키는 용도로 사용한다. 즉, 인터페이스를 다른 인터페이스로 변환하는 용도로 쓰인다.
반면 퍼사드는 어떤 서브시스템에 대한 간단한 인터페이스를 제공하는 용도로 쓰인다. 다시 말해, 퍼사드는 인터페이스를 단순하게 만드는 용도로 사용한다.

❔ 퍼사드의 장점을 정리하면?
✔️ 퍼사드의 장점을 정리하면 다음과 같다.

  • 단순화된 인터페이스를 제공하면서도 클라이언트는 시스템의 모든 기능을 사용할 수 있다.
  • 클라이언트 구현과 구성 요소로 이루어진 서브시스템을 분리할 수 있다.

(5) 퍼사드 패턴 예시 코드

아까 봤던 홈시어터를 코드로 구현해보자.

[HomeTheaterFacade]

public class HomeTheaterFacade {
	// 구성 부분
    // 사용하고자 하는 서브시스템의 모든 구성 요소가 인스턴스 변수 형태로 저장됨
    Amplifier amp;
    Tuner tuner;
    StreamingPlayer player;
    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("영화 볼 준비하는 중");
    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 class HomeTheaterTestDrive {
	public static void main(String[] args) {
    	// 구성 요소 초기화
        // 테스트 중이므로구성 요소 직접 생성
        // 보통은 클라이언트에 퍼사드가 주어지므로 직접 생성 안 해도 됨
        
        HomeTheaterFacade homeTheater = new HomeTheaterFacade(
        	amp, tuner, player, projector, screen, lights, popper);
            
      	homeTheater.watchMovie("가디언즈 오브 갤럭시: Volume 3");
        homeTheater.endMovie();
    }
    
}

(6) 최소 지식 원칙 (Principle of Least Knowledge)

디자인 원칙
진짜 절친에게만 이야기해야 한다.

여기서 새로운 디자인 원칙이 등장한다.
데메테르의 법칙이라고도 불리는 최소 지식 원칙객체 사이의 상호작용은 될 수 있으면 아주 가까운 '친구' 사이에서만 허용하는 편이 좋다 고 말한다.

이게 무슨 말이냐 ?

이 말은 시스템을 디자인할 때 어떤 객체든 그 객체와 상호작용을 하는 클래스의 개수와 상호작용 방식에 주의를 기울여야 한다 는 뜻이다.

이 원칙을 잘 따르면,
여러 클래스가 복잡하게 얽혀 있어, 시스템의 한 부분을 변경했을 때 다른 부분까지 줄줄이 고쳐야 하는 상황을 방지할 수 있다는 장점이 있다.

친구를 만들지 않는 4개의 가이드라인

그렇다면 어떻게 여러 객체와 친구가 되는 것을 피할 수 있을까.
최소 지식 원칙은 친구를 만들지 않는 4개의 가이드라인을 제공한다.

  • 객체 자체

  • 메소드에 매개변수로 전달된 객체

  • 메소드를 생성하거나 인스턴스를 만든 객체

  • 객체에 속하는 구성 요소
    '구성 요소'는 인스턴스 변수에 의해 참조되는 객체를 의미한다.
    즉, "A에는 B가 있다" 라는 관계에 있는 객체를 생각하면 된다.


(7) 퍼사드 패턴과 최소 지식 원칙

업로드중..

퍼사드 패턴최소 지식 원칙을 적용하면 이렇게 된다.

클라이언트의 친구가 HomeTheaterFacade 하나뿐이다.
이처럼 객체지향 프로그래밍에서는 친구가 하나만 있는 게 좋다.



3. 정리

객체지향 기초

  • 추상화
  • 캡슐화
  • 다형성
  • 상속

객체지향 원칙

  • 바뀌는 부분은 캡슐화 한다.
  • 상속보다는 구성을 활용한다.
  • 구현보다는 인터페이스에 맞춰서 프로그램이한다.
  • 상호작용하는 객체 사이에서는 가능하면 느슨한 결합을 사용해야 한다.
  • OCP: 클래스는 확장에는 열려 있어야 하지만 변경에는 닫혀 있어야 한다.
  • 추상화된 것에 의존하게 만들고 구상 클래스에 의존하지 않게 만든다.
  • 진짜 절친에게만 이야기해야 한다.

객체지향 패턴

전략 패턴

전략 패턴은 알고리즘군을 정의하고 캡슐화해서 각각의 알고리즘군을 수정해서 쓸 수 있게 한다.
전략패턴을 사용하면 클라이언트로부터 알고리즘을 분리해서 독립적으로 변경할 수 있다.

옵저버 패턴

한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체에게 연락이 가고 자동으로 내용이 갱신되는 방식으로, 일대다 (one-to-many) 의존성을 정의한다.

데코레이터 패턴

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

팩토리 메소드 패턴

객체에서 생성할 때 필요한 인터페이스를 만든다.
어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정한다.
이 패턴을 사용하면 클래스 인스턴스 만드는 일을 서브클래스에게 맡긴다.

추상 팩토리 패턴

구상 클래스에 의존하지 않고도 서로 연관되거나 의존적인 객체로 이루어진 제품군을 생산하는 인터페이스를 제공한다.
구상 클래스는 서브클래스에서 만든다.

싱글턴 패턴

클래스 인스턴스를 하나만 만들고, 그 인스턴스로의 전역 접근을 제공한다.

커맨드 패턴

요청 내역을 객체로 캡슐화해서 객체를 서로 다른 요청 내역에 따라 매개변수화할 수 있다. 이러면 요청을 큐에 저장하거나 로그로 기록하거나 작업 취소 기능을 사용할 수 있다.

어댑터 패턴 (Adapter Pattern)

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

퍼사드 패턴 (Facade Pattern)

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

0개의 댓글