본문 바로가기

OOP, FP/디자인패턴

헤드퍼스트 디자인 패턴: 1. 스트래티지 패턴

앞서 디자인 패턴의 원칙 9가지를 복습을 마쳤다.


이번에는 책에서 다룬 디자인 패턴에 대해서 복습하려고 한다.

그 첫번째인 스트래티지 패턴(Strategy Pattern)


스트래티지 패턴

알고리즘군을 정의하고 각각을 캡슐화하여 교환해서 사용할 수 있도록 만든다.

이 패턴을 사용하면 알고리즘을 사용하는 클라이언트와는 독립적으로 알고리즘을 변경할 수 있다.

(실행중에 동적으로 알고리즘을 변경할 수 있다.)


이번에도 역시 스타크래프트의 테란 종족의 등장이다.

테란의 유닛이 각각 스피드와 체력을 상태로 갖고 움직이는 메소드, 공격을 시작하는 메소드, 행동을 멈추는 메소드가 있다고 한다.


그런데 여기서 Scout에 문제가 생겼다. 얘는 움직일 때 move()가 아닌 fly()를 써야된다.

스카우트만 날수있으면 상관없겠지, 하지만 배틀, 사이언스 베슬 등... 다양한 공중 유닛이 존재한다.


그럼 상속을 이용해서 구성했을 때 Unit에 fly()를 추가해보자.

의도와는 다르게 지상유닛인 마린과 파이어벳도 날 수있는 녀석이 되버렸다.

실수로 누군가 마린과 파이어벳의 fly를 호출하면 날개도 동력도 없는 것들이 날아다니는 모습을 보여줄것이다.


코드의 한 부분만을 바꿨는데 프로그램의 일부가 아닌 전체에 에러가 미칠 수 있는 상횡이 된것이다.

그럼 마린과 파이어벳의 fly()메소드의 구현에 아무것도 하지 않도록 구현해보면?

필요 없는 코드가 중복되는 셈이 된다.


상속의 단점

(1) 서브클래스에서 코드가 중복된다.


(2) 실행시에 특징을 바꾸기 힘들다. 

- 이는 스트래티지 패턴에서 해결한다


(3) 모든 서브클래스들의 행동을 한눈에 파악하기가 힘들다.

- 각각 특정 구현에 의존하고 있으므로


(4) 코드를 변경했을 때 다른 서브클래스에 원치 않은 영향을 끼칠 수 있다.


자 그럼 가장 큰 문제라고 할 수 있는건 무엇일까?

어차피 우린 프로그램이 동작만 하면 된다고 믿는다.

게임과 같은 경우에는 업데이트가 그 생명줄을 좌지우지한다고해도 과언이 아니다.

스타크래프트의 업데이트라하면 유닛의 체력이나 속도를 조정해서 밸런스를 맞춘다거나

각 종족에 새로운 유닛을 추가한다거나 기존 유닛에 새로운 스킬을 추가하는 상황일 것이다.


이런 경우 프로그램 코드의 수정이 빈번하게 일어날텐데, 상속을 주로 이용해서 게임을 구현했다면

각 유닛이나 건물의 규격, 기능, 역할이 바뀔 때 마다 메번 해당 서브클래스의 메서드와 상태를 일일이 살펴봐야한다.

오버라이드를 해서 기능을 확장하거나 아예 바꿔야할 수도 있으니까.

규모가 커질수록 이러한 행위는 고된 노동이 될 것이다.


그래서 우리는 일부 형식의 Unit클래스만 공중에서 움직일 수 있도록 하는 방법을 찾아야한다.





가장 간단한 방법은 다형성을 이용하는 방법이다.

지상유닛은 Walkable(편의상 Runable대신), 공중유닛은 Flyable 인터페이스를 각각 구현하여

walk(), fly() 메소드를 각각 오버라이드해서 구현한다.


이후 유닛의 move()의 호출을 walk()나 fly()로 넘기기만 하면 된다.

허나 이런 해결법은 일부 문제점은 해결할 수 있지만 fly()나 walk()와 같은 행동에 대한 코드 재사용성을 전혀 기대할 수 없게 된다.

이 또한 코드 관리 면에 있어서 문제가 된다는 것이다.


테란 종족만이 아닌 저그, 프로토스의 공중유닛과 지상유닛의 이동방법이 상이할 수도 있다.

이를 디자인 원칙을 이용해서 해결해야한다.


우선 디자인 원칙에서 알아봤듯이 스트래티지패턴에선 총 3개의 디자인 원칙이 등장한다.


(1) 바뀌는 부분은 캡슐화

- 위 코드만의 관점에서 move()를 제외하면 문제없이 잘 작동하며 변동의 여지가 별로 없다.

  그럼 move()가 바뀌는 부분이라는 뜻이니까 fly와 walk를 분리해야한다.

  fly와 walk는 결국 유닛이 움직이는 move이므로 움직이는 클래스 집합 하나로 분리할 수 있겠다.


(2) 구현이 아닌 인터페이스 위주

- 그럼 움직이는 행동을 구현하는 클래스 집합을 어떻게 분리해야할까?
  인터페이스를 이용해 최대한 유연하게 만들어야 한다.

  인터페이스를 이용하면 행동과 관련된 유연성을 높일 수 있으며

  생성자 혹은 세터메소드를 이용해 동적으로 행동을 할당할 수 있게 된다.




  이렇게 두가지 원칙을 적용해 MoveBehavior라는 움직이는 행동 클래스 집합을 정의한다.

  이제 움직이는 행동은 Unit에서 구현하지 않고 MoveBehavior라는 인터페이스를 구현한 서브클래스에서 구현한다.

  Unit의 서브클래스에서는 특정 구현이 아닌 MoveBehavior를 이용해서 행동을 하게 된다.


(3) 상속보다는 구성(Composition)

-  그럼 Unit이 MoveBehavior의 서브클래스를 상속해서 사용해야 할까?

  움직이는 행동은 다시 서브클래스에 의해 두가지로 분기되므로 특정 행동을 상속할 수 없다.

  이럴때 구성을 활용할 수 있다.


  우리는 객체지향에서 다형성과 상속 외에 A has B -> 구성(composition)이라는 특성에 대해 많이 들어봤다.

  Unit의 멤버 변수로 MoveBehavior를 구성하여 합치는 것이다.

  움직이는 행동을 상속하는 대신 올바른 행동의 객체로 구성됨으로써 특정 행동을 부여받게된다.


  단순히 움직이는 알고리즘을 별도의 클래스의 집합으로 캡슐화하는 것 뿐만 아니라

  구성요소로 사용하는 객체에서 올바른 행동에 대한 인터페이스를 구현하기만 하면 실행시에 행동을 바꿀 수 있다(동적)



  Unit 추상클래스에서 생성자나 setMoveBehavior()에서 행동 인스턴스를 지정해준 뒤

  public move() { moveBehavior.move() }; 와 같이 사용하면 된다.


  걷는 유닛이 아닌 달리는 유닛이 필요하다면 RunForMove를 추가하고 Unit의 인스턴수 변수에 셋팅해주기만하면 된다.

  걷는 유닛이 스팀팩 상태가 되어서 일정 시간동안 달릴수 있게 하려면 setMoveBehavior의 파라미터로 RunForMove의 인스턴스를

  넘겨준 뒤 move()를 호출하면 된다.(동적)



알고리즘을 캡슐화하는 스트래티지 패턴의 복습을 마치겠다.


1. 스트래티지 패턴