'자바 패턴'에 해당되는 글 12건

  1. 2009.01.14 [디자인 패턴_Again] Observer Pattern.
  2. 2009.01.10 [디자인 패턴_Again] Strategy 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들에게 연락이 가는 순서가 일정치 않기 때문입니다.



 

Posted by 용식

우리는 클래스를 설계 할 때 클래스 상속이란 것을 사용합니다.

클래스를 상속하면 클래스의 행동(메소드)와 상태(클래스변수등)를 상/하위 클래스들이

공유를 할 수 있습니다.


만약 와우 짝퉁을 만든다고 하겠습니다.
 
처음이기 때문에 간단한 직업을 3개만 우선 만들어보기로 합니다.

전사/도적/성기사죠. 이들은 모두 걷거나 뛰어서 이동하고 칼로 근접 전투를 합니다.

그래서 상위 클래스를 만들어서 "걷다" "뛰다" "액션" 라는 행동을 정의하고

그것을 상속받아서 각각의 하위 클래스에서 공격이 조금 다른 직업일 경우 이를 override 하기로

했습니다.

                           
                            Character
                               |
          Warrior        Thief       Paladin

이렇게 설계를 했다고 합시다.

많은 다른 좋은 방법이 있겠지만 일단 이렇게 -_-;;

Character는 걷다,뛰다,액션라는 행동을 가지고 있습니다.

그리고 각 캐릭터의 이름을 설정하고 가져 올 수 있는 getter와 setter를 추가합니다.

public class Character {
 String name;
 
 public void walk() {
  System.out.println("두발로 걷다.");
 }
 
 public void run() {
  System.out.println("두발로 뛰다");
 }
 
 public void setName(String name) {
  this.name = name;
 }
 
public String getName() {
 return name;
}

 public void action() {
   System.out.println("뭔가 한다.");
 }
}
그리고 이제 이 클래스를 상속받아서 직업 클래스를 만듭니다.

public class Paladin extends Character {
 @Override
 public void action() {
  System.out.println("방패로 공격");
 }
}


public class Thief extends Character {
 @Override
 public void action() {
  System.out.println("단검으로 공격");
 }
}


public class Warrior extends Character {
 
 @Override
 public void action() {
  System.out.println("장검으로 공격");
 }
}


한번 실행을 해보겠습니다.

package pattern.strategy;

public class Test {
 public static void main(String[] args) {
  Character a = new Paladin();
  Character b = new Thief();
  Character c = new Warrior();
 
  a.setName("인간성기사뿌뿌뿡");
  b.setName("인간도적");
  c.setName("타우렌전사");
 
  System.out.print(a.getName() + " : ");
  a.action();
  System.out.print(b.getName() + " : ");
  b.action();
  System.out.print(c.getName() + " : ");
  c.action();
 }
}



인간성기사뿌뿌뿡 : 방패로 공격
인간도적 : 단검으로 공격
타우렌전사 : 장검으로 공격


이렇게 하면 원하는대로 구현이 가능합니다. 상속을 잘 이용했다고도 생각 할 수 있습니다.

그런데 이 상태에서 하나의 캐릭터를 더 추가하려고 합니다.

스타에서 "메딕"의 역할을 하는 "성직자" 캐릭터입니다.

이 성직자의 액션은 적을 공격하는 대신 아군인지 확인하고 아군일 경우

아군의 체력을 치료해주는 일을 합니다.

일단, 위의 다른 캐릭터 클래스들 처럼 만들어보겠습니다.

public class Priest extends Character {

 @Override
 public void action() {
  System.out.println("치료를 합니다");
 }
}


이렇게 작성을 해도 작동은 제대로 합니다. 분명히 이렇게 클래스를 만드는 것도 나쁜 방법은 아니지만, 매번 하나의 새로운 캐릭터가 나올 때 마다 하위에서 오버라이드를 해주는 것보다 더 좋은 방법은 없는 것일까 고민을 할 수 있습니다. 그래서  이것을 이제 Strategy 패턴으로 바꿔보겠습니다.

패턴은 인터페이스를 이용합니다.

이 패턴의 핵심은 서로가 다르게 행동하는 것을 인터페이스로 추출하여 다른쪽과 분리시키는 것입니다. 그 행동 자체를 분리시켜서 행동을 필요할 때마다 알맞은 것으로 갈아끼우는 형식이죠.

위 예에서 모든 캐릭터는 걷고, 뛰고, 이름을 가지고 있습니다. 이것은 어떤 캐릭터가 새로 생기던지 변함이 없죠. 하지만, 액션은 다릅니다. 각각의 캐릭터가 고유의 행동을 해야합니다.

바로 이렇게 서로 다르게 행동하는 것을 추출해 내는 것이죠.

그러기 위해서 그 행동을 하나로 모아놓고 그룹을 지어줄 수 있는

인터페이스를 만들어 보겠습니다.


public interface ActionMethod {
 public abstract void action();
}


그리고 위에서 나오는 두가지 행동 "공격"과 "치료"그 자체의 행동을 위 인터페이스를 구현하여 작성합니다.

public class AttackAction implements ActionMethod {

 @Override
 public void action() {
  System.out.println("칼로 공격을 합니다.");
 }
}

public class HealingAction implements ActionMethod {
 @Override
 public void action() {
  System.out.println("아군이면 치료를 합니다.");
 }
}

그리고 이제 Character 클래스를 살짝 바꿔주겠습니다.
action을 바로 위 인터페이스를 이용해서 수행되도록 해주는 것입니다.

public class Character {
 String name;
 ActionMethod attackMethod;

 public void setAttackMethod(ActionMethod attackMethod) {
  this.attackMethod = attackMethod;
 }

 public void walk() {
  System.out.println("두발로 걷다.");
 }
 
 public void run() {
  System.out.println("두발로 뛰다");
 }
 
 public void setName(String name) {
  this.name = name;
 }
 
 public String getName() {
  return name;
 }

 public void action() {
  attackMethod.action();
 }

}

여기서 보시면 이제 Character의 action은 ActionMethod 인터페이스를 구현한 클래스들의 action을 실행하게 됩니다.

이렇게 하면 이제 각각의 직업별 클래스에서는 action 메서드가 없어지게 됩니다.

public class Paladin extends Character {

}

public class Priest extends Character {

}

public class Thief extends Character {

}

public class Warrior extends Character {

}

이제 테스트를 해보겠습니다.

public class Test {
 public static void main(String[] args) {
  Character a = new Paladin();
  Character b = new Thief();
  Character c = new Warrior();
  Character d = new Priest();
 
  a.setName("인간성기사뿌뿌뿡");
  b.setName("인간도적");
  c.setName("타우렌전사");
  d.setName("나엘사제");
 
  ActionMethod attack = new AttackAction();
  ActionMethod heal = new HealingAction();
 
  a.setAttackMethod(attack);
  b.setAttackMethod(attack);
  c.setAttackMethod(attack);
  d.setAttackMethod(heal);

 
  System.out.print(a.getName() + " : ");
  a.action();
  System.out.print(b.getName() + " : ");
  b.action();
  System.out.print(c.getName() + " : ");
  c.action();
  System.out.print(d.getName() + " : ");
  d.action();
 }
}
인간성기사뿌뿌뿡 : 방패로 공격
인간도적 : 단검으로 공격
타우렌전사 : 장검으로 공격
나엘사제 : 치료를 합니다

위 볼드처리 된 부분을 보시면 아시겠지만, 이제는 각 캐릭터가 필요한 액션을 setting해서 사용합니다. 오히려 위 처음에 상속만을 이용했을때보다 코드가 길어진 것 같지만 이것은 상당한 이득이 있는 방법입니다. 만약에, 게임이 업그레이드 되어서 사제에게도 공격을 할 수 있는 능력이 생겼다라고 하면 기존에는 사제 클래스를 열어서 action 메서드를 다시 오버라이드 해줘야 합니다.

그리고 또 만약 성기사에게도 치료 할 수 있는 능력이 생겼다면?

마찬가지로 또 성기사 클래스를 열어서 action 메서드를 다시 오버라이드 해줘야 합니다.

캐릭터가 더 많은 상태에서 위와 같은 일들이 벌어지면 몇개의 클래스를 다시 열어서 액션 메서드를 오버라이드 해줘야 할지 모릅니다. 하지만 위와 같이 strategy 패턴을 사용하게 되면

ActionMethod 인터페이스를 상속한 HealAndAttackAction 클래스를 만들고

public class HealAndAttack implements ActionMethod {
@Override
 public void action() {
  System.out.println("적이면 공격하고 아군이면 치료합니다");  
 }
}

이것을 사용해야 하는 캐릭터에게 위 예제처럼 setting만 해주면 됩니다.

Character d = new Priest();
d.setName("나엘사제");
AttackMethode healAndAttack = new HealAndAttack();
d.setAttackMethod(healAndAttack);
d.action();


위에서는 추출된 행동을 "액션" 하나만으로 가정했지만 만약에 이 캐릭터가 게임도중 말을 사서 말을 타기도 하고, 그냥 걸어 다닐 수도 있다고 해보면 문제는 좀 더 명확해집니다.

모든 클래스를 열어서 walk와 run 메서드에 if 분기문을 사용해서

말을 타고 있으면 말타고 달리기
없으면 걷기

run 메서드에서도
말을 타고 있으면 말타고 달리기
없으면 그냥 뛰기

이렇게 수정을 해줘야 한다는 것입니다.

하지만 마찬가지로 걷다뛰다라는 행동을 인터페이스로 추출하여 그때그때 마다 캐릭터의 상황에 맞게 그 행동을 set 해주면 캐릭터 클래스의 수정없이도 간단하게 그 행동을 제어 할 수 있습니다.

또한 인터페이스를 사용해서 행동 자체를 모듈화 시켰기 때문에 만약 제가 또 다른 게임을 만드는데 그 게임에서의 행동도 공격하는 것과 치료하는 것이 있다라고 하면 위 ActionMethode 인터페이스와 그것을 구현한 AttackAction와 HealingAction 클래스를 그대로 사용 할 수도 있습니다.



 

Posted by 용식