스트래티지(Strategy) 패턴

|

스트래티지(Strategy) 패턴의 UML은 다음과 같습니다.

Image

디자인 패턴 중에 가장 많이 쓰이는 패턴 중 하나입니다. 이름을 보면 ‘전략’이라는 뜻을 의미하고 있고, 실제로 알고리즘 등을 캡슐화하여 쉽게 교체해서 쓸 수 있도록 해주는 디자인 패턴입니다. 게임 등에서 난이도나 적들의 인공지능 들을 동적으로 쉽게 변경할 때도 많이 쓰입니다.


예제

Strategy 패턴 예제로 가장 많이 사용되고 있는 오리(Duck) 클래스를 예로 들도록 하겠습니다.

public abstract class Duck {

  public Duck() {}

  public abstract void display();

  public void fly() {
    System.out.println("Flying...");
  }

  public void quack() {
    System.out.println("Quack! Quack! Quack!");
  }
}

Duck이라는 추상 클래스가 있습니다. fly()quack() 메소드는 구현이 되어 있고, display() 메소드는 상속받아서 구현하도록 되어 있습니다.

이제 상속을 이용해서 노란 오리, 청둥 오리, 집 오리 등등 다양한 오리 클래스를 만들 수 있습니다.


상속의 한계점

하지만, ‘모형 오리’를 만들어야 하는 경우가 발생했습니다. 모형 오리는 날지도 못하고, 소리도 내지 못합니다.

물론, 상속을 이용해서 다음과 같이 fly() 메소드와 quack() 메소드 내부를 비워둔채로 구현해도 됩니다.

public class ModelDuck extends Duck {

  @Override
  public void display() {
    System.out.println("This is a ModelDuck.");
  }

  public void fly() {}

  public void quack() {}
}

이와 같은 방식으로 향후 추가될지도 모르는 ‘노란 모형 오리’, ‘태엽으로 움직이는 모형 오리’, ‘건전지로 움직이는 모형 오리’ 등 다양한 오리 클래스들도 모두 구현할 수 있습니다.

하지만, 수십/수백개의 클래스를 각각 만들어서 구현하고 관리하는 건 여간 번거로운 일이 아닐 수 없습니다. 특히 프로젝트 초기에는 모형 오리들이 말을 못했지만, 나중에 출시되는 제품들은 말을 할 수 있게 바꿔야 한다면, 각각의 클래스들을 전부 수정하는 것도 쉬운 일이 아닙니다.


Strategy 패턴 적용

이와 같은 경우 Strategy 패턴을 적용할 수 있습니다. Strategy 패턴은 각각의 알고리즘들을 캡슐화해서 쉽게 교체할 수 있게 할 수 있습니다.

여기서 fly()quack()라는 각각의 행동을 캡슐화할 수 있습니다. 예를 들어 각각의 행동을 IFlyBehavior, IQuackBehavior이라는 인터페이스로 치환할 수 있습니다.

public interface IFlyBehavior {
  void fly();
}
public interface IQuackBehavior {
  void quack();
}

그리고 Duck 클래스는 다음과 같습니다.

public abstract class Duck {

  IFlyBehavior iFlyBehavior;
  IQuackBehavior iQuackBehavior;

  public Duck() {}

  public abstract void display();

  public void performFly() {
    iFlyBehavior.fly();
  }

  public void performQuack() {
    iQuackBehavior.quack();
  }

  public void setFlyBehavior(IFlyBehavior fb) {
    iFlyBehavior = fb;
  }

  public void setQuackBehavior(IQuackBehavior fb) {
    iQuackBehavior = fb;
  }
}


각 행동들을 구현

fly()quack()라는 행동을 인터페이스로 정의했기 때문에 각 행동군들을 묶어서 클래스로 구현해줍니다.

public class FlyWithWings implements IFlyBehavior {

  @Override
  public void fly() {
    System.out.println("Fly with wings...");
  }
}
public class Quack implements IQuackBehavior {

  @Override
  public void quack() {
    System.out.println("Quack! Quack! Quack!");
  }
}

날지 못하는 오리들을 위한 NoFly 클래스도 만들어줍니다.

public class NoFly implements IFlyBehavior {

  @Override
  public void fly() {
    System.out.println("I can not fly.");
  }
}

이런 식으로 각 행동들을 알고리즘군으로 묶어서 캡슐화를 해주면, 각각의 오리 클래스들의 행동을 쉽게 구현하고 제어할 수 있게 됩니다.


오리 클래스 구현

모형 오리 클래스는 다음과 같습니다.

public class ModelDuck extends Duck {

  public ModelDuck() {
    setFlyBehavior(new NoFly());
    setQuackBehavior(new NoQuack());
  }

  @Override
  public void display() {
    System.out.println("I am a ModelDuck.");
  }
}


사용 예제

다음과 같은 코드를 이용해서 ‘ModelDuck’ 인스턴스를 만들고, 나중에 Fly 동작을 동적으로 변경이 가능합니다.

public class Main {

  public static void main(String[] args) {

    Duck duck = new ModelDuck();
    duck.display();
    duck.performFly();
    duck.performQuack();

    // change the behavior
    duck.setFlyBehavior(new FlyWithWings());
    duck.performFly();
  }
}

StringBuffer vs StringBuilder

|

안드로이드 개발을 하면서 StringBuffer 또는 StringBuilder를 많이 사용하게 될 것입니다. 특히 개발하면서 로그(Log) 메세지 등을 길게 붙여서 만들 때, StringBuffer나 StringBuilder를 모르는 경우 다음과 같이 코드를 작성하는 경우가 많습니다.

String strLog;
void log(String message) {
  strLog = strLog + message + "\n";
}


String

String은 Immutable 하기 때문에 한 번 할당되면 메모리 공간이 변하지 않습니다. 무슨 말이냐면, 위의 예제에서 strLog 라는 변수에 ‘strLog + message + “\n”’ 이라는 값을 넣게 되면, 새로운 String 인스턴스가 생기고 기존의 String은 제거가 되게 됩니다. (정확히는 가비지 컬렉터(Garbage Collector)가 제거할 것입니다.) 즉, 메모리 할당과 삭제가 빈번하게 일어나기 때문에 성능 하락의 원인이 될 수 있습니다.

이런 경우에는 String 대신 StringBuffer나 StringBuilder를 사용하면 성능 향상에 도움이 됩니다. 두 클래스는 append() 메소드를 이용하여 기존에 할당받은 메모리 공간을 유연하게 늘리면서 사용을 하게 됩니다. 그래서 메모리 할당과 삭제가 빈번하게 일어나는 일을 방지할 수 있습니다.

그러면 StringBuffer와 StringBuilder의 차이점을 알아보도록 하겠습니다.


StringBuffer vs StringBuilder

StringBuffer와 StringBuilder는 겉으로 보기에 크게 차이가 없습니다. 다만, 내부적으로 StringBuffer는 synchronized 키워드가 있어서 멀티쓰레드(Multi-Thread) 환경에서 좀 더 안전하다는 장점이 있습니다. 대신 성능은 StringBuilder 보다 약간 떨어지겠죠?

즉, 멀티쓰레드 환경에서는 StringBuffer, 그 외에는 StringBuilder를 사용하면 효율적으로 사용할 수 있습니다. 두 개를 외우기 싫으면 그냥 StringBuffer를 사용하시면 됩니다.

팩토리 메소드(Factory Method) 패턴

|

팩토리 메소드(Factory Method) 패턴의 UML은 다음과 같습니다.

Image

‘메소드’라는 단어를 생략하고 팩토리 패턴이라고도 불리우는 팩토리 메소드(Factory Method) 패턴에 대해서 알아보도록 하겠습니다.

Factory Method 패턴의 가장 큰 특징은 어떤 클래스의 인스턴스를 생성하는 방법을 바깥으로 노출시키지 않겠다는 것입니다. 인스턴스 생성은 서브 클래스에서 하게 되고 바깥에서는 Factory 역할을 하는 특정 클래스를 통해 해당 클래스의 인스턴스를 획득하게 됩니다.


예제 코드

예제를 보도록 하겠습니다.

public abstract class Pizza {

  String mName;
  String mDough;
  String mSauce;
  ArrayList<Topping> mToppingList = new ArrayList<Topping>();

  public void prepare() {
    System.out.println("Preparing " + mName);
    System.out.println("Tossing dough...");
    System.out.println("Adding sauce...");
    System.out.println("Adding topping: ");
    for(int i = 0; i < mToppingList.size(); i++) {
      System.out.println("   " + mToppingList.get(i));
    }
  }

  public void bake() {
    System.out.println("Bake for 25 minutes");
  }

  public void cut() {
    System.out.println("Cutting the pizza into diagonal slices");
  }

  public void box() {
    System.out.println("Place pizza in official PizzaStore box");
  }

  public String getName() {
    return mName;
  }
}


이렇게 Pizza 라는 추상 클래스를 만들고, Pizza를 상속하는 다양한 Pizza 클래스를 만듭니다.

class CheesePizza extends Pizza {
  // TODO
}

class PepperoniPizza extends Pizza {
  // TODO
}

class ClamPizza extends Pizza {
  // TODO
}

class VeggiePizza extends Pizza {
  // TODO
}


그리고 Pizza를 만들어주는 추상 클래스인 PizzaStore를 만들도록 하겠습니다.

public abstract class PizzaStore {

  public PizzaStore() {
  }

  Pizza orderPizza() {
    Pizza pizza = createPizza();

    pizza.prepare();
    pizza.bake();
    pizza.cut();
    pizza.box();

    return pizza;
  }

  abstract Pizza createPizza();
}


마찬가지로 PizzaStore를 상속받는 Concrete Class들을 만들어주면 됩니다.

class CheesePizzaStore extends PizzaStore {

  @Override
  Pizza createPizza() {
    return new CheesePizza();
  }

}

class PepperoniPizzaStore extends PizzaStore {

  @Override
  Pizza createPizza() {
    return new PepperoniPizza();
  }
}


이렇게 하면 각각의 PizzaStore에서 createPizza() 만 호출하면 그에 맞는 피자들의 인스턴스가 생성이 됩니다.

프록시(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


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

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

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