본문 바로가기
Lecture

자바 디자인 패턴: 상태 패턴, 옵저버 패턴, 메멘토 패턴, 파사드 패턴, 중재자 패턴

by Renechoi 2023. 7. 6.

1.  상태 패턴 State Pattern 

 

 

상태 패턴이란? 

- 상태가 달라지는 변화에 대응하는 패턴

- 클래스가 하나의 상태에 따라 그 내부의 여러 메서드의 기능이 바뀐다고 하면 이를 각각의 클래스로 분리한다.

 

 

 

의도와 동기 

- 객체의 기능은 상태에 따라 달라질 수 있다.

- 상태가 여러가지이고 클래스 전반의 모든 기능이 상태에 의존적이라 하면 상태를 클래스로 표현하는 것이 적절하다.
- 많은 if-else 문을 방지하고 추후 상태 변화에 대응한다. 

 

 

 

 

클래스 다이어그램 

 

언제 어떤 클래스가 쓰일지를 적절하게 switch 해주는 것이 관건이다. 

 

- Context  : ConcreteState의 인스턴스를 관리하고 서로 상태가 바뀌는 순간을 구현할 수 있다.
- State : Context 가 사용할 메서드를 선언한다.
- ConcreateState : 각 상태 클래스가 수행할 State에 선언된 메서드를 구현한다.

 

예시 

 

이전에 템플릿 메서드 패턴에서 다뤘던 예제를 다시 살펴보자. 

 

게임에서 플레이어별로 다른 레벨을 가질 때 이를 다른 상태로 보고 클래스 별로 나눠 다뤄본다. 

 

먼저 플레이어를 다음과 같이 정의한다. 

 

import org.designpattern.templatemethod.BeginnerLevel;
import org.designpattern.templatemethod.PlayerLevel;

public class Player {
   
   private PlayerLevel level;

   public Player() {
      level = new BeginnerLevel();
      level.showLevelMessage();
   }

   public PlayerLevel getLevel() {
      return level;
   }

   public void upgradeLevel(PlayerLevel level) {
      this.level = level;
      level.showLevelMessage();
   }

   public void play(int count) {
      level.go(count);
   }
}

 

플레이어는 레벨이 있다. 

 

public abstract class PlayerLevel {

   public abstract void run();

   public abstract void jump();

   public abstract void turn();

   public abstract void showLevelMessage();

   final public void go(int count) {
      run();
      for (int i = 0; i < count; i++) {
         jump();
      }
      turn();
   }
}

 

이때 플레이어의 레벨을 만약 상태 패턴을 사용하지 않는다면 다음과 같이 작성할 것이다. 

public class Player {

   private PlayerLevel level;
   private int levelNotWithStatePattern; 

   public Player() {
      level = new BeginnerLevel();
      level.showLevelMessage();
      levelNotWithStatePattern=1;
   }

   public PlayerLevel getLevel() {
      return level;
   }

   public void upgradeLevel(PlayerLevel level) {
      this.level = level;
      level.showLevelMessage();
      levelNotWithStatePattern++;
   }

   public void play(int count) {
      level.go(count);
   }
}

 

이런 방식으로 하는 것이 아니라 클래스를 분리해준다. 

 

public class AdvancedLevel extends PlayerLevel{
   @Override
   public void run() {
      System.out.println("advanced level 달리기.");
      
   }

   @Override
   public void jump() {
      System.out.println("advanced level jump.");
   }

   @Override
   public void turn() {
      System.out.println("advanced level Turn.");       
   }

   @Override
   public void showLevelMessage() {
      System.out.println("***** advanced level. *****");
   }
}

 

public class BeginnerLevel extends PlayerLevel{

   @Override
   public void run() {
      System.out.println("천천히 달립니다.");
   }

   @Override
   public void jump() {
      System.out.println("초보자 Jump.");
   }

   @Override
   public void turn() {
      System.out.println("초보자 Turn.");
   }

   @Override
   public void showLevelMessage() {
      System.out.println("***** 초보자 레벨. *****");
   }

}

 

 

이렇게 관리하면 변경에 용이하다. 즉 새로운 레벨이 추가 된다고 하면 클래스를 추가만 해주면 된다. 

 

스위치는 다음과 같이 메서드를 통해 해줄 수 있다. 

 

public static void main(String[] args) {

   Player player = new Player();
   player.play(1);
   AdvancedLevel aLevel = new AdvancedLevel();
   player.upgradeLevel(aLevel);
   player.play(2);
   SuperLevel sLevel = new SuperLevel();
   player.upgradeLevel(sLevel);
   player.play(3);
   
}

 

업그레이드 혹은 다운그레이드는 조건에 따라 다르게 설정해주면 되기 때문에 직접적으로 의존하지 않고도 레벨 변화를 가져올 수 있다. 

 

 

결론 

 

- 상태에 따른 기능을 분리하여 구현
- 새로운 상태가 추가되면 새로운 클래스를 추가한다.
- 각 상태의 switch를 명확하게 구현해 함

 

 

 

2. 옵저버 패턴 Observer Pattern 

 

 

옵저버 패턴이란? 

- 대부분 데이터와 뷰 사이에서 많이 쓰이는 패턴

- MVC 패턴에서 모델과 뷰 관계 

-객체 사이에 일대다의 의존 관계가 있고, 어떤 객체의 상태가 변하면 변화를 통지 -> 지켜보고 있다가 갱신되도록 

 

 

 

의도와 동기 

- 하나의 객체에 연동되는 여러 객체 집합이 있을 때 변화에 대한 일관성은 유지하고, 객체간의 결합도는 낮게하기 위한 패턴
- 변화에 관심이 있는 객체에 대한 가정없이 통보될 수 있도록 해야 함 -> 동시 업데이트 때린다. 
- 주로 data - view 의 관계에서 사용됨
- log와 그 handler들 관계에서도 사용됨 (file, console, 등등)

 

 

 

 

클래스 다이어그램 

 

- Subject : Observer를 알고 있는 주체 -> Observer를 더하거나 뺀다.
- Observer : Subject의 변화에 관심을 가지는 객체
- ConcreteSubject : ConcreteObserver에게 알려주어야하는 상태 변경 통보 (주로 List로 Observer관리)
- ConcreteObserver : 객체에 대한 참조자를 관리하고 Subject가 변경될 때 갱신되는 인터페이스 구현체 

 

 

예시 

 

숫자가 생성될 때 그것을 보여주는 로직을 예시로 들어보자.

 

옵저버가 인터페이스로 설정되어 있고, 구현체들은 상태를 계속 모니터링한다. 

 

다음과 같이 옵저버를 먼저 선언한다. 

 

public interface Observer {
    public abstract void update(NumberGenerator generator);
}

 

NumberGenerator를 받아서 업데이트를 해준다. 

 

public abstract class NumberGenerator {
    private List<Observer> observers = new ArrayList<>();      
    public void addObserver(Observer observer) {    // Observer 추가
        observers.add(observer);
    }
    public void deleteObserver(Observer observer) { // Observer 삭제
        observers.remove(observer);
    }
    public void notifyObservers() {               // Observer 통지
        Iterator<Observer> observers = this.observers.iterator();
        while (observers.hasNext()) {
            Observer o = observers.next();
            o.update(this);
        }
    }
    public abstract int getNumber();                // 수를 취득한다.
    public abstract void execute();                 // 수를 생성한다.
}

 

NumberGenerator는 옵저버를 list로 갖고 있는데 즉 여러 옵저버들을 알고 있다. 

 

이 추상 클래스를 extends한 난수 NumberGenerator는 다음과 같다. 즉, 숫자를 생성하고 이를 notify 하도록 한다. 

 

public class RandomNumberGenerator extends NumberGenerator {
    private Random random = new Random();   
    private int number;                   
    public int getNumber() {                // 수를 취득한다.
        return number;
    }
    public void execute() {
        for (int i = 0; i < 20; i++) {
            number = random.nextInt(50);
            notifyObservers();
        }
    }
}

 

옵저버는 다양한 옵저버가 있을 수 있다. 각기 다른 일을 하도록 구현을 해준다. 

 

public class GraphObserver implements Observer {
    public void update(NumberGenerator generator) {
        System.out.print("GraphObserver:");
        int count = generator.getNumber();
        for (int i = 0; i < count; i++) {
            System.out.print("*");
        }
        System.out.println("");
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
        }
    }
}

 

public class DigitObserver implements Observer {
    public void update(NumberGenerator generator) {
        System.out.println("DigitObserver:" + generator.getNumber());
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
        }
    }
}

 

각기 다른 뷰를 보여주는 옵저버들이다. 

 

이때, Thread.sleep()으로 시간 지연 시뮬레이션을 한다. 옵저버들이 숫자를 통지받은 후 일정 시간 동안 대기하는 것을 시뮬레이션한다. 

또한 옵저버들이 수신한 데이터를 출력하거나 처리하는 시간을 제어하는 것을 보여준다. GraphObserver와 DigitObserver가 숫자를 받은 후 출력하기 전에 일정 시간 동안 대기하여 그래프와 숫자를 순차적으로 출력하도록 하였다. 

 

public static void main(String[] args) {
   NumberGenerator generator = new RandomNumberGenerator();
   Observer observer1 = new DigitObserver();
   Observer observer2 = new GraphObserver();
   generator.addObserver(observer1);
   generator.addObserver(observer2);
   generator.execute();
}

 

 

 

 

결론 

 

- Subject와 Observer간의 추상적인 결합만의 존재
- BroadCast 방식의 교류 -> 데이터가 바뀌었을 때 그걸 알려만 주면 옵저버 쪽에서 알아서 핸들하도록 한다. 
- 데이터와 뷰 사이에 자주 사용되는 방법

 

 

 

 

3.  메멘토 패턴 Memento Pattern 

 

 

메멘토 패턴이란? 

- 객체의 내부 상태를 보관하는 패턴 -> 인스턴스의 값들을 보관한다는 뜻 

- 어느 순간에 이전 상태로 롤백을 해야하는 이유 (스냅샷, 히스토리)에 대해서 원래 상태로 복원하도록 한다. 

- 인스턴스를 복원하기 위해서는 내부 정보에 자유롭게 접근 가능해야 한다.

- 캡슐화 파괴가 일어나지 않도록 주의 ! (외부에 저장을 해야하므로) 

 

 

 

의도와 동기 

- 이전의 상태로 되돌리는 undo
- 했던 작업을 다시 하는 redo
- 기억해야 하는 순간을 저장하는 객체
- 오류를 복구하거나 수행 결과를 취소하기 위한 작업에 사용


 

클래스 다이어그램 

외부에 저장을 해야만 하는 상황에서 Orignator만으로 접근 가능하도록 한다. 저장해야 하는 객체를 Originator라고 한다. 

 

- Memento : Originator 객체의 내부 상태를 필요한 만큼 저장한다. Originator만이 Memento에 접근할 수 있다.
- Originator : Memento를 생성하여 현재 객체의 상태를 저장하고 내부 상태를 복구
- CareTaker (undo mechanism) : 여러 개의 과정이 있을 수 있다. 이를 관리 하도록 함 -> 이때 정보에 접근은 못하고 관리만 하도록 함.

 

 

 

 

예시 

 

주사위를 돌렸을 때 여러가지 액션을 수행하고 이를 스냅샷을 찍어 보관하는 예제를 해보자. 

 

돈을 보관하는 예시를 살펴보자. 

 

먼저 다음과 같은 게임을 수행하는 Gamer가 있다. 

 

public void bet() {
    int dice = random.nextInt(6) + 1;          // 주사위를 던진다.
    if (dice == 1) {                                 // 1-> 돈 증가
        money += 100;
        System.out.println("1-> 돈 증가.");
    } else if (dice == 2) {                         // 2-> 돈 반감
        money /= 2;
        System.out.println("2-> 돈 반감.");
    } else if (dice == 6) {                     // 6-> 과일 받기
        String f = getFruit();
        System.out.println("6 -> 과일 받기.");
        fruits.add(f);
    } else {                                    // 그밖에 -> 아무일도 없음 
        System.out.println("그밖에는 아무 일도 없음.");
    }
}

 

주사위를 던졌을 때 

1-> 돈 증가

2-> 돈 반감

6-> 과일 받기

그밖에 -> 아무일도 없음

과 같은 게임 규칙이 있다. 

 

과일을 얻을 때는 그냥 얻는 것이 아니라 랜덤하게 "맛있다"라는 수식어가 붙는다. 

 

private String getFruit() {                     // 과일 한 개 얻기
    String prefix = "";
    if (random.nextBoolean()) {
        prefix = "맛있다.";
    }
    return prefix + fruitsname[random.nextInt(fruitsname.length)];
}

 

 

메멘토 객체를 다음과 같이 구현한다. 

 

public class Memento {
    int money;
    ArrayList<String> fruits;
    public int getMoney() {
        return money;
    }
    Memento(int money) {
        this.money = money;
        this.fruits = new ArrayList<>();
    }
    void addFruit(String fruit) {
        fruits.add(fruit);
    }
}

 

메멘토가 생성되는 부분에서는 필요한 인자값을 넣어주되, 과일의 경우 맛있는 것만 보존하도록 해준다. 스냅샷이라고도 표현할 수 있다.

 

이처럼 스냅샷은 필요한 정보들을 말그대로 보관해주도록 인자값들을 갖고 객체를 만들어주면 된다. 

 

public Memento createMemento() {                    // 스냅샷 찍기
    Memento memento = new Memento(money);
    Iterator<String> fruits = this.fruits.iterator();
    while (fruits.hasNext()) {
        String fruit = fruits.next();
        if (fruit.startsWith("맛있다")) {         // 과일은 맛있는 것만 보존
            memento.addFruit(fruit);
        }
    }
    return memento;
}

 

 

그럼 이를 어떻게 사용할까? 

다음과 같이 필요에 따라 어느 시점에 스냅샷을 찍거나 되돌리게 구현해준다. 

// Memento의 취급 결정
if (gamer.getMoney() > memento.getMoney()) {
    System.out.println("    (많이 증가했으니 현재의 상태를 보존해두자)");
    memento = gamer.createMemento();
    history.add(memento);
} else if (gamer.getMoney() < memento.getMoney() / 2) {
    System.out.println("    (많이 줄었으니 이전의 상태로 복귀하자)");
    gamer.restoreMemento(memento);
    
}

 

전체 게임 진행 로직은 다음과 같다. 

 

public class MementoMain {
    public static void main(String[] args) {
        Gamer gamer = new Gamer(100);
        Memento memento = gamer.createMemento();
        ArrayList<Memento> history = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            System.out.println("==== " + i);
            System.out.println("현 상태:" + gamer);

            gamer.bet();

            System.out.println("돈은" + gamer.getMoney() + "원이 되었습니다.");

            // Memento의 취급 결정
            if (gamer.getMoney() > memento.getMoney()) {
                System.out.println("    (많이 증가했으니 현재의 상태를 보존해두자)");
                memento = gamer.createMemento();
                history.add(memento);
            } else if (gamer.getMoney() < memento.getMoney() / 2) {
                System.out.println("    (많이 줄었으니 이전의 상태로 복귀하자)");
                gamer.restoreMemento(memento);
                
            }

            // 시간을 기다림
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
            System.out.println("");
        }
    }
}

 

 

 

 

 

 

 

결론 

 

- 바둑 같은 경우를 생각해보면 쉽다. 보드라는 객체가 있고 여러 일들이 있을 때 플레이어가 게임을 하는 히스토리, 정보를 알고 있어야 한다.

- 필요한 정보만 저장했다가 되돌릴 수 있도록 함

- 복잡한 Originator 클래스의 내부 상태를 다른 객체로 분리함으로써 상태에 대한 캡슐화를 보장
- 복구에 필요한 상태만 따로 관리함으로써, Originator 내부에서 저장하지 않고 Originator를 단순하게 함 
- Memento의 사용에 오버헤드가 발생할 수 있다.

 

 

 

 

4.  파사드 패턴 Facade Pattern 

 

 

파사드 패턴이란? 

- 복잡한 연결을 단순하고 간단하게 처리하는 패턴 

- 간단한 창구

- 내부 적으로 여러 서브 모듈들이 있을 때, 다양한 매개변수 등 흐름이 복잡할 때 

- 통합된 인터페이스로 편하게 창구 역할을 해주는 것 

 

 

 

의도와 동기 

- 서브시스템을 합성하여 사용하는 다수 객체의 집합에 대한 하나의 일관된 인터페이스를 제공함으로써 사용하기 쉽게 한다.

 

즉, 내부에서 알아서 돌아가고 창구 처럼 열어만 준다-> 클라이언트는 결과만 받도록 함 

 

컴파일러를 생각해보면 된다.  javac를 입력하면 알아서 컴파일된 결과물을 주는데, 내부에서는 아주 많은 일이 돌아간다. 

 

 

 

클래스 다이어그램 

 

- Facade : 하나의 일관된 인터페이스 제공

 

 

 

 

결론 

 

- 복잡한 서브시스템들에 대한 단순하고 기본적인 인터페이스를 앞에서 제공
- 클라이언트와 서브시스템간의 결합도를 줄임

 

 

 

 

4.  중재자 패턴 Mediator Pattern 

 

 

중재자 패턴이란? 

- 마찬가지로 단순하게 처리하기 위한 패턴이다.

- 객체간의 상호 작용을 하나의 객체에서 캡슐화하여 처리
- UI 프로그래밍에서 많이 사용되는 방법으로 Widget 간의 상호 처리를 서로간에 처리하는 것이 아닌 한 객체가 전담하여 처리하도록 하는 방식 -> 콤보 박스에 클릭 이벤트가 발생하면 어떤 액션을 해주어야 한다는 등... 이런 모든 것을 각 위젯이 하면 복잡해진다. 
- 객체 서로간의 메세지를 전달할 일이 있을 때도 중재자를 두고 전달할 수 있음
- N:N의 관계를 1:N의 관계로 바꿀 수 있음 (counselor)

 

-> 한마디로 중앙에서 처리. 

-> 캡슐화의 문제가 있다. 

 

 

 

의도와 동기 

- 객체 지향 방법론에서는 객체 관련된 처리는 객체 내부에서 하는것이 맞지만, 그렇게 하면 상호작용의 급증이 발생하고 시스템의 변경이 어려워질 수 있다. 
- Mediator 객체가 상호작용을 제어하고 조율하게 함. 각 객체는 Mediator만 알면 된다. 


 

 

클래스 다이어그램 

 

 

 

- Mediator : Colleague 객체와 교류하는데 필요한 인터페이스를 정의
- ConcreteMediator : Colleague간의 이루어지는 협력을 구현하고, 자신의 Colleague들을 관리
- Colleague : Mediator만 알고 있으면 되며, 다른 객체와의 협력이 필요할때 Mediator에게 알림

 

대화 상자가 열리면 그 위에 여러 위젯들이 올라가게 되는데 그 상호작용은 중재자가 해주면 된다. 

 

예시 

 

실제로 UI 프로그래밍에서 많이 사용이 되는데, JAVA UI를 간단히 살펴보자. 

 

 

 

텍스트 필드를 살펴보면 Mediator를 참조하면서, 변화에 대해서 Mediator에게 통지해준다. 

 

public class ColleagueTextField extends TextField implements TextListener, Colleague {
    private Mediator mediator;
    public ColleagueTextField(String text, int columns) {   // 생성자
        super(text, columns);
    }
    public void setMediator(Mediator mediator) {            // Mediator를 보관
        this.mediator = mediator;
    }
    public void setColleagueEnabled(boolean enabled) {      // Mediator가 유효/무효를 지시한다.
        setEnabled(enabled);
        setBackground(enabled ? Color.white : Color.lightGray);
    }
    public void textValueChanged(TextEvent e) {             // 문자열이 변하면 Mediator에게 통지
        mediator.colleagueChanged(this);
    }
}

 

이때 Mediator는 해당 위젯들을 포함하고 있는 Frame이 된다. 이 프레임에서 위젯들을 제어해주는 것이다. 

 

public class LoginFrame extends Frame implements ActionListener, Mediator {
    private ColleagueCheckbox checkGuest;
    private ColleagueCheckbox checkLogin;
    private ColleagueTextField textUser;
    private ColleagueTextField textPass;
    private ColleagueButton buttonOk;
    private ColleagueButton buttonCancel;

    // 생성자
    // Colleague들을 생성해서 배치한 후에 표시를 실행한다.
    public LoginFrame(String title) {
        super(title);
        setBackground(Color.lightGray);
        // 레이아웃 매니저를 사용해서 4*2의 그리드를 만든다.
        setLayout(new GridLayout(4, 2));
        // Colleague들의 생성
        createColleagues();
        // 배치
        add(checkGuest);
        add(checkLogin);
        add(new Label("Username:"));
        add(textUser);
        add(new Label("Password:"));
        add(textPass);
        add(buttonOk);
        add(buttonCancel);
        // 유효/무효의 초기지정
        colleagueChanged(checkGuest);
        // 표시
        pack();
        show();
    }
   
    // Colleague들을 생성한다.
    public void createColleagues() {
        // 생성
        CheckboxGroup g = new CheckboxGroup();
        checkGuest = new ColleagueCheckbox("Guest", g, true);
        checkLogin = new ColleagueCheckbox("Login", g, false);
        textUser = new ColleagueTextField("", 10);
        textPass = new ColleagueTextField("", 10);
        textPass.setEchoChar('*');
        buttonOk = new ColleagueButton("OK");
        buttonCancel = new ColleagueButton("Cancel");
        // Mediator의 세트
        checkGuest.setMediator(this);
        checkLogin.setMediator(this);
        textUser.setMediator(this);
        textPass.setMediator(this);
        buttonOk.setMediator(this);
        buttonCancel.setMediator(this);
        // Listener의 세트
        checkGuest.addItemListener(checkGuest);
        checkLogin.addItemListener(checkLogin);
        textUser.addTextListener(textUser);
        textPass.addTextListener(textPass);
        buttonOk.addActionListener(this);
        buttonCancel.addActionListener(this);
    }

    // Colleague로부터의 통지로 각 Colleague의 유효/무효를 판정한다.
    public void colleagueChanged(Colleague c) {
        if (c == checkGuest || c == checkLogin) {
            if (checkGuest.getState()) {             // Guest 모드
                textUser.setColleagueEnabled(false);
                textPass.setColleagueEnabled(false);
                buttonOk.setColleagueEnabled(true);
            } else {                                 // Login 모드
                textUser.setColleagueEnabled(true);
                userpassChanged();
            }
        } else if (c == textUser || c == textPass) {
            userpassChanged();
        } else {
            System.out.println("colleagueChanged:unknown colleague = " + c);
        }
    }
    // textUser 또는 textPass의 변경이 있었다.
    // 각 Colleague의 유효/무효를 판정한다.
    private void userpassChanged() {
        if (textUser.getText().length() > 0) {
            textPass.setColleagueEnabled(true);
            if (textPass.getText().length() > 0) {
                buttonOk.setColleagueEnabled(true);
            } else {
                buttonOk.setColleagueEnabled(false);
            }
        } else {
            textPass.setColleagueEnabled(false);
            buttonOk.setColleagueEnabled(false);
        }
    }
    public void actionPerformed(ActionEvent e) {
        System.out.println("" + e);
        System.exit(0);
    }
}

 

 

colleagueChanged()를 각 위젯들이 호출하는데, 그에 대한 변경 사항을 중재자인 Frame이 실행한다. 

 

 

 

결론 

 

- 다대다의 관계를 일대다의 관계로 축소하여 이해하고 유지보수가 쉬워진다는 것이 핵심이다. 

- 다른 객체사이에 분산된 객체의 연관관계를 하나의 객체로 국한한다.
- Colleague 객체들 간의 종속성이 약화되어 결합도가 줄어든다.


 


참고자료

- 패스트 캠퍼스 (박은종의 객체지향 설계를 위한 디자인패턴 with 자바)

반응형