본문 바로가기

ACC/디자인 패턴 스터디

[디자인 패턴 스터디] 7. 어댑터 패턴과 퍼사드 패턴

어댑터 패턴

  • 어댑터: 서로 다른 단자를 연결시켜주는 것.
  • 어댑터 패턴: 특정 인터페이스 혹은 클래스를 타깃 인터페이스로 바꿔주는 것.

어댑터 패턴은 클라이언트, 어댑터와 어댑티, 타깃 인터페이스를 갖는다. 뭐가 어떤 역할인지는 예시로 보자.

 


어댑터 패턴 예시

1장에서 배운 오리 인터페이스에 칠면조 인터페이스를 적용시켜주고 싶은 경우.

 

///////////////////////////////////////// 오리 interface

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

////////////////////////////////////////// 칠면조 interface

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

class WildTurkey implements Turkey {
    public void gobble() {
        System.out.println("골골");
    }
    public void fly() {
        System.out.println("짧게 난다요");
    }
}

////////////////////////////////////////// 어댑터 패턴 클래스

class TurkeyAdapter implements Duck {
    Turkey turkey;

    public TurkeyAdapter(Turkey turkey) {
        this.turkey = turkey;
    }

    public void quack() {
        // 꽥을 호출하면 gobble을 그대로 실행
        turkey.gobble();
    }

    public void fly() {
        // 짧게 나는 대신 2번 날자
        for (int i = 0; i < 2; i++) turkey.fly();
    }
}

//////////////////////////////////////////

public class Main {
    public static void main(String args[]) {
        Turkey wildTurkey = new WildTurkey();
        Duck TurkeyToDuckAdapter = new TurkeyAdapter(wildTurkey);
        TurkeyToDuckAdapter.quack();
        TurkeyToDuckAdapter.fly();
    }
}

 

위와 같이 칠면조지만 위장으로 만든 Duck 인터페이스 타입으로 만들 수 있다.

지금은 쓸데 없이 칠면조를 오리로 바꾼 것처럼 보이겠지만, 후에 어떤 Duck 타입의 객체가 필요할 때 turkey를 넣어야 하는 상황이 오면 유용해질 것이다. 그런 유용한 예제는 아래에 나온다.

 

위의 상황에서 Duck이든 Turkey든 필요한 사람이 Client, Duck이 Target Interface, Turkey가 Adaptee, TurkeyAdapter가 Adapter가 된다.

 


Adaper Pattern의 이점

  • 어댑터가 어댑티를 감쌀 때, 객체 지향 원칙인 이용한 구성(Composition)을 사용했으므로 어댑티의 서브 클래스도 이용할 수 있다.
    만약 상속을 통해 코드를 작성했다면 이런 효과는 없었을 것.
  • 또한 Adatper는 구현이 아닌 Target Interface에 연결했고, 바뀌는 부분(Adapter)를 캡슐화하였으므로 Target Interface와 Adaptee를 분리하는 효과를 기대할 수 있다. 바뀌는 부분이 존재한다면 Adpater class만 수정해주면 된다.
    만약 구현을 통해 코드를 작성했다면 이런 효과는 없었을 것.

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

위에서 사용한 방법은 객체 어댑터였다. 이름에서 알 수 있듯이 Adapter가 Adaptee의 객체를 갖고 있는 형태

클래스 어댑터는 Target Interface와 Adaptee를 동시에 상속받아야 한다. Java에서는 다중 상속이 불가능하므로 사용할 수는 없다.

 

클래스 어댑터의 장점

  • 어댑터 클래스에서 어댑티 전체를 다시 구현할 필요가 없다. (객체 어댑터의 경우, 어댑티 인터페이스, 어댑티 클래스, 어댑터를 구현하지만 클래스 어댑터의 경우 어댑티 인터페이스, 어댑터만 구현하면 됨.)
  • 어댑티의 메서드를 사용하려면 오버라이드를 사용하여서 속도가 빠르다.

클래스 어댑터의 단점

  • 특정 어댑티 클래스(상속하는 클래스)에만 적용할 수 있다.
    객체 어댑터에서는 서브 클래스 객체를 갖고 있으면 사용 가능.
  • 유연성이 떨어진다. (느슨한 결합이 아니기 때문)

실전 적용 - Enumeration과 Iterator

지금의 Iterator는 이전에 Enumeration이라는 것이었다.

Iterator는 hasNext(), next(), remove()와 같은 메서드를 갖고, Enumeration은 hasMoreElements()와 nextElement()라는 메서드를 갖는다.

어댑터 패턴을 활용하여 Enumeration의 hasMoreElements()와 nextElement()를 각각 hasNext(), next()에 대응시킨다. remove()의 기능을 하는 메서드는 Enumeration에는 존재하지 않으므로 UnsupportedOperationException()을 던져주면 된다.

위의 실전 적용을 보면 알겠지만, 어댑터 패턴을 사용하기 위해서는 각 메서드를 1:1로 대응시켜야 하고, 만약 그럴 수 없는 경우에는 런타임 에러를 발생시키는 등의 방법을 사용해야 한다.

또한 해당 예제처럼 Enumeration만 지원하는 구식 형태에 맞추기 위해 사용하는 것이 어댑터 패턴이라는 것을 알 수 있다.

 

import java.util.Enumeration;
import java.util.Iterator;

class EnumerationIterator implements Iterator<Object> {
	// 어댑티
    Enumeration<?> enumeration;

    public EnumerationIterator(Enumeration<?> enumeration) {
        this.enumeration = enumeration;
    }

	// 어댑터 적용
    public boolean hasNext() {
        return enumeration.hasMoreElements();
    }

    public Object next() {
        return enumeration.nextElement();
    }

    public void remove() {
    	// 런타임 에러 발생
        throw new UnsupportedOperationException();
    }
}

 


퍼사드 패턴

복잡한 서브 시스템을 간소화해주는 클래스를 만들어주는 패턴이다.

 


퍼사드 패턴 예시

만약 영화를 본다고 생각해보자. 스크린을 내리고, 영사기를 켜고, 오디오를 켜고, 셋톱박스 혹은 넷*릭스를 켜고 등등의 일을 하나씩 하는 방법도 물론 있겠지만 버튼 하나로 위의 복잡한 일들을 한 번에 처리할 수도 있다. 마치 커맨드 패턴의 매크로처럼.

위 같은 방법을 위해 스크린, 영사기, 오디오 등의 클래스들을 감싼 새로운 클래스(캡슐화)를 만들어서 movieOn()과 같은 메서드를 선언해준다면 퍼사드 패턴을 구현할 수 있다. 클래스가 캡슐화되어 클라이언트와 서브 시스템을 분리시키는 효과도 있다.

물론, 퍼사드를 이용하지 않고 서브시스템에 직접 접근하여 더 세밀한 작업을 진행할 수도 있다.

 

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.tunner = tuner;
        this.dvd = dvd;
        this.cd = cd;
        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.setDvd(dvd);
        amp.setsurroundSound();
        amp.setVolume(5);
        dvd.on();
        dvd.play(movie);
    }

    public void endMovie() {
        System.out.println("Shutting movie theater down...");
        popper.off();
        lights.on();
        screen.up();
        projector.off();
        amp.off();
        dvd.stop();
        dvd.eject();
        dvd.off();
    }
}

 

위 코드처럼 영화를 켜고 끄는 작업을 간소화할 수도 있고, 직접 서브 시스템을 호출해도 된다.

 


타 패턴과의 비교

데코레이터, 어댑터, 퍼사드

  • 데코레이터 패턴: 확장될 수 있는 기능을 캡슐화하여 동적으로 인터페이스의 기능을 확장
  • 어댑터 패턴: 어떤 인터페이스를 다른 인터페이스로 변환. 어댑터를 캡슐화하여 서로 다른 두 인터페이스는 분리되어 있다.
  • 퍼사드 패턴: 복잡한 서브 시스템을 간소화해주는 클래스를 만드는 것이 목적. 클래스가 캡슐화되어 클라이언트와 서브 시스템을 분리시키는 효과.

퍼사드, 인보커

  • 커맨드 패턴: 행위를 캡슐화한 커맨드를 인보커가 갖다 쓰는 것. 행위의 캡슐화가 핵심
  • 퍼사드 패턴: 복잡한 서브 시스템을 간소화해주는 클래스를 만드는 것이 목적

최소 지식 원칙

객체 사이의 상호작용을 최소화하는 것. 상호작용하는 클래스의 개수와 상호작용 방식을 주의해야 한다.

 

한 객체가 다른 객체와 상호작용을 최소화하는 방법은 아래와 같다.

  • 자기 자신
  • 매개변수로 전달된 객체
  • 인스턴스를 만든 객체 (new 키워드로 할당된 객체)
  • 객체의 멤버변수 객체

위의 네 개의 객체들은 이용(메서드 호출 등)해도 상호작용을 최소화할 수 있다. 

 

예시는 아래와 같다.

 

class Car {
    // 멤버 변수로 선언된 객체. 사용해도 좋다.
    Engine engine;
    bool authorized;

    public Car(Engine engine, bool authorized) {
        this.engine = engine;
        this.authorized = authorized;
    }

    // key는 매개변수이므로 사용해도 좋다.
    public void start(Key key) {
        // 인스턴스를 만든 객체 (new). 사용해도 좋다.
        Doors doors = new Doors();
        // 메소드의 호출 결과를 할당한 객체. 사용하면 좋지 않다. 단 trunk 변수를 다른 함수의 매개변수로 넘기는 건 괜찮다.
        Doors trunk = doors.getTrunkState();

        if (authorized) {
            // 멤버 변수의 메서드. 사용해도 좋다.
            engine.start();
            // 사실 this.updateDashboardDisplay();이다. 자기 자신의 메서드이므로 사용해도 좋다.
            updateDashboardDisplay();
            // 인스턴스를 만든 객체의 메서드. 사용해도 좋다.
            doors.lock();
            // 메서드의 메서드를 호출한 것과 같다. 사용하면 좋지 않다.
            trunk.lock();
            // 이렇게 사용하는 건 괜찮다.
            doors.open(trunk);
        }
    }

    public void updateDashboardDisplay() {
        // TODO
    }
}

 


노트

어댑터 패턴

여러 개의 인터페이스를 감싼 어댑터를 만들어도 된다.

퍼사드 패턴

필요하다면 하나의 서브시스템에 여러 개의 퍼사드를 만들어도 된다.