Featured image of post 7. 어댑터 패턴과 퍼사드 패턴

7. 어댑터 패턴과 퍼사드 패턴

헤드 퍼스트 디자인 패턴

특정 인터페이스가 필요한 디자인을 다른 인터페이스를 구현하는 클래스에서 필요할 때(호환되지 않는 인터페이스를 사용해야 할 때), 어댑터 패턴을 이용하면 실제와 다른 인터페이스를 가진 것 처럼 보이게 만들 수 있다.

여기에 더해 퍼사드 패턴을 이용하면 객체를 감싸서 인터페이스를 단순화할 수 있다.

어댑터 패턴

왜 어댑터 패턴일까?

US Standard Socket AC JAJU 여행용 멀티 어댑터(그레이)

어댑터 패턴에서 의미하는 어댑터(Adaptor)는 흔히 볼 수 있는 AC 전원 어댑터에 사용되는 의미와 같다.

AC 전원 어댑터는 다른 규격의 플러그를 필요로하는 소켓에 사용할 수 있게 해주는 역할을 하는데, 객체지향 어댑터는 클라이언트에서 사용해야하는 인터페이스를(플러그)를 사용할 수 있는 인터페이스(소켓)에 맞는 형태로 적응시키는 역할을 수행한다.

어댑터 패턴의 정의

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

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

  • 클라이언트와 구현된 인터페이스를 분리할 수 있다.
  • 변경 내역이 어댑터에 캡슐화되므로 나중에 인터페이스가 바뀌더라도 클라이언트를 바꿀 필요가 없다.
classDiagram
    direction LR
    class Client { }
    class Target {
        << interface >>
        request()*
    }
    class Adapter {
        request()
    }
    class Adaptee {
        specificRequest()
    }
    Client-->Target
    Target<..Adapter
    Adapter-->Adaptee
  • 클라이언트는 타깃 인터페이스만 볼 수 있다.
  • 어댑터에서 타깃 인터페이스를 구현한다.
  • 어댑터는 어댑티로 구성되어있다.
  • 모든 요청은 어댑티에 위임된다.

어댑터는 여러 객체지향 원칙을 반영하고 있다.

  • 어댑티를 새로 바뀐 인터페이스로 감쌀 때는 객체 구성(composition)을 사용한다.
    • 이러한 접근으로 어댑티의 모든 서브클래스에 어댑터를 쓸 수 있다.
  • 클라이언트를 특정 구현이 아닌 인터페이스에 연결한다.
    • 서로 다른 백엔드 클래스로 변환시키는 여러 어댑터를 사용할 수도 있다.
    • 인터페이스를 기준으로 구현하므로 타깃 인터페이스만 제대로 유지한다면 나중에 다른 구현을 추가하는 것도 가능하다.

클래스 어댑터

어댑터 패턴은 객체 어댑터와 클래스 어댑터로 두 종류가 있다.

  • 클래스 어댑터는 다중 상속이 가능해야 구현할 수 있다.

클래스 어댑터는 타깃과 어댑티 모두 서브클래스로 만들어서 사용하고, 객체 어댑터는 구성으로 어댑티에 요청을 전달한다.

classDiagram
    direction LR
    class Client { }
    class Target {
        request()
    }
    class Adapter {
        request()
    }
    class Adaptee {
        specificRequest()
    }
    Client-->Target
    Adapter--|>Target
    Adapter--|>Adaptee

객체 어댑터

  • 구성을 사용하므로 어댑티 클래스와 그 서브클래스에 대해서도 어댑터 역할을 할 수 있다.
  • 구성을 사용하므로 어댑티한테 필요한 일을 시키는 코드만 만들면 되므로 코드를 많이 쓸 필요가 없다.
  • 구성을 사용하므로 유연성을 최대한 확보할 수 있다.
  • 어댑터 코드에 어떤 행동을 추가하면 그 어댑터 코드는 어댑티 클래스와 더불어 모든 서브클래스에 그대로 적용된다.

클래스 어댑터

  • 특정 어댑티 클래스에만 적용할 수 있지만, 어댑티 전체를 다시 구현하지 않아도 된다는 장점이 있다.
  • 서브클래스라서 어댑티의 행동을 오버라이드할 수 있으므로 코드 분량을 줄일 수 있다.
  • 메소드를 빠르게 오버라이드 할 수 있다.

Enumeration을 Iterator에 적응시키기

Enumeration

classDiagram
    class Enumeration {
        << interface >>
        hasMoreElements()*
        nextElement()*
    }

자바의 초기 컬렉션 형식(Vector, Stack, Hashtable)은 Enumeration을 반환하는 elements() 메소드가 구현되어 있었는데, Enumeration 인터페이스를 사용하면 컬렉션의 각 항목이 어떻게 관리되는지 신경 쓸 필요 없이 컬렉션의 모든 항목에 접근할 수 있다.

Iterator

classDiagram
    class Iterator {
        << interface >>
        hasNext()*
        next()*
        remove()*
    }

최근에는 컬렉션에 있는 일련의 항목에 접근하고, 그 항목을 제거할 수 있게 해주는 Iterator 인터페이스를 사용하기 시작했다.

Enumeration 인터페이스를 사용하는 구형 코드를 다뤄야 할 때도 있지만 새로운 코드를 만들 때는 Iterator만 사용하는 게 좋다. 이럴 때 어댑터 패턴을 사용할 수 있다.

어댑터 디자인하기

타깃 인터페이스를 구현하고, 어댑티 객체로 구성된 어댑터를 구현해야한다.

classDiagram
    direction TB
  class Enumeration {
    << interface >>
    hasMoreElements()*
    nextElement()*
  }

  class EnumerationIterator {
    hasNext()
    next()
    remove()
  }

  class Iterator {
    << interface >>
    hasNext()*
    next()*
    remove()*
  }
  
  Iterator <|.. EnumerationIterator
  Enumeration <|-- EnumerationIterator

hasNext()next() 메소드는 타깃에서 어댑티로 바로 연결되지만 remove() 메소드는 제공하지 않으므로 별도 구현이 필요하다.

remove() 메소드 처리하기

결론적으로 어댑터 차원에서 완벽하게 작동하는 remove() 메소드 구현 방법은 없다.

그나마 가장 좋은 방법은 런타임 예외를 던지는 것 이다.

이러한 상황을 대비하여 Iterator 인터페이스를 디자인 한 사람들은 UnsupportedOperationExcetpion을 지원하도록 구현하였다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class EnumerationIterator implements Iterator<Object> {
    EnumerationIterator<?> 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();
    }
}

데코레이터 패턴과 어댑터 패턴

데코레이터 패턴과 어댑터 패턴은 모두 구조적 패턴(Structural pattern)에 속하는데, 기존 클래스들을 조합하여 새로운 기능을 제공하거나 인터페이스를 맞추는 데 사용할 수 있지만, 목적과 사용되는 상황에서 차이가 있다.

데코레이터 패턴

데코레이터 패턴은 객체에 동적으로 새로운 책임을 추가하거나 객체를 감싸서 행동을 확장하는 패턴이다.

  • 상속을 통해 기능을 확장하는 대신 객체를 감싸는 방식으로 기능을 추가하게된다.
  • 클라이언트에게는 원래의 객체와 데코레이터로 감싼 객체를 동일하게 다루도록 한다.

어댑터 패턴

어댑터 패턴은 서로 다른 인터페이스를 가진 두 클래스를 함께 동작하도록 만들어주는 패턴이다.

  • 기존의 코드를 수정하지 않고 새로운 인터페이스를 제공한다.
  • 클라이언트 코드가 새로운 인터페이스를 사용할 수 있게 한다.

퍼사드 패턴

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

퍼사드 패턴은 하나 이상의 클래스 인터페이스를 깔끔하면서도 효과적인 퍼사드(facade)로 덮어 인터페이스를 단순하게 바꾸기 위해 인터페이스를 변경한다.

  • 쓰기 쉬운 인터페이스를 제공하는 퍼사드 클래스를 구현함으로써 복잡한 시스템을 훨씬 편리하게 사용할 수 있다.
  • 퍼사드는 인터페이스를 단순하게 만들고 클라이언트와 구성 요소로 이루어진 서브시스템을 분리하는 역할도 수행할 수 있다.

퍼사드와 어댑터는 모두 여러 개의 클래스를 감쌀 수 있지만 퍼사드는 인터페이스를 단순하게 만드는 용도로 쓰이는 반면, 어댑터는 인터페이스를 다른 인터페이스로 변환하는 용도로 쓰인다.

활용

퍼사드 패턴을 사용하려면 어떤 서브시스템에 속한 일련의 복잡한 클래스를 단순하게 바꿔서 통합한 클래스를 만들어야 한다.

  • 다른 패턴과 달리 퍼사드 패턴은 복잡한 추상화가 필요 없으므로 상당히 단순한 편이다.

퍼사드 패턴을 사용하면 클라이언트와 서브시스템이 서로 긴밀하게 연결되지 않아도 되고, 다음 최소 지식 원칙을 준수하는데도 도움을 준다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// 서브시스템 클래스
class CPU {
    public void processData() {
        System.out.println("Processing data by CPU");
    }
}

class Memory {
    public void load() {
        System.out.println("Loading data into memory");
    }
}

class HardDrive {
    public void readData() {
        System.out.println("Reading data from hard drive");
    }
}

// 퍼사드 클래스
class ComputerFacade {
    private CPU cpu;
    private Memory memory;
    private HardDrive hardDrive;

    public ComputerFacade() {
        this.cpu = new CPU();
        this.memory = new Memory();
        this.hardDrive = new HardDrive();
    }

    public void start() {
        System.out.println("Starting computer");
        cpu.processData();
        memory.load();
        hardDrive.readData();
        System.out.println("Computer started");
    }

    public void shutDown() {
        System.out.println("Shutting down computer");
        // 여러 서브시스템을 종료하는 로직
        System.out.println("Computer shut down");
    }
}

// 클라이언트 코드
public class Main {
    public static void main(String[] args) {
        ComputerFacade computer = new ComputerFacade();
        computer.start();

        // 클라이언트는 퍼사드를 통해서만 컴퓨터를 다룰 수 있음
        computer.shutDown();
    }
}

최소 지식 원칙

객체 사이의 상호작용은 될 수 있으면 아주 가까운 친구사이에만 허용하는 편이 좋다.

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

  • 이 원칙을 잘 따르면 여러 클래스가 복잡하게 얽혀 있어 시스템의 한 부분을 변경했을 때 다른 부분까지 줄줄이 고쳐야 하는 상황을 미리 방지할 수 있다.
  • 여러 클래스가 서로 복잡하게 의존하고 있다면 관리하기도 힘들고, 이해하기 어려운 불안정한 시스템이 만들어진다.

친구를 만들지 않고 다른 객체에 영향력 행사하기

최소 지식 원칙은 친구를 만들지 않는 4개의 가이드라인을 제시한다.

  • 객체 자체
  • 메소드에 매개변수로 전달된 객체
  • 메소드를 생성하거나 인스턴스를 만든 객체
  • 객체에 속하는 구성 요소

메소드를 호출한 결과로 리턴받은 객체에 들어있는 메소드를 호출하면 다른 객체의 일부분에 요청하게되고, 직접적으로 알고 지내는 객체의 수가 늘어난다.

이 상황에서 최소 지식 원칙을 따르려면 객체가 대신 요청하도록 만드러야하며, 이를 통해 객체의 한 구성 요소를 알고 지낼 필요가 없어진다.

원칙을 따르지 않는 경우

1
2
3
4
public float getTemp() {
    Thermometer thermometer = station.getThermometer();
    return thermometer.getTemperature();
}

원칙을 따르는 경우

1
2
3
public float getTemp() {
    return station.getTemperature();
}

최소 지식 원칙을 적용해서 thermometer에게 요청을 전달하는 메소드를 station 클래스에 추가하여 의존해야하는 클래스의 개수를 줄일 수 있다.

절친에게만 메소드 호출하기

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Car {
    // 구성 요소의 메소드는 호출해도 좋음
    Engine engine;
    // ...
  
    public Car() {
        // ...      
    }
    
    public void start(Key key) {
        // 새로운 객체를 생성
        // 이 객체의 메소드는 호출해도 좋음
        Doors doors = new Doors();
        // 매개변수로 전달된 객체의 메소드는 호출해도 좋음
        boolean authorized = key.turns();
        
        if (authorized) {
            // 구성 요소의 메소드를 호출해도 좋음
            engine.start();
            // 객체 내의 있는 메소드는 호출해도 좋음
            updateDashboardDisplay();
            // 직접 생성하거나 인스턴스를 만든 객체의 메소드는 호출해도 좋음
            doors.lock();
        }
    }
    
    public updateDashboardDisplay() {
        // 디스플레이 갱신
    }
}

최소 지식 원칙을 잘 따르면 객체 사이의 의존성을 줄일 수 있으며 소프트웨어 관리가 더 편해진다.

하지만 적용하다 보면 메소드 호출을 처리하는 래퍼 클래스를 더 만들어야 할 수도 있으며 이에 따라 시스템이 복잡해지고, 개발 시간도 늘어나고, 성능도 떨어질 수 있다.

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

  • 퍼사드 패턴에서 클라이언트의 친구는 퍼사드 클래스 하나 뿐이다.

정리

어댑터 패턴

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

퍼사드 패턴

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

  • 기존 클래스를 사용하려고 하는데 인터페이스가 맞지 않으면 어댑터를 쓴다.
  • 큰 인터페이스와 여러 인터페이스를 단순하게 바꾸거나 통합해야 하면 퍼사드를 쓴다.
  • 어댑터는 인터페이스를 클라이언트에서 원하는 인터페이스로 바꾸는 역할을 한다.
  • 퍼사드는 클라이언트를 복잡한 서브시스템과 분리하는 역할을 한다.
  • 어댑터를 구현할 때는 타깃 인터페이스의 크기와 구조에 따라 코딩해야 할 분량이 결정된다.
  • 퍼사드 패턴에서는 서브시스템으로 퍼사드를 만들고 진짜 작업은 서브클래스에 맡긴다.
  • 어댑터 패턴에는 객체 어댑터 패턴과 클래스 어댑터 패턴이 있으며, 클래스 어댑터를 쓰러면 다중 상속이 가능해야 한다.
  • 한 서브시스템에 퍼사드를 여러개 만들어도 된다.
  • 어댑터는 객체를 감싸서 인터페이스를 바꾸는 용도로, 데코레이터는 객체를 감싸서 새로운 행동을 추가하는 용도로, 퍼사드는 일련의 객체를 감싸서 단순하게 만드는 용도로 쓰인다.