본문 바로가기

Design Pattern

[디자인 패턴_Again] Strategy Pattern.

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

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

공유를 할 수 있습니다.


만약 와우 짝퉁을 만든다고 하겠습니다.
 
처음이기 때문에 간단한 직업을 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 클래스를 그대로 사용 할 수도 있습니다.