Skip to content

Instantly share code, notes, and snippets.

@Curookie
Last active February 25, 2024 07:51
Show Gist options
  • Save Curookie/3cf492219d58db4a10b1dde239cbe3fd to your computer and use it in GitHub Desktop.
Save Curookie/3cf492219d58db4a10b1dde239cbe3fd to your computer and use it in GitHub Desktop.
프로그래밍, 디자인패턴

0. 서문과 목차


디자인 패턴을 학습하기 위한 최소 조건

디자인 패턴은 프로그래밍 전문가들이 어떻게 하면 더욱 더 효율적으로 프로그래밍할 수 있는지에 대해 고민하고 패턴화한 이론이므로(프레임워크나 라이브러리보다 높은 단계에 속한다.) 기본적으로 프로젝트 경험이나 언어에 대한 기본적 이해가 없으면 이해하기 매우 어렵고 공감도 되지 않습니다. 따라서 아래 조건을 다 만족시키고 공부하세요.

  1. 한 개이상의 객체지향 언어의 중급 개발자 수준
  2. OOP에 대해 알고 있고 실제 프로젝트에 적용하고자 하는자.
  3. 내 코드의 관점을 넓히고 싶고, 프로그래밍 적 시야를 넓히고 싶은자.

디자인 패턴의 장점

  1. 간단한 단어로 많은 분량을 전달할 수 있고, 상대가 기억해야할 분량을 줄여준다.
  2. 아키텍쳐에 대해 생각할 수 있는 수준도 끌어올려준다.


1. 스트래티지 패턴 (Strategy Pattern)

스트래티지 패턴에서는 알고리즘군을 정의하고 각각을 캡슐화하여 교환해서 사용할 수 있도록 만든다. 스트래티지를 활용하면 알고리즘을 사용하는 클라이언트와는 독립적으로 알고리즘을 변경할 수 있다.

상속의 문제

  1. 서브클래스에서 코드가 중복된다.
  2. 실행시에 특징을 바꾸기 어렵다.
  3. 모든 서브클래스의 메서드를 알기 어렵다.
  4. 코드를 변경했을 때 다른 서브클래스에게 원치 않는 영향을 끼칠 수 있다.

그럼 인터페이스와 상속을 같이 사용하면 어떨까?

  1. 서브클래스마다 2가지 이상의 형태로 사용되는 메서드(기능)를 따로 빼서 각각 인터페이스화
    -> 바보같은 코드, 인터페이스화 한 기능을 수정해야할 경우 그 기능을 가진 모든 클래스를 수정해야한다.(재사용이 불가)

그럼 어떻게 하라고!!

  1. 소프트웨어를 만들 때, 나중에 혹시 고쳐야 할 때도 기존 코드에 미치는 영향은 최소한으로 줄이면서, 작업을 할 수 있도록 만들 수 있는 방법이 있나요?

-> ★ 핵심 기술 : 디자인 원칙 1. 애플리케이션에서 달라지는 부분을 찾아내고, 달라지지 않는 부분으로부터 분리시킨다.
=> 달라지는 부분을 찾아서 나머지 코드에 영향을 주지 않도록 "캡슐화" 한다.

  1. 변하는(달라지는) 부분의 각각의 모든 기능을 클래스화해서 하나의 셋(Set=캡슐화)으로 묶는다. (하나의 기능을 인스턴스로 묶어준다.)

-> ★ 핵심 기술 : 디자인 원칙 2. 구현이 아닌 인터페이스에 맞춰서 프로그래밍한다.
=> 달라지는 기능의 클래스는 각 기능의 원초 행동 인터페이스를 상속받아 구현한다.
=> 꼭 인터페이스여야 하나요? 아니다! 추상클래스를 써도됨. 중요한 건 추상 상위 형식을 사용하는(다형성)을 고려해서 구현하라는 뜻!

  1. 이제 슈퍼클래스에 변하는 행동(만들었던 인터페이스) 형식의 인스턴스 변수를 추가해서 이 행동을 구현할 함수를 만들어 거기서 인터페이스에 담긴 행동을 실행. 이제 이 슈퍼클래스를 상속받는 서브클래스에서 생성자에 인스턴스 변수에 적절한 행동클래스를 생성해서 담아주면 끝!

-> 이제 슈퍼클래스에 Setter 함수로 행동변수에 새로운행동을 넣는 함수를 만들면 동적으로 행동을 수정할 수도 있다.
=> 즉, 캡슐화된 알고리즘(변하는 행동)을 만든다. A는 B이다.(상속)이 아닌 A에는 B가 있다.(구성= Composition)의 형태로 설계해라
-> ★ 핵심 기술 : 디자인 원칙 3. 상속보다는 구성을 활용한다.


2. 옵저버 패턴 (Observer Pattern)

객체들에게 연락망을 돌립시다.

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

출판사 + 구독자 = 옵저버 패턴

신문 구독 메커니즘만 제대로 이해할 수 있다면 옵저버 패턴을 쉽게 이해할 수 있다.
출판사를 주제(subject), 구독자를 옵저버(observer)라고 부른다는 것만 외워둬라.

옵저버 패턴의 특징

옵저버 패턴은 느슨한 결합, 상호작용을 하지만 서로에 대해 서로 잘 모른다는 것을 의미한다.
주제가 옵저버에 대해서 아는 것은 옵저버가 특정 인터페이스(Observer 인터페이스)를 구현한다는 것 뿐
옵저버의 구상 클래스가 무엇인지, 옵저버가 무엇을 하는지 등에 대해서는 알 필요가 없다.

옵저버는 언제든지 새로 추가할 수 있습니다.
새로운 형식의 옵저버를 추가하려고 할 때도 주제를 전혀 변경할 필요가 없습니다.
-> ★ 핵심 기술 : 디자인 원칙 4. 서로 상호작용을 하는 객체 사이에서는 가능하면 느슨하게 결합하는 디자인을 사용해야 한다.

기본적인 옵저버 패턴은 푸쉬 방식이여서,
정보가 필요하지 않을 때도, 많은 정보가 필요하지 않아도 값이 변경될 때 받아야한다.
이 부분을 고려한 Pull 방식을 결합한 옵저버 패턴이 있다.

Observable 클래스

  • addObserver()
  • deleteObserver()
  • notifyObserver()
  • setChanged()

WeatherData 클래스 (주제)

  • getTemperature()
  • getHumidity()
  • getPressure()

Observer 인터페이스

  • update()

GeneralDisplay 클래스 (옵저버)

  • update()
  • display()

자바의 java.util 패키지에 들어있는 옵저버 패턴은 Observer인터페이스와 Observable클래스의 구조를 갖고있다.
여기서 Subject interface가 아니라 클래스라는 점이 가장 큰 차이점인데,

객체가 옵저버가 되는 방법
Observer 인터페이스를 구현하고 Obserable 객체의 addObserver() 메소드를 호출하면 된다. 탈퇴하고자하면 deleteObserver()를 호출하면 됨.

Observable에서 연락을 돌리는 방법
java.util.Observable 수퍼클래스를 확장해서 Observable 클래스를 만들고,
setChanged() 메소드를 호출헤서 객체의 상태가 바뀌었다는 것을 알려야 한다.
그 다음 다음 중 하나의 메소드를 호출해야 한다. notifyObservers() [풀방식] 또는 notifyObservers(Object arg) - [푸쉬방식] arg는 옵저버 객체한테 전달할 임의의 데이터 객체임

옵저버가 연락을 받는 방법
update(Observable o, Object arg) - 연락을 보내는 주제 객체, notifyObservers()메소드에서 인자로 전달된 데이터 객체, 데이터 객체가 지정되지 않은경우 null이 된다. push 방식일 때는 notifyObservers(arg) 형태로 전달해야한다. 아니면 pull 방식을 써야 한다.

setChange() { //변화가 있는지 없을 땐 보내지 않도록 하기위해서 필요하다.  
  changed=true;  
}  

notifyObservers(Object arg) {
  if(changed) {
    update(this,arg)
    }
    changed=false;
  }
}

notifyObservers() {
  notifyObservers(null);
}

Observable은 클래스라는 단점이 있다. setChanged() 메소드가 Protected로 선언되어 있어서 서브클래스에서만 호출가능.
Observable API를 잘 활용하면 된다.

Listener(옵저버)와 Event를 처리하는 부분에는 이런 옵저버 패턴이 들어간다. Swing이나 JavaBeans에도 있다.


3. 데코레이터 패턴 (Decorator Pattern)

객체에 추가적인 요건을 동적으로 첨가한다. 데코레이터는 서브클래스를 만드는 것을 통해서 기능을 유연하게 확장할 수 있는 방법을 제공한다.
팩토리 패턴과 함께 사용하면 복잡성을 극복할 수 있다.

OCP(Open - Closed Principle)

기존 코드는 건드리지 않은 채로 확장을 통해서 새로운 행동을 간단하게 추가할 수 있도록 하는 게 바로 우리의 목표다. 이 목표를 달성했을 때 무엇을 얻을 수 있을까? 새로운 기능을 추가하는 데 있어서 매우 유연해서 급변하는 주변 환경에 잘 적응할 수 있으면서도 강하고 튼튼한 디자인을 만들 수 있다.
-> ★ 핵심 기술 : 디자인 원칙 5. 클래스는 확장에 대해서는 열려 있어야 하지만 코드 변경에 대해서는 닫혀 있어야 한다.

  • 데코레이터는 자신이 장식하고 있는 객체에게 어떤 행동을 위임하는 것 외에 원하는 추가적인 작업을 수행할 수 있다.
  • 데코레이터의 수퍼클래스는 자신이 장식하고 있는 객체의 수퍼클래스와 같다.
  • 한 객체를 여러 개의 데코레이터로 감쌀 수 있다.
  • 데코레이터는 자신이 감싸고 있는 객체와 같은 수퍼클래스를 가지고 있기 때문에 원래 객체(싸여져 있는 객체)가 들어갈 자리에 데코레이터 객체를 집어넣어도 상관 없다.
  • 객체는 언제든지 감쌀 수 있기 때문에 실행중에 필요한 데코레이터를 마음대로 적용할 수 있다.
public class StarBuzzCoffee {
  
  public static void main(String args[]) {
    Beverage beverage = new Espresso();
    System.out.println(beverage.getDescription() + " $"+ beverage.cost());
    
    Beverage beverage2 = new DarkRoast();
    beverage2 = new Mocha(beverage2);
    beverage2 = new Mocha(beverage2);
    beverage2 = new Whip(beverage2);
    System.out.println(beverage2.getDescription() + " $"+ beverage2.cost());
    
    Beverage beverage3 = new HouseBlend();
    beverage3 = new Soy(beverage3);
    beverage3 = new Mocha(beverage3);
    beverage3 = new Whip(beverage3);
    System.out.println(beverage3.getDescription() + " $"+ beverage3.cost());
  }
}

데코레이터가 적용된 예시는 java.io
InputStream 추상 구성요소
FilterInputStream 추상 데코레이터 FileInputStream, StringBufferInputStream, ByteArrayInputStream 구상 구성요소
PushbackInputStream, BufferedInputStream, DataInputStream, LineNumberInputStream 구상 데코레이터


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

객체를 생성하기 위한 인터페이스를 정의하는데, 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정하게 만듭니다. 팩토리 메소드 패턴을 이용하면 클래스의 인스턴스를 만드는 일을 서브클래스에서 맡기게 된다.
간단한 팩토리 패턴은 사실 상 패턴이라 하기보단, 하나의 객체지향의 관용구 같은 것이다. 객체 생성하는 부분을 클래스화(캡슐화)하여 변하는 부분을 다른 클래스에 맡기는 방식이다.

팩토리 메소드 패턴은 상속을 통해서 객체를 만든다.
서브 클래스를 통해서 객체를 만들기 위한 것.

병렬 클래스 계층구조

ex) 제품 클래스와 생산자 클래스를 캡슐화하고 각각은 구현체에서 서로 연관이 되어 있다.

메소드를 final 로 선언하면 서브클래스에서 고쳐서 사용하지 못하게 할 수 있다.

의존성 뒤집기 원칙 (Dependency Inversion Priciple)

뒤집기인 이유는 객체지향 디자인을 할 때 일반적으로 생각하는 방법과는 반대로, 뒤집어서 생각해야 하기 때문이다.
고수준, 저수준 모듈이 둘 다 하나의 추상 클래스에 의존하고 있어야하기 때문이다.

구상 클래스에 대한 의존성을 줄이는 것이 좋다.
-> ★ 핵심 기술 : 디자인 원칙 6. 추상화된 것에 의존하도록 만들어라. 구상 클래스에 의존하도록 만들지 않도록 한다.
구성요소를 고수준과 저수준으로 나눌 수 있는데, 고수준은 저수준 구성요소에 의해 정의되는 행동이 들어 있는 구성요소, 저수준은 공통되는 것 묶을 수 있는 것이다.

내 생각에는 팩토리 패턴하고 스트레티지 패턴과 비슷하다.

원칙을 지키는 데 도움이 될만한 가이드라인

  • 어떤 변수에도 구상 클래스에 대한 레퍼런스를 저장하지 않기

  • 구상 클래스에서 유도된 클래스를 만들지 말기

  • 베이스 클래스에 이미 구현되어 있던 메소드를 오버라이드하지 말기.

                 PizzaStore  
                 +createPizza()  
           △                     △  
           |                     |  
      NYPizzaStore        ChicagoPizzaStore  
      +createPizza()      +createPizza()  
      
                     Pizza  
           △                     △  
           |                     |                     
    

    NYStyleCheesePizza ChicagoStyleCheesePizza
    NYStylePepperoniPizza ChicagoStylePepperoniPizza


5. 팩토리 추상화 패턴 (Factory Method Pattern)

인터페이스를 이용하여 서로 연관된, 또는 의존하는 객체를 구상 클래스를 지정하지 않고도 생성할 수 있다.
제품 군을 만드는 방법을 정의하기 위한 인터페이스이다.
서로 연관된, 또는 의존적인 객체들로 이루어진 제품군을 생성하기 위한 인터페이스를 제공합니다. 구상 클래스는 서브 클래스에 의해 만들어진다.

팩토리 추상화 패턴은 객체 구성(Composition)을 통해서 객체를 만들다.
추상 형식을 제공한다. 인스턴스를 만든 다음 만든 코드에 전달해야 함
구현 시에 팩토리 메소드를 써서 제품을 생산하는 용도로 사용해야할 때가 있다.

               <<인터페이스>>   
               PizzaIngredientFactory  
               +createDough()  
               +createSauce()  
          △                             △   
          :                             :  

NYPizzaIngredientFactory ChicagoPizzaIngredientFactory
+createDough() +createDough()
+createSauce() +createSauce()

         <<인터페이스>>  
         Dough  
   △               △  
   :               :   

ThickCrustDough ThinCrustDough


6. 싱글톤 패턴 (Singleton Pattern)

객체 중에서는 사실 하나만 있어야하는 경우가 많다. 스레드 풀이든가 캐시, 대화상자, 사용자 설정, 레지스트리 설정을 처리하는 객체, 로그 기록용 객체 등을 구현하는데 사용한다.
전역변수와 차이점은 필요할 때만 객체를 생성할 수 있다는 점이다.
해당 클래스의 인스턴스가 하나만 만들어지고, 어디서든지 그 인스턴스에 접근할 수 있도록 하기 위한 패턴

고전적인 싱글톤 패턴 구현

public MyClass {
  private MyClass() {} //생성자 생성방지  
  
  private static MyClass _mC; //유일한 인스턴스를 저장하기 위한 변수
  
  public static MyClass getInstance() {
    if(_mC==null) _mC = new MyClass();
    return _mC;
  }
  
  //유일한 클래스내에서 행할 메서드들..
}

고전적인 싱글톤 패턴의 문제는 멀티스레딩일 때 두 개의 객체가 생성되서 리턴될 수 있다는 점이다.

해결 방법

  1. synchronized 지정자를 getInstance()에 입력해 동기화를 시키는 방법
  2. 인스턴스를 필요할 때 생성하지 말고, 처음부터 만들어 버린다.
  3. DCL(Double-Checking-Locking)을 써서 getInstance()에서 동기화되는 부분을 줄인다. ex)
public MyClass {
  private MyClass() {} //생성자 생성방지  
  
  private volatile static MyClass _mC; //유일한 인스턴스를 저장하기 위한 변수, volatile 키워드를 사용하면 멀티스레딩을 쓰더라도 초기화되는 과정이 올바르게 진행되도록 할 수 있다.  
  
  public static MyClass getInstance() {
    if(_mC==null) {
      synchronized (MyClass.class) {
        if(_mC==null) _mC = new MyClass();  //블록으로 들어온 후에도 다시 한 번 변수가 null인지 확인한 다음 인스턴스를 생성한다. 
      }
    }
    return _mC;
  }
  
  //유일한 클래스내에서 행할 메서드들..
}

발생할 수 있는 문제점과 장점

  • 클래스 로더가 두개 이상일 경우 두개의 인스턴스가 생길 수 있다. 이때 클래스로더를 지정해주는게 좋다.
  • 싱글턴의 서브클래스를 만드는 건 옳은 방안이 아니다.
  • 게으른 인스턴스 생성을 할 수 있다.
  • 전역 변수를 사용하다 보면 간단한 객체에 대한 전역 레퍼런스를 자꾸 만들게 되면서 네임스페이스를 지저분하게 만드는 경향이 생기게 된다.

7. 커맨드 패턴 (Command Pattern)

호출 캡슐화, 메소드 호출을 캡슐화
요구 사항을 객체로 캡슐화 할 수 있으며, 매개변수를 써서 여러 가지 다른 요구 사항을 집어넣을 수도 있다. 또한 요청 내역을 큐에 저장하거나 로그로 기록할 수도 있으며, 작업취소 기능도 지원 가능합니다.

커맨드 패턴은 모두 같은 인터페이스를 구현해야 한다. 메소드가 하나밖에 없다. execute()

public interface Command {
  public void execute();
}

전등을 켜기 위한 커맨드 클래스 구현

public class LightOnCommand implements Command {
  Light light;
  
  public LightOnCommand(Light light) {
    this.light = light
  }
  
  public void execute() {
    light.on();
  }
}

커맨드 객체 사용하기

public class SimpleRemoteControl {
  Command slot;
  
  public SimpleRemoteControl() {}
  
  public void setCommand(Command command) {
    slot = command;
  }
  
  public void buttonWasPressed() {
    slot.execute();
  }
}

리모컨을 사용하기 위한 간단한 테스트 클래스

public class RemoteControlTest {
  public static void main(String[] args) {
    SimpleRemoteControl remote = new SimpleRemoteControl();
    Light light = new Light();
    LightOnCommand lightOn = new LightOnCommand(light);
    
    remote.setCommand(lightOn);
    remote.buttonWasPressed();
  }
}

메타 커맨드 패턴(Meta Command Pattern)을 이용하면 명령들로 이뤄진 매크로를 만들어서 여러 개의 명령을 한 번에 실행할 수 있다.

ex) 구현

public class MacroCommand implements Command {
  Command[] commands;
  
  public MacroCommand(Command[] commands) {
    this.commands = commands;
  }
  
  public void execute() {
    for(int i=0; i<commands.length; i++) {
      commands[i].execute();
    }
  }
}

ex) 사용

Command[] partyOn = { lightOn, stereoOn, tvOn, hottubOn };
Command[] partyOff = { lightOff, steroOff, tvOff, hottubOff };

MacroCommand partyOnMacro = new MacroCommand(partyOn);
MacroCommand partyOffMacro = new MacroCommand(partyOff);

remoteControl.setCommand(0, partyOnMacro, partyOffMacro);

NoCommand를 만들어서 미리 넣어두면, 특정 슬롯에 아무것도 없을 때 처리도 할 수 있다.

Undo 기능을 넣을 때는 이전 상태를 기억해놓고 커맨드가 작용할 때 이전 상태로 돌아가면 된다.

History 기능은 실행한 커맨드를 스택에 집어넣으면 되고, Undo버튼을 누르면 인보커에서 스택 맨 위에 있는 항목을 꺼내서 Undo() 메서드를 호출하면 됨.

커맨드 패턴의 장점

  1. 요청을 큐에 저장할 수 있으므로, 컴퓨테이션의 한 부분을 패키지로 묶어서 일급 객체 형태로 전달가능하고, 스레드처리에도 유리해진다.
  2. 요청을 로그에 기록하기 쉬워진다. (store(), load() 메소드를 추가해서 복구할 수도 있음)

8. 어댑터 패턴 (Adapter Pattern)

한 클래스의 인터페이스를 클라이언트에서 사용하고자 하는 다른 인터페이스로 변환한다. 어댑터를 이용하면 인터페이스 호환성 문제 때문에 같이 쓸 수 없는 클래스들을 연결해서 쓸 수 있다.

사용방법

  1. 클라이언트에서 타겟 인터페이스를 사용하여 메소드를 호출함으로써 어댑터에 요청을 합니다.
  2. 어댑터에서는 어댑티 인터페이스를 사용하여 그 요청을 어댑티에 대한 (하나 이상의) 메소드 호출로 변환합니다.
  3. 클라이언트에서는 호출 결과를 받긴 하지만 중간에 어댑터가 껴 있는지는 전혀 알지 못합니다.

사용 예

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

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

public class TurkeyAdapter implements Duck {
  Turkey turkey;
  
  public TurkeyAdapter(Turkey turkey) {
    this.turkey = turkey;
  }
  
  public void quack() {
    turkey.gobble();
  }
  
  public void fly() {
    for(int i=0; i<5; i++) {
      turkey.fly();
    }
  }
}

public class DuckTestDrive {
  public static void main(String[] args) {
    MallardDuck duck = new MallardDuck();
    
    WildTurkey turkey = new WildTurkey();
    Duck turkeyAdapter = new TurkeyAdapter(turkey); //TurkeyAdapter를 써서 Duck처럼 보이게 만듬
    
    testDuck(duck);
    
    testDuck(turkeyAdapter);
  }
  static void testDuck(Duck duck) {
    duck.quack();
    duck.fly();
  }
}

대체적으로 한 클래스만 감싸는 편이다. 여러개를 감싸는 내용은 퍼사드 패턴과 연관이 깊다.

                    ㅊ
Client ----▶  <<interface>>
①                target
                   △
                   .
                   .
                   .
                 Adapter  ----▶  Adaptee
                    ③                ④

① 클라이언트에서는 오리한태 얘기하고 있다고 생각한다.
① Target은 Duck 클래스 클라이언트에서는 이 클래스와 메소드를 호출한다.
③ 어댑터에서는 Duck 인터페이스를 구현하지만, 메소드가 호출되었 을 때 그 호출을 Turkey 메소드 호출로 변환해 준다.
④ Turkey클래스의 인터페이스는 Duck 인터페이스와 다르다. Turkey 클래스에는 quack()같은 메소드가 없다. 어댑터 덕분에 클라이언트에서 Duck 인터페이스에 대해 호출한 것을 Turkey(어댑터)에서도 받아서 받아서 처리할 수 있다.

위에는 객체 어댑터이고 클래스 어댑터도 있다.
클래스 다중상속이 가능한 언어에서는 클래스 어댑터를 사용할 수 있는데 구조는 이렇다.

Client ----▶  target
                 △
                  |
                  |
                  |
               Adapter  ----▷  Adaptee

클래스 어댑터에서는 어댑터를 만들 때 타겟과 어댑티 모두의 서브클래스로 만들고, 객체 어댑터에서는 구성을 통해서 어댑티에 요청을 전달한다는 점을 제외하고는 별 다른 차이점이 없다.

실전상황 예시

Enumerator 인터페이스를 사용하는 구형 코드를 사용해야 하는 경우가 종종 있지만, 새로 만드는 코드에서는 Iterator만 사용할 계획일 경우, 어댑터 패턴을 사용하면 좋다.

다만 두 인터페이스에 공통된 함수가 없을 경우, 예외처리를 해야한다.

데코레이터 패턴과 비교해보기

데코레이터는 책임과 행동이 디자인에 추가되지만 어댑터는 그렇지 않다. 어댑터는 한 인터페이스를 다른 인터페이스로 변환해주는 역할.
데코레이터는 인터페이스를 변환하지 않고, 객체를 감싼다.
데코레이터는 메소드 호출이 전달돼도 얼마나 많은 다른 데코레이터들을 거쳐왔는지도 알 수 없다.


9. 퍼사드 패턴 (Facade Pattern)

인터페이스를 단순화시키기 위해서 인터페이스를 변경하는 패턴이 있다. 하나 이상의 클래스의 복잡한 인터페이스를 깔끔하면서도 말쑥한 퍼사드(겉모양, 외관을 뜻함)로 덮어준다. 어떤 서브시스템의 일련의 인터페이스에 대한 통합된 인터페이스를 제공한다. 퍼사드에서 고수준 인터페이스를 정의하기 때문에 서브시스템을 더 쉽게 사용할 수 있다.

아래와 같은 시스템을 간단하게 만들기 위해 필요한 방법.

popper.on();
popper.pop();

lights.dim(10);

screen.down();

projector.on();
projector.setInput(dvd);
projector.wideScreenMode();

amp.on();
amp.setDvd(dvd);
amp.setSurroundSound();
amp.setVolume(5);

dvd.on()
dvd.play(movie);

구현방법

퍼사드는 클래스를 모든 서브시스템에 장착해 접근하여 저런 일련의 과정들을 하나의 메서드에 담아 처리한다.

퍼사드의 장점

클라이언트 구현과 서브시스템을 분리시킬 수 있다.
인터페이스를 단순화 시킬 수 있다.

구현의 예제

public 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.tuner = tuner;
    this.dvd = dvd;
    this.cd = cd;
    this.projector = projector;
    this.screen = screen;
    this.lights = lights;
    this.popper = popper;
  }

  public void watchMovie(String movie) {
    popper.on();
    popper.pop();

    lights.dim(10);

    screen.down();

    projector.on();
    projector.setInput(dvd);
    projector.wideScreenMode();

    amp.on();
    amp.setDvd(dvd);
    amp.setSurroundSound();
    amp.setVolume(5);

    dvd.on()
    dvd.play(movie);
  }
}

public class HomeTheaterTestDrive {
  public static void main(String[] args) {
    //instantiate components here
    
    HomeTheaterFacade homeTheater = new HomeTheaterFacade(amp, tuner, dvd, cd, projector, screen, lights, popper);
    
    homeTheater.watchMovie("ABC Movie");
  }
}

-> ★ 핵심 기술 : 디자인 원칙 7. 최소 지식 원칙 - 정말 친한 친구하고만 얘기하라.

객체 사이의 상호작용은 될 수 있으면 아주 가까운 "친구" 사이에서만 허용하는 것이 좋다.
상호작용하는 클래스의 개수에 주의하고, 어떤 식으로 상호작용하는지에도 주의를 기울여야 한다. 줄줄이 고쳐야하는 상황을 방지할 수 있다.

친구는 만들지 않으면서 다른 객체에 영향력을 행사하는 방법 == 데메테르의 법칙(Law of Demeter)

4종류의 메소드만 호출하면 된다.

  1. 객체 자체
  2. 메소드에 매개변수로 전달된 객체
  3. 그 메소드에서 생성하거나 인스턴스를 만든 객체
  4. 그 객체에 속하는 구성요소

public class Car {
  Engine engine;  // 이 클래스의 구성요소. 이 구성요소의 메소드는 호출해도 됨.
  // 기타 인스턴스 변수 
  
  public Car() {
    // 엔진 초기화 등을 처리 
  }
  
  public void start(Key key) { // 매개변수로 전달된 객체의 메소드는 호출해도 됨.
    Door doors = new Doors(); // 새로운 객체를 생성. 이 객체의 메소드는 호출해도 됨.
    
    boolean authorized = key.turns();  // 매개변수..
    
    if(authorized) {
      engine.start(); // 이 객체의 구성요소의 메소드
      updateDashboardDisplay(); // 객체 내에 있는 메소드는 호출해도 됨.
      doors.lock(); // 직접 생성하거나 인스턴스를 만든 객체의 메소드는 호출해도 됨.
    }
  }
  
  public void updateDashboardDisplay() {
    // 디스플레이 갱신
  }
}

최소 지식 원칙의 단점

객체들 사이의 의존성의 장점이 있지만,
다른 구성요소에 대한 메소드 호출을 처리하기 위해 "래퍼" 클래스를 더 만들어야 할 수도 있다. 그러다 보면 시스템이 더 복잡해지고, 개발 시간도 늘어고, 성능이 떨어질 수도 있다.


10. 템플릿 메소드 패턴 (Template Method Pattern)

알고리즘 캡슐화. 메소드에서 알고리즘의 골격을 정의한다. 이걸 이용하면 알고리즘의 구조를 그대로 유지하면서 알고리즘의 여러 단계 중 일부는 서브클래스에서 재정의 할 수 있다.

  • 템플릿 메소드에서는 알고리즘의 각 단계들을 정의하며, 그 중 한 개 이상의 단계가 서브클래스에 의해 제공될 수 있다.

예제

public abstract class CaffeineBeverage {
  final void prepareRecipe() { // 템플릿 부분은 final이므로 오버라이드해서 건드릴 수 없다.
    boilWater();
    brew();           // 서브 클래스에서 처리
    pourInCup();
    addCondiments();  // 서브 클래스에서 처리
  }
  
  abstract void brew();
  abstract void addCondiments();
  
  void boilWater() {
    // 메소드를 구현하는 코드
  }
  void pourInCup() {
    // 메소드를 구현하는 코드
  }
}

public class Tea extends CaffeineBeverage {
  public void brew() {
    System.out.println("차를 우려내는 중");
  }
  public void addCondiments() {
    System.out.println("레몬을 추가하는 중");
  }
}

public class Coffee extends CaffeineBeverage {
  public void brew() {
    System.out.println("필터로 커피를 우려내는 중");
  }
  public void addCondiments() {
    System.out.println("설탕과 커피를 추가하는 중");
  }
}

템플릿 메소드와 후크(hook)!

후크(hook)는 추상 클래스에서 선언되는 메소드긴 하지만 기본적인 내용만 구현되어 있거나 아무 코드도 들어있지 않는 메소드다.
사용 용도는 서브클래스 입장에서 무시하거나 다양한 위치에서 알고리즘에 끼어들 수 있는 역할을 할 수 있다.

사용 예

public abstract class CaffeineBeverageWithHook {
  
  void prepareRecipe() { // final을 선언하지 않는다. hook를 위해
    boilWater();
    brew();          
    pourInCup();
    if(customerWantsCondiments()) {
      addCondiments();  
    }
  }
  
  abstract void brew();
  abstract void addCondiments();
  
  void boilWater() {
    // 메소드를 구현하는 코드
  }
  void pourInCup() {
    // 메소드를 구현하는 코드
  }
  boolean customerWantsCondiments() {
    return true;                          // 서브클래스에서 필요에 따라 오버라이드 할 수 있는 메서드이므로 후크다.
  }
}

public class Coffee extends CaffeineBeverageWithHook {
  public void brew() {
    System.out.println("필터로 커피를 우려내는 중");
  }
  public void addCondiments() {
    System.out.println("설탕과 커피를 추가하는 중");
  }
  
  public boolean customerWantsCondiments() {  // 후크 오버라이드해서 원하는 기능을 집어넣기
    String answer = getUserInput();
    
    if(answer.toLowerCase().startsWith("y")) {
      return true;
    } else {
      return false;
    }
  }
  
  private String getUserInput() {
    String answer = null;
    
    System.out.print("커피에 우유와 설탕을 넣어 드릴까요? (y/n) ");
    
    BufferedReader in = new BufferedReader (new InputStreamReader(System.in));
    try {
      answer = in.readLine();
    } catch (IOException ioe) {
      System.err.println("IO 오류");
    }
    if(answer == null) {
      return "no";
    }
    return answer;
  }
}

-> ★ 핵심 기술 : 디자인 원칙 8. 헐리우드 원칙 - 먼저 연락하지 마세요. 저희가 연락 드리겠습니다.

구성요소가 복잡하게 꼬여있는 경우를 의존성 부패(dependency rot)라고 하는데 이를 방지 할 수 있다.
의존성이 부패되면 시스템이 어떤 식으로 디자인된 것인지 거의 아무도 알아볼 수 없게 된다.

고수준 구성요소 // 하지만 언제 어떤 식으로 쓰이는지는 고수준 구성요소에 의해 결정된다. 
  ↑       ↑
저수준  저수준  // 저수준 구성요소도 컴퓨테이션에 참여할 수 있지만, 절대 고수준 구성요소를 직접 호출할 수 없다.

템플릿 메소드 패턴도 헐리우드 원칙을 고수한다.

템플릿 메소드를 사용해서 실전 1. 정렬

자바의 Arrays 클래스에 mergeSort()를 호출하면 정렬알고리즘이 들어있으며 compareTo()메소드에 의해 결과가 결정된다. 서브 클래스를 만들어서 쓰는 대신 Comparable 인터페이스를 구현해야한다.

템플릿 메소드를 사용해서 실전 2. 스윙 프레임

JFrame은 가장 기본적인 스윙 컨테이너로, paint() 메소드를 상속받는 컨테이너다. 기본적으로 paint() 메소드는 후크 메소드기 때문에 아무 일도 하지 않는다. paint()를 오버라이드하면 JFrame에서 화면의 특정 영역에 어떤 내용을 표시하는 알고리즘에 원하는 그래픽을 추가할 수 있다.

템플릿 메소드를 사용해서 실전 3. 애플릿

Applet 서브클래스를 만들 때 후크가 몇개 들어있다. start(), stop(), repaint() 등..


11. 이터레이터 패턴 (Iterator Pattern)

잘 관리된 컬렉션, 수퍼 컬렉션, 컬랙션 구현 방법을 노출시키지 않으면서도 그 집합체 안에 들어있는 모든 항목에 접근할 수 있게 해 주는 방법을 제공해 준다.

그냥 iterator라는 인터페이스를 구현해서 상속시킨다. (모든 언어에 기본적으로 제공되는 인터페이스 이기도 함) hasNext(); - bool 다음 항목있는지 체크용 next(); - 다음 항목을 가져온다. remove(); - 마지막 next 항목을 제거한다.

구현 예

public interface Iterator {
  boolean hasNext();
  Object next();
}

import java.util.Iterator;

public class DinerMenuIterator implements Iterator {
  MenuItem[] items;
  int position = 0;
  
  public DinerMenuIterator(MenuItem items) {
    this.items = items;
  }
  
  public Object next() {
    MenuItem menueItem = items[position];
    postion = postion + 1;
    return menuItem;
  }
  
  public boolean hasNext() {
    if (position >= items.length || items[position] == null) {
      return false;
    } else {
      return true;
    }
  }
  
  public void remove() {  //java 배열에는 Iterator가 없고 왼쪽으로 밀어줘야하니까 
    if(position <= 0) {
      throw new IllegalStateException("next()를 한번도 호출하지 않은 상태에서는 삭제할 수 없습니다.");
    }
    if(list[position-1] != null) {
      for(int i= position-1; i<list.length-1); i++) {
        list[i] = list[i+1];
      }
      list[list.length-1] = null;
    }
  }
}

public interface Menu {
  public Iterator createIterator();
}

public class DinerMenu implements Menu {
  static final int MAX_ITEM = 6;
  int numberOfItems = 0;
  MenuItem[] menuItems;
  
  public Iterator createIterator() {
    return new DinerMenuIterator(menuItems);
  }
}

public class CafeMenu implements Menu {
  Hashtable menuItems = new Hashtable();
  
  public CafeMenu() {
    // 생성자 코드
  }
  
  public void addItem(String name, String description, boolean vegetarian, double price)
  {
    MenuItem menuItem = new MenuItem(name, description, vegetarian, price);
    menuItems.put(menuItem.getName(), menuItem);
  }
  
  public Iterator createIterator() {
    return menuItems.values().iterator(); // Hashtable 전체에 대한 반복자가 아닌 값들에 대한 반복자를 리턴한다는 점
  }
}

import java.util.Iterator;

public class Waitress {
  Menu pancakeHouseMenu;
  Menu dinerMenu;
  Menu cafeMenu;
  
  public Waitress (Menu pancakeHouseMenu, Menu dinerMenu, Menu cafeMenu) {
    this.pancakeHouseMenu = pancakeHouseMenu;
    this.dinerMenu = dinerMenu;
    this.cafeMenu = cafeMenu;
  }
  
  public void printMenu() {
    Iterator pancakeIterator = pancakeHouseMenu.createIterator();
    Iterator dinerIterator = dinerMenu.createIterator();
    Iterator cafeIterator = cafeMenu.createIterator();
    System.out.println("메뉴\n----\n아침 식사");
    printMenu(pancakeIterator);
    System.out.println("\n점심 식사");
    printMenu(dinerIterator);
    System.out.println("\n저녁 식사");
    printMenu(cafeIterator);
  }
  
  private void printMenu(Iterator iterator) {
    while(iterator.hasNext()) {
      MenuItem menuItem = (MenuItem) iterator.next();
      System.out.print(menuItem.getName() + ", ");
      System.out.print(menuItem.getPrice() + " -- ");
      System.out.print(menuItem.getDescription());
    }
  }
}

장점

내부적인 구현 방법을 외부로 노출시키지 않으며 집합체에 있는 모든 항목에 일일이 접근 가능하다. 일일이 접근하는 작업을 컬렉션 객체(집합체)가 아닌 반복자 객체에서 맡게 된다.

-> ★ 핵심 기술 : 디자인 원칙 9. 클래스를 바꾸는 이유는 한 가지 뿐이어야 한다.


12. 컴포지트 패턴 (Composite Pattern)

객체들을 트리 구조로 구성하여 부분과 전체를 나타내는 계층구조로 만들 수 있다. 이 패턴을 이용하면 클라이언트에서 개별 객체와 다른 객체들로 구성된 복합 객체(composite)를 똑같은 방법으로 다룰 수 있다.

  • 잎 노드와 복합 노드 모두에서 사용할 수 있는 컴포넌트 추상 클래스를 만든다.
  • 잎 노드 클래스와 복합 노드 클래스에서 컴포넌트 클래스를 구현한다.

구현 예

public abstract class MenuComponent {
  public void add(MenuComponent menuComponent) {
    throw new UnsupportedOperationException();
  }
  public void remove(MenuComponent menuComponent) {
    throw new UnsupportedOperationException();
  }
  public MenuComponent getChild(int i) {
    throw new UnsupportedOperationException();
  }
  // 여기까지는 MenuComponent를 추가하고 제거하고 가져오기 위한 메소드
  
  public String getName() {
    throw new UnsupportedOperationException();
  }
  public String getDescription() {
    throw new UnsupportedOperationException();
  }
  public double getPrice() {
    throw new UnsupportedOperationException();
  }
  public boolean isVegetarian() {
    throw new UnsupportedOperationException();
  }
  
  public void print() {
    throw new UnsupportedOperationException();
  }
  
  public Iterator createIterator() {  // 이터레이터 패턴을 위함
    throw new UnsupportedOperationException();
  }
}

public class MenuItem extends MenuComponent { // 잎 에 해당되는 
  String name;
  String description;
  boolean vegetarian;
  double price;
  
  public MenuItem(String name, String description, boolean vegetarian, double price) {
    this.name = name;
    this.description = description;
    this.vegetarian = vegetarian;
    this.price = price;
  }
  
  public String getName() {
    return name;
  }
  
  public String getDescription() {
    return description;
  }
  
  pubilc double getPrice() {
    return price;
  }
  
  public boolean isVegetarian() {
    return vegetarian;
  }
  
  public void print() {
    System.out.print(" " + getName());
    if(isVegetarian()) {
      System.out.print("(v)");
    }
    System.out.println(", " + getPrice());
    System.out.println("   -- " + getDescription());
  }
  
  public Iterator createIterator() {  
    return new NullIterator();  // 널 반복자 1. null을 리턴하거나 2. haxNext()가 호출되었을 때 무조건 false를 리턴하는 반복자를 리턴
  }
}

public class Menu extends MenuComponent { // 복합 객체 클래스에 해당한다.
  ArrayList menuComponents = new ArrayList();
  String name;
  Stirng description;
  
  public Menu(String name, String description) {
    this.name = name;
    this.description = description;
  }
  
  public void add(MenuComponent menuComponent) {
    menuComponents.add(menuComponent);
  }
  
  public void remove(MenuComponent menuComponent) {
    menuComponents.remove(menuComponent);
  }
  
  public MenuComponent getChild(int i) {
    return (MenuComponent) menuComponets.get(i);
  }
  
  public String getName() {
    return name;
  }
  
  public String getDescription() {
    return description;
  }
  
  public void print() {
    System.out.print("\n"+ getName());
    System.out.print(", " + getDescription());
    System.out.print("---------------------");
    
    Iterator iterator = menuComponents.iterator();
    while(iterator.hasNext()) {
      MenuComponent menuComponent = (MenuComponent) iterator.next();
      menuComonent.print();
    }
  }
  
  public Iterator createIterator() {   
    return new CompositeIterator(menuComponents.iterator()); // CompositeIterator라는 새로운 반복자를 사용해 복합 객체에 대해서도 반복작업을 할 수 있게 한다.
  }
}

public class Waitress { 
  MenuComponent allMenus;
  
  public Waitress(MenuComponent allMenus) {
    this.allMenus = allMenus;
  }
}

public class MenuTestDrive {
  public static void main(String args[]) {
    MenuComponent pancakeHouseMenu = new Menu("팬케이크 하우스 메뉴", "아침 메뉴");
    MenuComponent dinerMenu = new Menu("객체마을 식당 메뉴", "점심 메뉴");
    MenuComponent cafeMenu = new Menu("카페 메뉴", "저녁 메뉴");
    MenuComponent dessertMenu = new Menu("디저트 메뉴", "디저트를 즐겨 보세요!");
    
    MenuComponent allMenus = new Menu("전체 메뉴", "전체 메뉴");
    
    allMenus.add(pancakeHouseMenu);
    allMenus.add(dinerMenu);
    allMenus.add(cafeMenu);
    
    dinerMenu.add(new MenuItem("파스타", "마리나라 소스 스파게티. 효모빵도 드립니다.", true, 3.89));
    dinerMenu.add(dessertMenu);
    
    dessertMenu.add(new MenuItem("애플 파이", "바삭바삭한 크러스트에 바닐라 아이스크림이 얹혀 있는 애플 파이", true, 1.59));
    
    // 메뉴 추가 생략..
    
    Waitress waitress = new Waitress(allMenus);
    waitress.printMenu();
  }
}

장점

객체의 구성과 개별 객체를 노드로 가지는 트리 형태로 객체를 구축할 수 있다.
복합 구조를 사용하면 복합 객체와 개별 객체에 대해 똑같은 작업을 적용할 수 있다. 즉, 대부분의 경우에 복합 객체와 개별 객체를 구분할 피룡가 없어진다.


13. 타입 객체 패턴 (Type Object Pattern)

타입 객체 패턴은 언어에 제약된게 아닌데 C++ 에 대한 정보만 있고 C#에 대한 국내 정보는 없어서 적는다.

이 패턴은 간단한 게임개발에서 엄청 유용한 패턴이다.
게임 내의 정보들을 정의할 때 쓴다.
무기 아이템이 있고 여러 타입별(둔기, 활, 검)로 아이템이 있으면 상속으로 여러 클래스를 구현하는게 아니라 타입 클래스를 1개 만들어서 해당 타입 객체에 있는 정보를 가져오는 식으로 구현한다.
예를 들어 유니티엔진에서 Scriptable Object로 타입 클래스를 만들고 무기 클래스에서 해당 타입의 스크립트를 가져다 쓰는 식으로 구현할 수 있다.

장점

값을 빼서 처리할 수 있으므로 기획자와 소통이 편해진다. 변경사항이 많을 때 유리하다. 컴파일이나 코드 변경 없이 새로운 타입을 추가하거나 변경할 수 있다.

단점

코드가 비효율 적이다. 상속을 활용하기 힘들다.

c# 예제 코드

/// Weapon.cs 타입을 사용하는 클래스(Typed Class)

using UnityEngine;

public class Weapon : MonoBehaviour
{
  [SerializeField]
  private WeaponData weaponData;
  
  ~~~
  
  public void Attack(Target target)
  {
    if(weaponData.Damage > 0) target.TakeDamage(weaponData.Damage);
    if(weaponData.StunDuration > 0) target.Stun(weaponData.StunDuration);
    if(weaponData.FreezeDuration > 0) target.Freeze(weaponData.FreezeDuration);
    
    ~~~
  }
}

/// WeaponData.cs 타입 클래스(Type Class)

using UnityEngine;

[CreateAssetMenu(menuName = "Weapon Data")]
public WeaponData : ScriptableObject 
{
  public int Damage;
  public string Message;
  pubilc GameObject Model;
  public int StunDuration;
  public int FreezeDuration;
}

추천 한마디

게임 내부의 데이터(레벨 수치, 몬스터 정보, 아이템 정보 등등)는 모두 이 패턴을 사용해서 (Unity엔진을 사용한다면 ScriptableObject로) 구현하는게 좋다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment