디자인 패턴 두 번째 시간입니다.
2번째 디자인패턴 주제는 전략 패턴 Strategy Pattern 입니다.
Strategy는 한글 말로 번역하면 '전략'이고, 이는 "본래 군사에서 쓰이는 낱말로, 특정한 목표를 수행하기 위한 행동 계획" 이라는 뜻을 지닙니다. 즉, 프로그래밍에서 말하는 전략은 일종의 알고리즘 혹은 특정한 기능이나 행동을 수행하기 위한 동작을 의미한다고 볼 수 있습니다.
그럼, Strategy Pattern은 프로그래밍에서 어떤 부분을 설계해 주는 걸까요?
Strategy Pattern의 목적은 같은 종류의 작업을 하는 알고리즘을 정의, 각 알고리즘을 캡슐화, 그리고 알고리즘들을 서로 바꿔 사용할 수 있도록 함. 스트래티지 패턴은 알고리즘을 사용하는 클라이언트로부터 독립적으로 알고리즘을 바꿔서 적용할 수 있도록 함.
캡슐화: 객체지향프로그래밍의 특징 중 하나로 클래스 내부의 연관 있는 속성들과 기능들을 하나의 속성으로 만든 데이터를 외부로부터 보호하는 것
제가 대학 강의에서 배웠던 자료에는 Strategy Pattern의 목적은 이렇게 명세 되어나와 있습니다. 즉, 하나의 동작 수행에 포함된 모든 알고리즘 중에 변형이 빈번한 알고리즘을 수정할 경우, 기능 전체를 검토하며 수정하는 것이 아니라 해당 부분만 확장, 추가, 수정해도 동작할 수 있게 만들어 주는 디자인 패턴입니다. 즉, 유지 보수를 쉽게 관리해 주기 위한 기법으로 볼 수 있습니다.
여러분들의 이해를 돕기 위해 특정한 상황을 예시로 표현해 보겠습니다.
현재 당신은 유명한 게임 IP [드래곤 퀘스트], [포켓몬스터]와 같은 장르인 턴제 RPG 게임을 만들었다. 여기서 모든 케릭터들은 턴마다 시전할 수 있는 스킬Skill(...)를 사용할 수 있고, 모든 Skill들은 마나를 사용한다. 그리고 단순히 데미지를 입히는 스킬뿐만 아니라 상태 이상을 부여할 수 있는 특수 스킬을 구현하고자 한다. 이후에 [중독], [화상], [동상], [기절] 등의 상태 이상을 Skill(...) 클래스를 상속하여 만들어진 Poisoning, Burning, Freezing, Stunning 클래스들을 아래와 같이 정의한다.
public class Skill {
private String name;
private int mana_points;
private int power;
public Skill(String name, int mana_points, int power) {
this.name = name;
this.mana_points = mana_points;
this.power = power;
}
public void Attack() {
System.out.println("적에게 "
+ name + "을 시전!"
+ mana_points + "마나를 사용하고"
+ power + "만큼 피해 입혔다.");
}
public String getName() { return name; }
public int getMp() { return mana_points; }
public int getPower() { return power; }
}
public class Poisoning extends Skill{
private String statusEffect = "중독";
public Poisoning(String name, int mana_points, int power) {
super(name, mana_points, power);
}
@Override
public void Attack() {
System.out.println("적에게 " + name + "을 시전!"
+ mana_points + "마나를 사용하고"
+ power + "만큼 피해 입혔다."
+ statusEffect + "상태 이상을 부여했다."
);
}
}
=> 나머지 Burning, Freezing, Stunning도 위의 Posioning 과 같다.
public class Main {
public static void main(String[] args) {
int power = 10;
Skill[] skills = {
new Poisoning("중독 스킬", power),
new Burning("화상 스킬", power),
new Freezing("동상 스킬", power),
new Stunning("기절 스킬", power),
};
for (Skill s : skills) {
s.Attack();
}
}
}
하지만 이후 당신은 스킬들 중에 아군 파티원을 회복시키는 Healing 스킬과 능력치를 향상시키는 Buff 스킬들을 추가하기 위해, 추가로 클래스(Healing, Buff)를 만들어 상속을 통해 확장시킨다. 하지만 당신은 한 가지 실수를 저질렀다는 사실을 알아차린다.
바로 Healing 스킬이 Skill 클래스로부터 Attack() 매서드를 상속받아 적에게 피해를 줄 수 있게 된 것이다. 반대로, Posioning, Burning, Freezing, Stunning 스킬들은 Heal()를 사용하게 되었다.
물론 Skill의 Attack()를 지우고, Posioning 및 기타 공격 스킬들에 각각 Attack()을 따로 정의해서 이 문제를 해결할 수도 있겠지만, 이후에 새로운 아이디어에서 나온 특수한 공격 스킬을 구현할 때도 Attack()을 따로 정의해야 하는 번거로움을 겪게 될 것이며, Attack() 알고리즘 자체에 변경 사항이 발생할 경우, 이를 상속받은 모든 스킬의 알고리즘을 일일이 확인해야 하는 대참사가 일어납니다;;
위와 같은 상황이 발생하는 이유는 객체지향프로그래밍의 상속 특징으로 인한 알고리즘들의 강한 결합Tight Coupling으로 기능의 확장, 분리할 때 알고리즘들끼리 강한 의존성으로 인해 어려움을 겪게 됩니다. 그래서 빈번히 수정이 발생할 알고리즘들과 그렇지 않을 알고리즘들을 분리해, 약한 결합Loose Coupling 관계를 설계해야 하는데, 이에 대한 해결책으로 Strategy Pattern이 유용하게 쓰입니다.
현재 스킬들은 attack(), heal(), buff() 중 각각 하나를 시전할 수 있습니다. 하지만 이제 모든 스킬들의 시전 타입을 하나로 묶은 [Action] 인터페이스(추상 클래스로도 구현 가능함)로 만들고 위 3가지 행동들의 클래스를 [Action] 인터페이스를 implement하게 만들어보겠습니다.

이렇게 [Action]을 implement하는 알고리즘들의 구현이 완성되고 Skill 클래스는 생성할때 이 클래스 중 하나를 가지게 됩니다. 또한, 후에 Action 타입을 바꿀 수 있도록 setAction(Action):void 도 만들어 줍니다.

자, 그럼 이제 문제가 해결되었는지 확인해보기 위해 스킬들을 생성하고, execute()를 통해 각각의 action에 맞는 행동을 하는지 확인해볼까요?
public class Skill {
private String name;
private int mana_points;
private int power;
private Action act = null;
public Skill(String name, int mana_points, int power, Action act) {
this.name = name;
this.mana_points = mana_points;
this.power = power;
this.act = act;
}
public void setAction(Action act) {
this.act = act;
}
public String getName() { return name; }
public int getPower() { return power; }
public void execute() {
System.out.println(mana_points + "마나를 사용함" ); //고정
act.action(this); //Action 인터페이스를 통해 Action의 종류를 결정할 수 있음
}
}
public class Main {
public static void main(String[] args) {
Skill[] skills = {
new Skill("중독", 1, 10, new Attack()),
new Skill("화상", 2, 10, new Attack()),
new Skill("빙결", 3, 10, new Attack()),
new Skill("기절", 4, 10, new Attack()),
new Skill("회복", 5, 10, new Heal()),
new Skill("버프", 6, 10, new Buff())
};
// 스킬들 각각 시전했을 때, 결과
for (Skill s : skills) {
s.execute();
}
// 스킬의 Action 타입 변경할 수도 있음
skills[0].setAction(new Buff());
skills[4].setAction(new Attack());
}
}
실행결과:
1마나를 사용함
적에게 중독을 시전!10데미지를 피해를 입혔다
2마나를 사용함
적에게 화상을 시전!10데미지를 피해를 입혔다
3마나를 사용함
적에게 빙결을 시전!10데미지를 피해를 입혔다
4마나를 사용함
적에게 기절을 시전!10데미지를 피해를 입혔다
5마나를 사용함
아군에게 회복을 시전!10만큼 체력을 회복시켰다
6마나를 사용함
아군에게 버프을 시전!버프을 부여했다
와우! 잘 작동되는게 보이시죠?
이런 식으로 Strategy Pattern 설계를 구현하고 나면, 새로운 스킬(Action을 implement한 새로운 클래스)을 만들고나서, 다른 스킬들과 똑같이 execute()를 통해 실행가능하고, 알고리즘들이 충돌이 발생하지 않습니다! 또한, 스킬들의 고정 패턴인 "마나를 소비하는 과정"을 수치만 조정하고 그대로 적용시킬 수 있습니다.
이제 각 케릭터들에게 사용할 수 있는 스킬들만 할당하면, 전투시스템이 완성되겠군요!
실제로, 현재 상용되고 있는 게임들이 저런 설계도를 따라줄진 모르겠습니다; 다만, 게임 중 전투 밸런스 패치가 빈번한 게임들에서 (대표적으로 리그오브레전드, 혹은 fps 게임 부류들은) Strategy Pattern 처럼 유지보수에 신경 쓰는 코드들은 분명히 존재할테니 공부삼아 한번 구현한 다음 성능 테스트로 확인해 보시는 것을 추천드리겠습니다.
글 잘 보고 가요~