[Design Pattern] 전략패턴(Strategy Pattern)

2021. 1. 29. 23:57개발/디자인패턴

 

Head First Design Patterns를 학습한 내용을 기록합니다.

 

 

게임에 존재하는 몇가지 종류의 오리를 만들어 보려고한다. 이를 클래스로 설계하면 아래와 같이 할 수 있을것이다. 

 

Duck이라는 클래스를 선언하고 quack()과 swim() 메소드를 구현한다. display()는 선언만하고 구현하진 않는다. 

각 서브클래스 오리들(MallardDuck , ReadheadDuck)은 수퍼클래스 Duck을 상속받아서 오리의 특색에 맞게 display를 구현한다. 현재 구조를 보면 모든 오리가 똑같이 수영하고 꽥꽥 소리를 낸다.

 

 

 

 

위 클래스 설계에서 오리가 날 수 있도록 기능을 추가하려면 어떻게 하면될까?? 

아래의 방식으로 설계하면 각 오리별로 메소드를 추가할 필요 없이 손쉽게 모든 오리들이 날 수 있도록 할 수 있다.

( Duck 클래스에 fly 메소드를 구현한다 )

 

 

 

 

어느날 게임에 RubberDuck이 새롭게 추가 되었다. 고객의 요구사항에 의하면 RubberDuck은 기존의 오리와 다른 소리를 내야 하며 날아 다닐 수 없다고 한다. 현재 구조에서는 다음과 같은 해결방법이 있다.

 소리를 다르게 내야하는 경우 quack() 메소드를 오버라이드 받아 다른 소리를 내도록 구현할 수 있다. 날아다니지 못하게 하기 위해서 fly() 메소드를 오버라이드 받아 아무 동작도 하지 못하도록 구현한다. 

 

 

 

위 처럼 하면 문제를 해결 할 수 있지만, 만약 목각오리가 추가 되어 소리도 못내고, 날지도 못하면 RubberDuck과 마찬가지로 슈퍼클래스(Duck)에 있는 모든 메소드를 상속받아 목각오리에 특성에 맞게 메소드를 수정해야한다. 

 

즉 모든 종류의 오리가 소리를 내거나 날 수 있는것이 아니기 때문에 위와 같은 상속구조로는 오리의 종류가 자주 추가될때 효율적인 모습을 보여주지 못한다

 

 

 

 

이런 문제를 해결하기 위해서 우리는 Duck 클래스에서 변경이 잦은 메소드인 quack과 fly를 분리한다. 

 

FlyBehavior와 QuackBehavior 인터페이스를 사용하여 각각의 인터페이스를 구현하는 클래스들이 존재한다.

FlyBehavior 인터페이스내 fly 메소드를 선언한다. 현재 게임속에 존재하는 오리들의 나는 동작들은 FlyBehavior를 상속받아 구현하도록 한다. ( QuackBehavior도 마찬가지다. )

 

 

 

이제 Duck을 새롭게 작성한다. FlyBehavior와 QuackBehavior 인터페이스 인스턴스를 가지고 있다. 동작이 있다(선언만 했다) 뿐이지 아직 어떤 동작을 할지는 정의(구현)하지 않은 상태다. 아래 형광펜으로 색칠한 performQuack() 메소드와 performFly() 메소드에서 구현한다.

 

 

Duck 클래스의 소스코드의 일부를 살펴보자. 예상대로 performQuack() 메소드 내에서 QuackBehavior인터페이스의 quack() 메소드를 수행한다. 다만 아직까지는 동작이 구현되어 있는것은 아니다. 

 

 

 

 

이제 이러한 구조를 가지고 오리의 종류를 하나씩 추가해보자.

만약 MallardDuck이 추가 된다고 했을때 다음과 같이 코드를 짤 수 있을 것이다. 

Duck 클래스를 상속받은 MallardDuck에서 Duck 클래스내에 선언된 인터페이스인 quackBehavior와 flyBehavior를 구현한다.  

 

 

 

 

RubberDuck이 추가 되었다고 해보자. MallardDuck과 마찬가지로 이미 구현된 동작인 MuteQuack 클래스와 FlyNoWay 클래스를 통해 간편하게 오리의 동작을 수정할 수 있다. 

 

 

 

 

위 케이스에서 런타임 시에 RubberDuck이 날아 다닐수 있게 할수 있을까?? 당연히 불가능하다. 따라서 런타임시에 동적으로 오리의 동작을 수정하기 위해 좀더 유현하게 소스코드를 수정 해야한다. 

 

 

먼저 Duck 클래스에 Setter 메소드를 추가한다. 

 

 

 

 

그리고 동적인 동작을 테스트하기 위해 ModelDuck (모형오리)를 추가한다.  생성자를 보면 알수 있듯이 모형 오리는 날 수없지만 꽥꽥 소리를 낼 수 있다.

 

 

 

추가로 날아다니는 동작을 하나더 구현한다.

 

 

 

이제 ModelDuck을 런타임 시에 로켓추친으로 날아가게 바꿔보자.

 

 

 

ModelDuck 객체를 생성하고 performFly()를 호출하면 기본 셋팅인 FlyNoWay()가 호출된다. 하지만 그다음 줄에 setter 메소드를 이용해 동작을 변경하였고, performFly()를 다시 호출하면 FlyRocketPowered()가 호출된다. 

 

결과콘솔

 

 

 

 

최종 디자인 모습

 

 

 

 

지금까지 설명한 디자인 방식이 전략패턴 방식이다. Duck 클래스에서 동작의 구현을 분리하고 Duck에는 동작을 실제로 하는것이 아닌 "이런 동작이 존재한다" 정도만 정의해둔다. Duck을 상속받는 서브클래스(각종 오리들)는 전략화된 동작들을 바꿔 끼워가면서 런타임시에도 자신의 동작을 손쉽게 수정할 수 있다.