본문 바로가기

Design Pattern

[디자인 패턴_Again] Observer Pattern.

옵저버패턴은 어떠한 상태가 변화되었을 때 그것을 통보해주는 녀석과
통보를 받고 알아서 행동하는 녀석들이 있습니다.
여기서 이 통보를 받고 각자의 행동을 하는 녀석들이 observer입니다.

앞서 잠깐 언급한 와우 짝퉁으로 다시 돌아가서
앞을 정찰하는 Watcher가 있고, 이녀석이 적을 만났을 때 각각의 캐릭터들에게
"적이다!" 라고 알려줍니다.

각각의 캐릭터들은 적을 만났을때, 각자의 행동을 합니다. 예를 들면

전사는 칼과 방패를 들고 맨 전열로 나가고
성직자는 다른 캐릭터들에게 축복을 걸고
도적은 그림자 숨기를 시전합니다.
성기사는 전사와 같이 전열로 나가겠죠.

물론 게임이라면 유저가 컨트롤 하는 것이겠지요..^^

이렇게 적이 나타났다고 알려주는 역할을 하는 녀석(Watcher)는 자기가 가지고 있는 옵저버들에게
통보만 해줄 뿐 실제로 옵저버들이 무슨 역할을 하는지까지는 관여하지 않습니다.

그러면 이렇게 Watcher의 역할을 하는 녀석을 만들어보겠습니다.

public interface Watcher {
 public void registerObserver(Observer o);
 public void removeOserver(Observer o);
 public void notifyObservers();
}

인터페이스로 3개의 메서드를 가지고 있습니다.
Watcher가 가져야 할 기본적인 행동이죠..
registerObserver는 자기에게 observer리스트를 추가하는 것이고
removerObserver는 등록된 observer를 삭제합니다.
notifyObservers는 바로 "적이다!" 라고 알려주는 역할을 하지요..

그러면 이 Watcher의 기본적인 역할을 구현한 클래스를 만들어 보겠습니다.

import java.util.ArrayList;

public class BeteranWatcher implements Watcher {
 
 private ArrayList<Observer> observers = new ArrayList<Observer>();
 
 @Override
 public void notifyObservers() {
  for(Observer observer : observers) {
   observer.update();
  }
 }

 @Override
 public void registerObserver(Observer o) {
  observers.add(o);
 }

 @Override
 public void removeOserver(Observer o) {
  int idx = observers.indexOf(o);
  if(idx > 0) observers.remove(idx);
 }
}

코드 자체는 어려울 것이 없습니다.
notifyObservers를 보시면 자기에게 등록되어 있는 observer들의 update를 호출합니다.
상태가 바뀌었으니 (적이 나타났으니) 옵저버들에게 행동하라는 것이죠.
무슨 행동인지는 신경쓰지 않습니다.

그렇다면 이제 이 행동을 할 캐릭터들을 만들어 보겠습니다.

일단, 위 Watcher에 등록 할 수 있도록 Observer 인터페이스를 만듭니다.

public interface Observer {
 public void update();
}

그리고 각 캐릭터들의 행동을 정의 할 Character 인터페이스도 만듭니다.
public interface Character {
 public void action();
}

여기에 등장하는 캐릭터들은 Character 인터페이스를 구현하고 각자의 행동인 action 메서드를 구현해야 합니다.

이제 각각의 캐릭터들을 만들어 보겠습니다.

public class Paladin implements Character , Observer {

 public Paladin(Watcher watcher) {
  watcher.registerObserver(this);
 }
 
 @Override
 public void update() {
  System.out.println("적의 출현을 보고 받았고, 나갈 준비를 합니다.");
  action();
 }
 
 @Override
 public void action() {
  System.out.println("성기사는 칼과 방패를 들고 앞으로 나간다.");
 }
}

-----------------------------------------

public class Priest implements Character , Observer {

 public Priest(Watcher watcher) {
  watcher.registerObserver(this);
 }
 
 @Override
 public void update() {
  System.out.println("적의 출현을 보고 받았고, 축복을 시전 할 준비를 합니다.");
  action();
 }
 
 @Override
 public void action() {
  System.out.println("성직자는 축복을 시전합니다.");
 }
}

--------------------------------------

public class Thief implements Character , Observer {
 public Thief(Watcher watcher) {
  watcher.registerObserver(this);
 }
 
 @Override
 public void update() {
  System.out.println("적의 출현을 보고 받았고, 그림자 숨기를 시전 할 준비를 합니다.");
  action();
 }
 
 
 @Override
 public void action() {
  System.out.println("도적은 그림자 숨기를 시전합니다.");
 }
}
----------------------------
public class Warrior implements Character , Observer {
 public Warrior(Watcher watcher) {
  watcher.registerObserver(this);
 }
 
 @Override
 public void update() {
  System.out.println("적의 출현을 보고 받았고, 싸울 준비를 합니다.");
  action();
 }
 
 @Override
 public void action() {
  System.out.println("전사는 칼과 방패를 들고 앞으로 나간다.");
 }
}
각각의 캐릭터들은 watcher가 적이 나타났다고 알려줄 때 호출 할 update 메서드를 가지고 있고
이때의 행동을 위해 action 메서드를 구현하고 있습니다.

즉, watcher는 적이 나타났을때 Observer의 update를 호출하고, 이는 곧
character의 action을 호출하도록 되어있는 것입니다.
그리고, 각각의 캐릭터 인스턴스를 생성하였을 때 자기 자신을 watcher에게 등록하기 위하여
생성자에서 Watcher를 받아서 watcher.registerObserver(this)를 호출 해 자기 자신을
등록시키고 있습니다.

만약의 Watcher의 종류가 여러가지라면 마찬가지로 Watcher 인터페이스를 구현한 다른 Watcher를 구현하고, 이를 이용하면 되겠지요.. Strategy 패턴과 비슷하게 말이죠..

그러면 이것을 테스트해보겠습니다.

public class Test {
 public static void main(String[] args) {
  Watcher watcher = new BeteranWatcher();
  Character paladin = new Paladin(watcher);
  Character warrior = new Warrior(watcher);
  Character priest = new Priest(watcher);
  Character thief = new Thief(watcher);
 
  watcher.notifyObservers();
 }
}

티스토리 새로운 에디터 정말 개판이네요 -_-;;

아무튼 테스트 코드를 보시면 아시겠지만, watcher는 적이나타났다고 알려주기만 합니다.

이런식으로 옵저버들에게 일괄적으로 상태의 변화를 밀어넣는 방식을 push 방식이라고 합니다.

나머지 행동은 각각의 Observer들이 알아서 하죠. 결과는..

적의 출현을 보고 받았고, 나갈 준비를 합니다.
성기사는 칼과 방패를 들고 앞으로 나간다.
적의 출현을 보고 받았고, 싸울 준비를 합니다.
전사는 칼과 방패를 들고 앞으로 나간다.
적의 출현을 보고 받았고, 축복을 시전 할 준비를 합니다.
성직자는 축복을 시전합니다.
적의 출현을 보고 받았고, 그림자 숨기를 시전 할 준비를 합니다.
도적은 그림자 숨기를 시전합니다.



라고 나옵니다.

이것은 객체적인 관점에서 보면 역시 각자 할일은 각자가 알아서 하는 겁니다.
Watcher는 통보만 해주면 되고, Observer는 통보받고 자기 할 일만 하면 되는거죠...
여기서 이 할 일이라는 것이 변경이 되는 부분이라고 하면 앞서 이야기 했던
Strategy 패턴을 적용 할 수도 있을 것입니다.

그런데 Observer 패턴을 적용 하는 방법이 한가지 더 있습니다.
바로 JDK 내장 옵저버 패턴을 사용하는 것입니다.
이 내장패턴에는 이미 준비되어 있는 Observer 인터페이스와 Observable 클래스를 사용합니다.

그리고 이 jdk 내장 옵저버 패턴은 위의 push 방식이 아니라 pull 방식을 사용합니다.

즉 observer들이 자기가 필요 할 때 그 상태를 가져가겠다는 겁니다.

위 각각의 캐릭터들이 Observer이고 Watcher Observable입니다.

간단하게 구현해보겠습니다.

import java.util.Observable;

public class BeteranWatcher extends Observable {
 
 private String enemyType;
 private String distanceFromEnemy;
 
 public void enemyFound() {
  enemyType = "트롤";
  distanceFromEnemy = "100미터";
  setChanged();
  notifyObservers();
 }
 
 public String getEnemyType() {
  return enemyType;
 }

 public String getDistanceFromEnemy() {
  return distanceFromEnemy;
 }
}

이제 Watcher는 java.util.Observable을 상속받아 사용합니다.
첫번째 예제처럼 observer를 add하는 부분은 사라지고 단지 어떤 상태가 변하였을때
그 상태가 변했다고 알려줄 수 있는 setChanged() 메서드와
실제로 Observer들에게 통보를 하는 notifyObservers() 메서드를 사용하고 있습니다.

그외 적종류라던가 적과의 거리 같은 내부 데이터를 추가로 넣어봤습니다.

pull방식을 설명드리기 위해서입니다.

그러면 이제 Observer에 해당하는 전사 클래스를 하나 만들어보겠습니다.

public interface Character {
 public void action();
}


 

import java.util.Observable;
import java.util.Observer;

public class Warrior implements Observer, Character {

 Observable observable;
 private String type;
 private String distance;
 
 public Warrior(Observable o) {
  this.observable = o;
 }
 @Override
 public void update(Observable o, Object arg) {
  if(o instanceof BeteranWatcher) {
   BeteranWatcher watcher = (BeteranWatcher)o;
   type = watcher.getEnemyType();
   distance = watcher.getDistanceFromEnemy();
   action();
  }
 
 }

 @Override
 public void action() {
  System.out.println("적 ["+type+"], 거리["+distance+"] 칼을 들고 앞으로 나간다!");
 
 }

}

전사 클래스는 java.util.Observer를 구현하고 있습니다.
update(Observable o, Object arg)를 구현하면 Watcher로 부터 상태 변화를 통보받고
무언가 액션을 할 수 있습니다.

앞선 예제에서는 단지 통보만을 해줬지만 이 예제에서의 update 메서드는
Watcher에 해당하는 Observable 이 같이 넘어옵니다. 이것을 가지고 Watcher의 내부 데이터에
접근 할 수 있는 기회가 생기는 것이죠..
즉, 상태가 변했을 경우 그 상태가 변한 것 중 각각의 Observer가 자기에게 필요한 데이터만
가져다 쓸 수 있게.. 혹은 자기가 무언가 액션을 해야하는 상태가 변했을 때만
작업을 할 수 있게 해줍니다. 이것이 pull 방식입니다.

그리고 중요한 것 하나는..
Observer에게 연락이 가는 순서에 의존해서는 안됩니다.

Observer1,2,3,4가 있다고 했을때 이 Observer들에게 연락이 가는 순서가 일정치 않기 때문입니다.