본문 바로가기
👩🏻‍💻Technical things/Design pattern

[Design pattern] Strategy Pattern

by Bㅐ추 2020. 6. 22.
728x90
반응형

본 게시글은 Head First Design Pattern을 읽고 정리한 내용 입니다.

해당 소스코드는, https://github.com/so1gging/Design-Pattern를 참고하세요:)


 

0. 정의

객체들이 할 수 있는 행위를 각각에 대해 클래스로 생성하고, 유사한 행위들을 캡슐화하는 인터페이스를 정의하여

객체의 행위를 동적으로 바꾸고 싶은 경우 직접 행위를 수정하지 않고 클래스를 바꿔주기만 함으로써 행위를 유연하게 확장하는 방법을 말한다.

 

1. 일반 객체지향 기법으로 구현했을 때의 문제

John은 오리 어플리케이션 게임을 운영하는 회사를 다니면서 오리게임을 만든다.

이 게임에서는 헤엄치고 꽥꽥 울음소리를 내는, 매우 다양한 오리들을 보여준다.

John은 표준적인 객체지향 기법을 사용하여 Duck 이라는 슈퍼클래스를 만든다음 그 클래스를 확장하여 다른 종류의 오리를 만들었다.

 

요구사항 결과

모든 오리는 꽥꽥 소리를 낼 수 있고, 헤엄을 칠 수 있다.

오리는 모양새가 다르다.

공통사항을 하나로 묶은 Duck 추상 클래스 생성.
(quack / swim / display (오리는 각각 모양이 다르기 때문에 추상메서드로 선언하여 하위 클래스에서 구현) )

 

 

 

 

추상클래스인 Duck 클래스를 ReadHeadDuck 클래스와 MallardDuck 클래스가 상속을 받아 추상메소드인 display()를 각각 구현한다.

 

 

문제의 시작1 - 요구사항의 변경

원래는 그럴 계획이 없었는데..

  오리들이 물에 떠있는 기능 이외에 날아다녀야하는 요구사항이 생겼다. John은 날아다니는 기능을 추가해야 한다.

Duck클래스에 fly()메서드만 추가하면 해결되지 않을까? 지금까지 객체지향으로 해결했던 것 처럼, fly()메서드를 추가하면 모든 오리들(하위 클래스)은 상속받을 것이다.

 

이제 모든 오리들에게 날수있는 기능이 추가되었다.

 

 

이것이 더 심각한 문제가 되었다.

날 수없는 오리가 있었다는 사실을 잊고있었다. 즉, 상속받은 모든 하위클래스 오리가 모두 날 수 있는 것이 아니였다.(고무오리도 있지 않은가?) 

 

 

John은 문제를 해결하기 위해, RubberDuck 클래스에서 fly() 메소드와 quack() 메소드를 오버라이드 하여 소리와 날수있게 하는기능을 변경시켜주었다.

 

 

일단 문제는 해결되었지만, 향후에 RubberDuck과 같은 가짜오리가 더 추가가 된다면 그때마다 맞지않는 상속되는 메소드들을 오버라이드 해서 구현해야하는 문제가 여전히 존재한다. 객체지향 기법으로 구현했지만, 과연 이것이 효율적인 프로그래밍인 것인가 John은 의심이 들기 시작한다.

 

 

문제의 시작2 : 지속적인 업데이트

 

회사에서 1개월마다 한번씩 새로운 오리를 업데이트 한다고 한다. 여러 오리가 새롭게 추가될것이고 그 규격도 계속 변할 것이라고 한다. 그렇다면 매번 모든 오리 서브클래스의 fly() 와 quack() 같은 메소드를 일일이 살펴봐야하고 상황에따라 오버라이드로 해야할수있다. 이쯤되면 상속활용이 맞는건가 의구심이 든다. 다시생각해보자.

 

만약, 인터페이스를 사용한다면 ?

fly()를 Duck 수퍼클래스에서 빼고 fly()메서드가 들어있는 Flyable 인터페이스를 만들 수 도 있다. 이렇게 하면 날 수 있는 오리들에 대해서만 그 인터페이스를 구현해서 fly()메서드를 집어넣을 수 있겠지. 모든 오리들이 꽥꽥거리는 건 아니니까 Quackable이라는 인터페이스도 같이 만들면 좋겠네. 라고 John은 생각한다.

 

과연 좋을까?

 

 

 

제일 바보같은 아이디어.

메소드 몇 개 오버라이드해야 하는 것이 좋지 않다면, 날아가는 동작을 조금 바꾸기 위해 Duck의 서브클래스 가운데 날아다닐 수 있는 마흔 여덟 개의 코드를 전부 고쳐야 하는 상황은 어떻게 생각하는지 John에게 묻고싶다. 게다가 코드를 더이상 재사용할 수 없다. Flyable과 Quackable은 코드가 없는 인터페이스이기 때문이다.

 

이러한 문제상황을 해결하기 위해 디자인 패턴을 배우는 것이다.

이제 John이 문제상황을 해결할 수 있도록 도와주자.

 

2. 문제 파악

상속을 사용하는 것은 서브클래스들의 행동이 바뀔수 있는데 불구하고 모든 서브클래스들이 하나의 행동을 사용하는것이 문제가되고,  Flyable과 Quackable 인터페이스 사용을 하는 방법도 코드재사용을 할 수 없다는 문제가 있다. (한가지의 행동을 바꿀 때 마다 그 행동이 정의되어있는 모든 서브클래스들은 전부 찾아서 코드를 일일히 고쳐야 하고, 그 과정에서 새로운 버그가 생길 가능성이 많음!)

 

디자인 원칙1
애플리케이션에서 달라지는 부분을 찾아내고, 달라지지 않는 부분으로부터 분리시킨다.
달라지는 부분을 찾아서 나머지 코드에 영향을 주지 않도록 "캡술화"를 시켜준다.

 

Duck클래스를 분석해보자.

오리마다 달라지는 부분은 fly() 와 quack() 가 있다.

이러한 행동을 Duck 클래스로부터 분리시키기 위해 각 행동을 나타낼 새로운 클래스의 집합을 만들어 준다.

 

즉, 각 행동은 인터페이스로 표현하고

(FlyBehavior/QuackBehavior)

 

각 행동을 구현할 때 해당 인터페이스를 구현하도록 한다.

(FlyWithWings/MuteQuack)


 

디자인 원칙2
구현이 아닌 인터페이스에 맞춰서 프로그래밍 한다.

행동에 관한 인터페이스가 생기고 구체적인 행동을 구현하는 클래스들이 각각 생성이 된다.

행동에 따라서 각 인터페이스으 구현이 집합 클래스 형태로 나타난다.

=>FlyBehavior의 집합과 QuackBehavior 집합.

이제 더이상 Duck에서 나는 행동과 소리를 내는 행동을 Duck 클래스나 그 서브클래스에서 구현하지않고 다른클래스에게 위임을 해주게 된다. 그리고 오리의 기능이 바뀌어도 언제든 재 사용할 수 있으며 새로운 기능이 추가되어도 기존의 코드에는 영향을 끼치지 않는다.

예를들어, 꽥꽥 소리를 내는 Quack클래스를 사용하다가 그 오리는 소리를 내지 않아야 한다면 MuteQuack클래스를 사용하면 된다.

꺼억꺼억 소리를 내는 새로운 기능이 필요하다면 QuackBehavior 인터페이스를 상속받아 새로운 클래스로 구현하면 된다.

 

디자인원칙3
상속보다는 구성을 활용한다.
아래애 설명할 Duck클래스는 위에 구현된 행동 클래스 집합들을 상속받는 것이 아니라, Duck클래스 안에 구성됨으로써 행동을 부여받게 된다.

 

 

기존 Duck코드와 비교해보면, 행동관련 메소드는 Duck클래스에서 직접 수행하는 것이 아니라, 각 행동 클래스에게 위임했다는 것을 알 수 있다. 이 코드에서는 오리가 어떤 소리를 내는 지, 어떻게 나는지는 중요하지 않다. 그저 해당 행동을 실행시킬 수 있다는 것이 중요하다.

 

이렇게 하면 MallardDuck에서 상속받은 performQuack()메서드가 호출된다. 이 메서드에서는 객체의 QuackBehavior에게 할 일을 위임한다. 즉 quackBehavior에게 레퍼런스의quack()메서드가 호출된다. performFly()또한 마찬가지다.

 

실행 중에 오리의 행동을 바꿀 수 있도록, 즉 동적으로 코드를 변경해보자.

Duck 클래스에 추가


4. 다시 정의 확인

Strategy Pattern이란, 객체들이 할 수 있는 행위를 각각에 대해 클래스로 생성하고, 유사한 행위들을 캡슐화하는 인터페이스를 정의하여 객체의 행위를 동적으로 바꾸고 싶은 경우 직접 행위를 수정하지 않고 클래스를 바꿔주기만 함으로써 행위를 유연하게 확장하는 방법을 말한다.

 


이미지 출처: https://jusungpark.tistory.com/7 [정리정리정리]

728x90
반응형