정의

스트래티지 패턴은 알고리즘군을 정의하고 각각을 캡슐화하여 교환해서 사용할 수 있도록 만들며 알고리즘을 사용하는 클라이언트와는 독립적으로 알고리즘을 변경할 수 있다. (실행중에 동적으로 알고리즘을 변경할 수 있다.)

이번에도 스타크래프트의 테란 종족을 예로 들어본다. 테란의 유닛은 스피드와 체력이 있으며 이동, 공격과 중지하는 기능이 있다. 그런데 Scout은 지상유닛이 아닌 공중유닛으므로 하늘을 날아다니며 이동해야한다. 만약 공중유닛이 스카우트뿐이라면 상관없겠지만 다양한 공중 유닛이 존재한다고 가정한다.

우선 상속을 이용해서 이를 해결해보자. Unit에 fly()라는 하늘을 나는 기능을 추가한다. 그러나 Marine과 Firebat도 Unit을 상속하기 때문에, 지상유닛인 마린과 파이어벳도 날 수있는 유닛이 돼버린다. 누군가 마린과 파이어벳의 fly를 호출하면 날개도 동력도 없는 것들이 날아다니는 모습을 보여주게 된다. 코드의 한 부분만을 바꿨는데 프로그램의 일부가 아닌 전체에 에러가 미칠 수 있는 상황의 한 예시이다. 그렇다고 마린과 파이어벳의 fly()메소드의 구현에 아무것도 하지 않도록 구현하면 필요 없는 코드가 중복되게 된다.

상속의 단점

  1. 서브클래스에서 코드가 중복된다.
  2. 실행시에 특징을 바꾸기 힘들다.
    • 스트래티지 패턴이 이를 해결하는데 도움을 준다.
  3. 모든 서브클래스들의 행동을 한눈에 파악하기가 힘들다.
    • 각각 특정 구현에 의존하고 있기 때문이다.
  4. 코드를 변경했을 때 다른 서브클래스에 원치 않은 영향을 끼칠 수 있다.

상속을 이용해도 문제를 해결할 수 있다. 다만 작업이 많아지고 변경에 대처하기 힘들 뿐이다. 게임분야는 꾸준한 업데이트가 생존을 결정한다고 해도 과언이 아니다. 스타크래프트의 업데이트는 유닛의 체력이나 속도를 조정해서 밸런스를 맞추거나 각 종족에 새로운 유닛을 추가하거나 기존 유닛에 새로운 스킬을 추가하는 내용일 것이다. 이런 경우 프로그램 코드의 수정이 빈번하게 일어날텐데, 상속을 주로 이용해서 게임을 구현했다면 각 유닛이나 건물의 규격, 기능이나 역할이 바뀔 때 마다 서브클래스의 메서드와 상태를 일일이 살펴봐야한다.

예시

일부 형식의 Unit클래스만 공중에서 움직일 수 있도록 하는 방법을 찾아보자. 가장 간단한 방법은 다형성을 이용하는 방법이다. 지상유닛은 Walkable, 공중유닛은 Flyable 인터페이스를 각각 구현하여 walk(), fly() 메소드를 구현한다. 유닛의 move()의 호출을 walk()나 fly()로 넘기기만 하면 된다. 그러나 이런 해결법은 일부 문제점은 해결할 수 있지만 fly()나 walk()와 같은 코드의 재사용을 기대할 수 없게 된다. 결국 코드 관리 면에 있어서 문제가 된다.

예를 들어 저그, 프로토스의 공중유닛과 지상유닛의 이동방법이 상이할 수도 있다. 디자인 원칙을 이용하면 이러한 변화에 유연하게 대처할 수 있다. 디자인 원칙에 대한 포스팅에서 작성한 것처럼 스트래티지패턴에는 총 3개의 디자인 원칙이 등장한다.

바뀌는 부분은 캡슐화한다.

테란의 유닛에서 move()를 제외하면 문제없이 잘 작동하며 변동의 여지가 별로 없다. move()는 변경될 가능성이 있는 부분이므로 fly와 walk를 분리해야한다. fly와 walk는 결국 유닛이 움직이는 move이므로 움직이는 클래스 집합 하나로 분리할 수 있다..

구현이 아닌 인터페이스 위주로 프로그래밍한다.

움직이는 행동을 구현하는 클래스 집합은 최대한 유연하게 만들어야 한다. 인터페이스를 이용하면 행동과 관련된 유연성을 높일 수 있으며 생성자 혹은 메소드를 이용해 동적으로 행동을 할당할 수 있게 된다.

두가지 원칙을 적용해 MoveBehavior라는 움직이는 행동 클래스 집합을 정의한다. 움직이는 행동은 Unit에서 구현하지 않고 MoveBehavior라는 인터페이스를 구현한 서브클래스에서 구현한다. Unit의 서브클래스에서는 특정 구현이 아닌 MoveBehavior를 이용해서 행동을 하게 된다.

상속보다는 구성(Composition)을 이용하라.

Unit이 MoveBehavior의 서브클래스를 상속해서 사용해야 할까? 움직이는 행동은 다시 서브클래스에 의해 두가지로 분기되므로 특정 행동을 상속하면 안된다. 이럴때 구성을 활용할 수 있다. Unit의 멤버 변수로 MoveBehavior를 구성하여 합성한다. 움직이는 행동을 상속하는 대신 올바른 행동의 객체로 구성됨으로써 특정 행동을 부여받게된다. 단순히 움직이는 알고리즘을 별도의 클래스의 집합으로 캡슐화하는 것 뿐만 아니라 구성요소로 사용하는 객체에서 올바른 행동에 대한 인터페이스를 구현하기만 하면 실행시에 행동을 바꿀 수 있다.

위 그림은 최종 코드의 다이어그램이다. Unit 추상클래스에서 생성자나 setMoveBehavior()에서 행동 인스턴스를 지정한다. 이후 move의 구현은 지정된 행동 인스턴스의 move()를 호출하기만하면 된다. 걷는 유닛이 아닌 달리는 유닛이 필요하다면 RunForMove를 추가하고 Unit의 인스턴수 변수에 지정한다.

다른 예로 게임 중에 걷는 유닛이 스팀팩 상태가 되어서 일정 시간동안 달릴수 있게 하려면 setMoveBehavior의 파라미터로 RunForMove의 인스턴스를 넘겨준 뒤 move()를 호출하면 된다.

스트래티지 패턴 예제