프록시(Proxy) 패턴

|

프록시(Proxy) 패턴의 UML은 다음과 같습니다.

Image

Proxy는 어떤 작업을 대신해줄 수 있는 ‘대리인’이라고 생각하면 됩니다.

보통 Proxy라고 하면 흔히 생각할 수 있는 것으로는 HTTP Proxy 같은 것을 떠올릴 수 있습니다. HTTP Proxy는 중간에 있는 Proxy 서버에서 특정 웹사이트들을 캐싱(Caching)하고 있다가 사용자의 요청이 있으면 캐시에 저장하고 있던 페이지를 보내주고, 필요에 따라 실제 웹페이지에 접속하여 더 많은 정보를 사용자에게 보여주는 일을 하고 있습니다.

그 외에도 안드로이드 개발을 해보았으면 한 번쯤은 접해보았을 법한 AIDL(Android Interface Definition Language)도 Proxy로 구성되어 있습니다. Proxy에서 양쪽간의 인터페이스를 정의하는 언어를 ‘IDL’이라고 합니다. 안드로이드에서는 여기에 Android의 ‘A’를 붙여서 ‘AIDL’이라고 합니다.


예제 코드

예제를 들어보도록 하겠습니다. 프린터라는 클래스에 대한 Proxy를 두는 예제인데, 이름을 바꾸는 등의 간단한 작업은 실제 프린터가 하지 않고, 프린터의 Proxy가 수행합니다. 하지만, ‘출력’과 같이 Proxy가 할 수 없는 일은 실제 프린터가 수행하는 예제입니다.

public interface Printable {

  public abstract void setPrinterName(String strName);

  public abstract String getPrinterName();

  public abstract void print(String strString);
}


public class Printer implements Printable {

  private String mName;

  public Printer() {
    heavyJob("Making an instance of Printer class");
  }

  public Printer(String name) {
    mName = name;
    heavyJob("Making an instance of Printer class + " + name);
  }

  @Override
  public void setPrinterName(String name) {
    mName = name;
  }

  @Override
  public String getPrinterName() {
    return mName;
  }

  @Override
  public void print(String text) {
    System.out.println("[Printer] === " + mName + " ===");
    System.out.println("[Printer] " + text);
  }

  private void heavyJob(String message) {
    System.out.println("[Printer] " + message);

    for(int i = 0; i < 5; i++) {
      try {
        Thread.sleep(1000);
      } catch(InterruptedException e) {
        e.printStackTrace();
      }

      System.out.println("[Printer] .");
    }

    System.out.println("[Printer] Finish..");
  }
}


public class PrinterProxy implements Printable {

  private String mName;
  private Printer mRealPrinter;

  public PrinterProxy() {
  }

  public PrinterProxy(String name) {
    mName = name;
  }

  @Override
  public void setPrinterName(String name) {
    if(mRealPrinter != null) {
      mRealPrinter.setPrinterName(name);
    }
    mName = name;
  }

  @Override
  public String getPrinterName() {
    return mName;
  }

  @Override
  public void print(String text) {
    realize();
    mRealPrinter.print(text);
  }

  private synchronized void realize() {
    if(mRealPrinter == null) {
      mRealPrinter = new Printer(mName);
    }
  }
}

데코레이터(Decorator) 패턴

|

데코레이터 패턴의 UML은 다음과 같습니다.

Image

Head-First 교재에서 설명하는 데코레이터 패턴의 UML은 다음과 같습니다.

Image

데코레이터 패턴을 간단히 설명하면 어떤 객체를 꾸미고자 할 때(장식할 때) 그 결과값으로 동일한 객체 타입이 리턴되는 패턴이라고 할 수 있습니다.

예를 들어, 어떤 커피점에서 커피를 만들어낸다고 할 때, 각 커피에 해당하는 클래스를 일일이 만들 경우 다음과 같은 상황이 발생할 수 있습니다.

Image

이 때 데코레이터 패턴을 사용하면 각 커피에 해당하는 클래스를 하나하나 만들지 않더라도 다음과 같은 형태로 다양한 커피를 만들어 낼 수 있습니다.


데코레이터 패턴이 적용된 예시

Beverage darkMochaCoffee = new Whip(new Mocha(new DarkRoast()));

Beverage soyMochaCoffee = new HouseBlend();
beverage2 = new Soy(beverage2);
beverage2 = new Mocha(beverage2);
beverage2 = new Whip(beverage2);

위 예제와 같이 데코레이터 패턴을 이용하면 새로운 커피 조리법이 나오더라도 코드의 큰 수정없이 대처가 가능합니다.

Java의 I/O 스트림의 경우가 데코레이터 패턴이 적용된 대표적인 예시라고 볼 수 있습니다.

Image


컴포지트 패턴과 데코레이터 패턴

컴포지트 패턴과 데코레이터 패턴은 비슷한 부분이 많이 있습니다. 특히 재귀적으로 순환하는 방식은 공통적인 성격입니다. 다만 두 패턴은 그 목적에서 큰 차이점이 있습니다. 컴포지트 패턴은 여러 개로 구성된 클래스들이 동일한 형태로 구성될 수 있도록 구조화하는데 목적이 있다면, 데코레이터 패턴은 기능을 클래스화함으로써 동적으로 기능을 추가하거나 삭제할 수 있도록 하는데 목적이 있습니다.

컴포지트 패턴과 데코레이터 패턴은 동시에 사용되어 상호보완적인 역할을 할 수도 있습니다.

컴포지트(Composite) 패턴

|

컴포지트(Composite) 패턴의 UML은 다음과 같습니다.

Image

컴포지트 패턴의 개념은 각 객체들을 동일화시키겠다는 것입니다. 조금 다르게 표현하자면 추상적인 상위 클래스 하나를 만들고, 그 클래스를 상속받는 다양한 자식 클래스들을 만드는 것입니다. 그런 다음 그 자식 클래스들을 마치 같은 종류의 클래스 다루듯이 동일시해서 사용하겠다는 패턴입니다.

컴포지트 패턴은 커맨드(Command) 패턴이나 방문자(Visitor) 패턴, 데코레이터(Decorator) 패턴 등에 응용되서 사용되어질 수 있고, 상당히 널리 쓰이는 패턴입니다. 그리고 여기서 더 나아가 트리 구조와 같은 재귀적인(Recursive) 구조를 만들기 위해서도 유용하게 쓰이고 있습니다.

트리 구조의 가장 대표적인 예로는 ‘파일 구조’를 들 수 있습니다. 파일 구조는 폴더와 파일로 이루어져 있으며, 폴더 아래에는 폴더들이 있을 수 있고, 또한 파일들이 있을 수 있습니다.


예제 코드

컴포지트 패턴은 다음과 같이 Component, Leaf, Composite 로 구성됩니다.

public abstract class Component {
// ...
}

public class Leaf extends Component {
// ...
}

public class Composite extends Component {
  // ...
  private ArrayList list = new ArrayList();

  public Component add(Component cp);

  public abstract void remove(Component cp);
  // ...
}


위에서 언급했던 ‘파일 구조’ 예제를 들어보도록 하겠습니다.


Component

public abstract class Entry {

  public abstract String getName();

  public abstract int getSize();

  public void printList() {
    printList("");
  }

  protected abstract void printList(String prefix);

  @Override
  public String toString() {
    return getName() + "(" + getSize() + ")";
  }
}


Leaf

public class File extends Entry {

  String name;
  int size;

  public File(String name, int size) {
    this.name = name;
    this.size = size;
  }

  @Override
  public String getName() {
    return this.name;
  }

  @Override
  public int getSize() {
    return this.size;
  }

  @Override
  protected void printList(String prefix) {
    System.out.println(prefix + "/" + this);
  }
}


Composite

public class Directory extends Entry {

  String name;
  ArrayList<Entry> list = new ArrayList<>();

  public Directory(String name) {
    this.name = name;
  }

  public void add(Entry item) {
    list.add(item);
  }

  public void remove(Entry item) {
    list.remove(item);
  }

  @Override
  public String getName() {
    return this.name;
  }

  @Override
  public int getSize() {
    int size = 0;

    for (Entry item: list) {
      size += item.getSize();
    }

    return size;
  }

  @Override
  protected void printList(String prefix) {
    System.out.println(prefix + "/" + this);
    for (Entry item: list) {
      item.printList(prefix + "/" + name);
    }
  }
}


Main

public class Main {
  public static void main(String[] args) {
    Directory root = new Directory("root");
    Directory user = new Directory("user");
    Directory snowdeer = new Directory("snowdeer");
    Directory media = new Directory("media");
    Directory image = new Directory("image");
    Directory music = new Directory("music");
    Directory video = new Directory("video");

    root.add(user);
    root.add(media);
    user.add(snowdeer);
    media.add(image);
    media.add(music);
    media.add(video);

    snowdeer.add(new File(".bashrc", 42));
    snowdeer.add(new File(".profile", 36));

    image.add(new File("cat.png", 122));
    image.add(new File("dog.png", 56));
    image.add(new File("bird.png", 465));

    music.add(new File("twice.png", 4382));
    music.add(new File("apink.png", 7726));

    video.add(new File("frozen.mp4", 341242));

    root.printList();
  }
}

퍼샤드(Facade) 패턴

|

퍼샤드(Facade)는 ‘정면’, ‘표면’ 이라는 뜻입니다. 그리고 카메라로 사진을 찍을 때 보는 ‘바늘 구멍’ 이라고 하기도 합니다. 모든 시야는 그 바늘 구멍을 통해서 보게 되듯이 특정 모듈의 ‘창구’ 역할을 하는 클래스를 두는 패턴을 Facade 패턴이라고 합니다.

Facade 패턴의 UML을 살펴보면 다음과 같습니다.

Image

Facade 패턴은 클래스의 은닉화, 캡슐화와 아주 깊은 관련이 있습니다. 어떤 클래스의 구조가 아주 다양하고 복잡하다고 하더라도 그걸 사용하는 개발자들은 그 내부 구조를 일일이 알 필요없이 특정 인터페이스 몇 개만 알아도 사용이 가능하도록 해주는 것이 바로 Facade 패턴입니다.

그러다보니, 앞서 포스팅했던 Mediator 패턴과 비슷한 부분이 있습니다.

한 군데로 모아서 관리한다는 특징이 비슷한데, 시스템 내부적으로 볼 때는 Mediator, 외부에서 볼 때는 Facade가 되는 경우가 많습니다. 따라서 보통 두 패턴이 동시에 사용되는 경우가 많습니다.


예제 코드

public class FacadeExample {

  private MusicPlayer mMusicPlayer = new MusicPlayer();
  private VideoPlayer mVideoPlayer = new VideoPlayer();
  private Gallery mGallery = new Gallery();

  public void playMusic() {
    mMusicPlayer.play();
  }

  public void playVideo() {
    mVideoPlayer.play();
  }

  public void showImage() {
    Gallery.show();
  }
}


위의 예제는 간단합니다. 음악 플레이어, 비디오 플레이어, 갤러리가 있을 때 각 컴포넌트를 감싸는 Facade 클래스를 만들고, 외부에서는 Facade를 통해 각 컴포넌트의 기능을 호출하는 예제입니다. 외부에서는 Facade 내부에 어떤 클래스가 있는지 알 필요가 없기 때문에 어떤 기능들을 SDK나 라이브러리 API 형태로 제공을 할 때 Facade 패턴을 많이 사용합니다.

중재자(Mediator) 패턴

|

중재자(Mediator) 패턴은 Colleague들의 분쟁을 중간에서 중재해주는 클래스를 두고, 각 Colleague들끼리는 서로를 직접적으로 참조하지 못하도록 하여 낮은 결합도(Low Coupling)를 가지게 해주는 패턴입니다.

Colleague들끼리는 서로를 알지 못하기 때문에 특정 작업을 요청 하려면 무조건 Mediator에게 요청을 해야 합니다. 따라서 Mediator의 함수가 많아지고 코드량이 길어진다는 단점이 있습니다. 하지만, 결합도를 낮추어주기 때문에 향후 Colleague가 변경되거나 추가, 삭제될 때 유지 보수가 유리해진다는 장점이 있습니다.

Mediator 패턴의 UML은 다음과 같습니다.

Image

Mediator 패턴을 쓸 수 있는 경우는 다음과 같은 시나리오를 들 수 있습니다.

어떤 GUI 화면이 있다. 특정 체크박스의 값이 바뀌면 화면에 있는 각 버튼의 Enabled/Disabled 속성이 변경 된다. 또한, 텍스트 박스의 값에 따라 각 컴포넌트의 속성이 변경된다. 이와같이 각 컴포넌트들이 서로가 서로에게 영향을 미치는 경우이다.


위와 같은 경우 Mediator를 두지 않고, 각 버튼이나 체크 버튼, 텍스트 박스 등의 이벤트 처리 부분에서 각 컴포넌트의 속성을 직접 바꾸는 경우를 생각해 봅시다. 이 경우, 복수의 컴포넌트가 하나의 리소스에 동시에 작업 요청을 한다던지, 두 컴포넌트가 서로에게 작업 요청을 하여 무한 루프와 같은 데드락(Deadlock) 상황이 발생할 수도 있습니다. 또한 각 컴포넌트간이 참조가 많기 때문에 향후 특정 컴포넌트가 다른 컴포넌트로 교체하는 등의 유지보수가 쉽지 않다는 문제가 있습니다.

이런 경우 Mediator를 두고 각 상태에 따라 Mediator가 각 컴포넌트의 상태들을 변경시켜주게 되면 코드도 깔끔해지고 유지 보수도 수월할 수 있습니다.


예제 코드

Mediator 패턴은 크게 Mediator 인터페이스와 Colleague 인터페이스로 구성됩니다.

public abstract class Mediator {

  private ArrayList<Colleague> mColleagueList = new ArrayList<Colleague>();

  public void addColleague(Colleague colleague) {
    mColleagueList.add(colleague);
  }

  public void removeColleague(Colleague colleague) {
    mColleagueList.remove(colleague);
  }

  public void init() {
    for(Colleague colleague : mColleagueList) {
      colleague.init();
    }
  }

  public void fin() {
    for(Colleague colleague : mColleagueList) {
      colleague.fin();
    }
  }
}


public abstract class Colleague {

  private Mediator mMediator;

  public void setMediator(Mediator mediator) { mMediator = mediator; }

  public Mediator getMediator() { return mMediator; }

  public abstract void init();

  public abstract void fin();
}


그리고 Mediator와 Colleague를 상속(구현)받는 클래스들을 구현하면 됩니다.