스트래티지 패턴(strategy pattern) 이란?1. 일반 객체지향 기법으로 구현을 했을 때의 문제1-1. 간단한 오리 연못 시뮬레이션 게임을 만들어보자1-2. 요구사항의 변경이 생겼다.1-3. 그런데 심각한 문제가 발생했다. (문제1)1-4. Duck 클래스의 단점2. 인터페이스 활용2-1. 상속대신 인터페이스를 사용한다면?2-2 이 또한 문제다. (문제2)3. 문제 파악 하기3-1 행동 디자인4. 행동기반의 Duck 클래스4-1 MallardDuck4-2 동적인 Duck 클래스 5. 정리5-1. 이제 다시 스트래티지 패턴의 정의를 읽어보자.
스트래티지 패턴(strategy pattern) 이란?
- 스트래티지 패턴에서는
알고리즘군
을 정의하고 각각을캡슐화
하여 교환해서 이용할 수 있도록 만든다. - 스트래티지를 활용하면 알고리즘을 사용하는 클라이언트의 영향 없이 독립적으로 알고리즘을 변경할 수 있다.
이해하였는가?
- 디자인패턴은 정의와 예제만 읽고 이해할 수 없다. 왜 이런 패턴이 필요하고 없으면 어떤게 무엇인지 정확하게 알고 이해하는 것이 중요하다.
- 이 글을 읽고나서 다시 정의를 읽어보면 그 땐 모두 이해가 갈 것이다!
1. 일반 객체지향 기법으로 구현을 했을 때의 문제
1-1. 간단한 오리 연못 시뮬레이션 게임을 만들어보자
여러분이 오리 연못 시뮬레이션 게임을 만드는 회사에 다니고 있다고 가정해보자.
이 게임에서는 헤엄치고 꽥꽥 울음 소리를 내는, 매우 다양한 오리를 보여준다.
그래서 객체지향 기법을 이용하여 Duck이라는 수퍼클래스를 만들고, 이 클래스를 상속받아 다양한 종류의 오리 클래스를 만들었다.
요구사항
- 모든 오리는 꽥꽥 소리를 낼 수 있고, 헤엄을 칠 수 있다.
- 오리는 모양새가 다르다.
결과
공통사항을 하나로 묶은 Duck 추상 클래스 생성
- quack: 꽥꽥 우는 소리는 내는 기능
- swim: 수영하는 기능
- display: 인터페이스(오리는 모양이 다르기 때문에 하위 클래스에서 구현)
Duck 클래스를 상속받아 다양한 오리 클래스 생성
- 청둥오리
- 미국흰죽지
- 고무오리
public abstract class Duck {
public void quack() {
System.out.println("꽥꽥운다");
}
public void swim() {
System.out.println("헤엄친다");
}
abstract void display();
}
xxxxxxxxxx
public class MallardDuck extends Duck{
void display() {
System.out.println("천둥오리 생김새...");
}
}
xxxxxxxxxx
public class RedheadDuck extends Duck{
void display() {
System.out.println("미국흰죽지 생김새...");
}
}
1-2. 요구사항의 변경이 생겼다.
- 회사 임원들이 이 게임은 오리가 날라다닐 수 있도록 해야 대박이 터질거라고 이야기했다.
- 여러분은 Duck 클래스에 fly()메소드만 추가하면 아주 쉽게 모든것이 해결 될거라고 생각할 것이다.
xxxxxxxxxx
public abstract class Duck {
public void quack() {
System.out.println("꽥꽥운다");
}
public void swim() {
System.out.println("헤엄친다");
}
//fly 메소드 추가
public void fly() {
System.out.println("하늘을 난다");
}
abstract void display();
}
1-3. 그런데 심각한 문제가 발생했다. (문제1)
상속 받은 모든 서브클래스 오리가 날 수 있는게 아니라는 것을 깜빡했다.
fly() 메소드가 추가되면서 고무오리에게 적합하지 않은 행동이 추가됐다.
한 부분의 코드 추가로 전체 프로그램에 부작용이 나타난 것이다.
우선 메소드를 재정의 함으로써 급한불을 껐다.
xxxxxxxxxx
public class RubberDuck extends Duck{
void display() {
System.out.println("고무오리 생김새...");
}
public void quack() {
//고무오리는 꽥꽥 울지 않기 때문에, 삑삑소리를 내도록 변경
System.out.println("삑삑");
}
public void fly() {
//날지못하기 때문에 아무것도 수행하지 않는 함수로 변경
}
}
만약 울지도 못하고 날지도 못하는 나무 오리가 추가되면 어떻게 해야할까?
클래스를 상속받고, 아무것도 수행하지 않도록 또 재정의 해야한다.
객체지향 기법으로 구현을 했지만, 과연 이게 효율적인 프로그래밍인 것인가? 하고 의심이 들기 시작한다.
1-4. Duck 클래스의 단점
서브클래스에서 코드가 중복된다.
- 슈퍼클래스에서 제공하는 기능과 다른 기능을 구현하고 싶을 땐, 하위 클래스에서 메소드를 재정의한다.
- 이런 하위 클래스가 여러 개일 경우, 같은 코드를 계속 재정의 함으로써 코드가 중복돤다.
프로그램 실행 시, 오리의 특징을 바꾸기 힘들다.
- 한 번 천둥오리로 생성된 클래스는 프로그램이 종료될때까지 천둥오리로 남아있다.
모든 오리의 행동을 알기 어렵다.
- 변수가 참 많습니다. 오리가 날거라고 상상이나 했겠습니까?
- 다음에는 또 어떤 엉뚱한 기능들이 추가될까요
코드를 변경했을 때, 원치 않는 영향을 끼칠 수 있다.
- 기능을 추가해달라고 해서 추가했는데, 심각한 문제가 발생했다.
2. 인터페이스 활용
2-1. 상속대신 인터페이스를 사용한다면?
- 위의 문제를 해결하기 위해, 오리마다 달라질 수 있는 기능들을 인터페이스로 구현해보자
- 이렇게 하면 날 수 있는 오리들은 Flyable 인터페이스를 구현하면 될 것이고,
- 꽥꽥 우는 오리들은 Quackble 인터페이스 구현하면 되겠다.
- 그리고 새롭게 추가되는 기능들은 ~ble 형태의 인터페이스를 만들어서 추가하면된다.
- 해당 기능이 없는 오리들은 인터페이스를 상속받지 않으면 된다.
2-2 이 또한 문제다. (문제2)
기존에 있는 기능에 문제가 생겨 수정해야하는 경우가 생겼다고 가정해보자.
날아다니는 동작을 조금 바꾸기위해, Flyble을 인터페이스로 구현한 모든 서브 클래스들을 다 고쳐야한다.
- 만약 Flyble을 상속받아 구현한 오리 클래스가 50개가 넘어간다면...? 끔찍할 것이다..
즉, 이러한 구조로는 코드를 재사용할 수 없다.
- Flyble과 Quackble은 코드가 없는 인터페이스이기 때문
이러한 문제를 해결하기 위해 디자인패턴을 배우는 것이고, 스트래티지 패턴을 설명하기 앞서 장황하게 문제점들을 설명했습니다.
3. 문제 파악 하기
상속을 사용하는 것이 그리 성공적이지 못하다는 것을 확실히 알게되었고, Flyable과 Quackable와 같이 인터페이스를 사용하는 방법은 문제를 해결하는 듯 싶었으나 코드를 재사용 할 수 없다는 문제점이 있었다.
디자인 원칙1
- 애플리케이션에서 달라지는 부분을 찾아내고, 달라지지 않는 부분으로부터 분리시킨다.
- 즉, 바뀌는 부분은 따로 캡슐화시킨다. 그렇게 하면 나중에 바뀌지 않는 부분에는 영향끼치지 않고, 바뀌는 부분만 고치거나 확장할 수 있다.
Duck 클래스를 분석해보자
fly()와 quack()은 기존 Duck클래스에서 달라지는 부분이기 때문에, 이러한 행동을 클래스에서 끄집어내어 각 클래스 집합 형태로 만들어 보자
3-1 행동 디자인
최대한 유연하게 만드는 것이 중요하다.
- 애초에 오리의 행동들이 유연하지 못해 발생한 문제들이었다.
- 프로그램 시작 시, 오리의 행동을 초기화 하거나 중간에 동적으로 변경할 수 있게 만들어야한다.
각 행동은 인터페이스로 표현하고,
- 예) FlyBehavior, QuackBehavior
각 행동을 구현할 때 해당 인터페이스를 구현하도록 만든다.
- 예) FlyWithWings, MuteQuack 등
디자인 원칙2
- 구현이 아닌 인터페이스에 맞춰서 프로그래밍한다.
행동에 따라서 각 인터페이스의 구현이 집합 클래스 형태로 나타난다.
- FlyBehavior 인터페이스 집합: FlyWithWings, FlyNoWay 클래스
- QuackBehavior 인터페이스 집합: Quack, MuteQuack, Squeak 클래스
오리의 기능이 바뀌어도 언제든지 재사용할 수 있으며, 새로운 기능이 추가되어도 기존의 코드에는 영향을 끼치지 않는다.
- 예를들어, 꽥꽥 소리를 내는 Quack클래스를 사용하다가 그 오리는 소리를 내지 않아야 한다면 MuteQuack 클래스를 사용 하면 된다.
- 꺼억꺼억 소리를 내는 새로운 클래스가 필요하다면, QuackBehavior 인터페이스를 상속받아 새로운 클래스를 구현하면 된다.
디자인 원칙3
상속보다는 구성을 활용한다.
- 아래에서 설명할 Duck 클래스는 위에 구현된 행동 클래스 집합들을 상속받는 것이 아니라, Duck클래스 안에 구성됨으로써 행동을 부여받게 된다.
- 이러한 구성을 활용하여 시스템을 만들면 유연성을 크게 향상시킬 수 있다.
4. 행동기반의 Duck 클래스
우리는 위에서 여러 행동 클래스들을 구현했다. 이제는 Duck 클래스를 구현할 차례이다.
Duck 클래스는 그저 각 인터페이스의 인스턴스변수를 추가하면 된다.
- 즉, 우는 행동과 나는 행동은 Duck 클래스에 구성된다는 뜻이다.
xpublic abstract class Duck {
FlyBehavior flyBehavior;
QuackBehavior quackBehavior;
public void performQuack() {
this.quackBehavior.quack();
}
public void performFly() {
this.flyBehavior.fly();
}
//기타 오리 관련 메소드
abstract public void swim();
abstract public void display();
}
기존 Duck 코드와 비교해보면, 행동관련 메소드는 Duck클래스에서 직접 수행하는 것이 아니라, 각 행동클래스에게 위임했다는 것을 알 수 있다.
이 코드에서는 오리가 어떤 소리를 내는지, 어떻게 나는지 중요하지 않다. 그저 해당 행동을 실행시킬 수 있다는 것이 중요하다.
어떤 행동이 수행되는지는 각 멤버 인스턴스에 할당되는 행동 클래스에 따라 달라질 것이다.
이제 이 Duck 클래스를 이용하여 실제 오리 클래스를 만들어보자
4-1 MallardDuck
xxxxxxxxxx
public class MallardDuck extends Duck {
public MallardDuck() {
quackBehavior = new Quack();
flyBehavior = new FlyWithWings();
}
public void swim() {
System.out.println("헤엄은 매우 잘해요.");
}
public void display() {
System.out.println("물오리 입니다.");
}
}
xxxxxxxxxx
public class FlyWithWings implements FlyBehavior {
public void fly() {
System.out.println("하늘을 날 수 있어요!");
}
}
xxxxxxxxxx
public class Quack implements QuackBehavior {
public void quack() {
System.out.println("꽥꽥 운다.");
}
}
xxxxxxxxxx
public class Main {
public static void main(String[] args) {
Duck mallard = new MallardDuck();
mallard.performFly();
mallard.performQuack();
}
}
핵심은 다형성이다.
프로그램의 기능이 유연하게 바뀌기 위해서는 다형성이 굉장히 큰 핵심이다.
xxxxxxxxxx
Duck mallard = new MallardDuck();
quackBehavior = new Quack();
flyBehavior = new FlyWithWings();
오리와 오리의 행동이 고정된 것이 아니라, 아래와 같이 다형성에 의해 얼마든지 바뀔 수 있다는 것
xxxxxxxxxx
Duck mallard = new RubberDuck();
quackBehavior = new MuteQuack();
flyBehavior = new FlyNoWay();
하지만 현재 위의 코드 자체는 유연하여 코드를 바꿔 쉽게 동작을 바꿀 순 있지만, 프로그램 실행 중에 동작을 바꿀 순 없다.
파라미터를 활용하여 동적으로 바뀌도록 조금 고쳐보자
4-2 동적인 Duck 클래스
사실 말은 거창하지만, Duck클래스에 그저 set 메소드를 추가하여 행동을 할당하도록 구현하면 된다.
xxxxxxxxxx
public void setFlyBehavior(FlyBehavior flyBehavior) {
this.flyBehavior = flyBehavior;
}
public void setQuackBehavior(QuackBehavior quackBehavior) {
this.quackBehavior = quackBehavior;
}
xxxxxxxxxx
public class Main {
public static void main(String[] args) {
Duck mallard = new MallardDuck();
mallard.performFly();
mallard.performQuack();
//행동 변경
mallard.setFlyBehavior(new FlyNoWay());
mallard.setQuackBehavior(new MuteQuack());
mallard.performFly();
mallard.performQuack();
}
}
5. 정리
지금까지 오리 시뮬레이터 디자인을 아주 깊이 파헤쳐보았다.
오리들은 Duck클래스를 상속받아 확장해서 만들었고
오리마다 달라질 수 있는 행동은 FlyBehavior와 QuackBehavior를 구현해서 만들었다.
이번에는 생각을 조금 바꿔, 이 일련의 행동들을 알고리즘군으로 생각해보자
이제 아래 큰 그림을 보면서 스트래티지 패턴이 정확히 무엇인지 알아보자
클라이언트에서는 각각 캡슐화된 알고리즘군(오리의 행동)을 활용한다.
FlyWithWings, MuteQuack 이러한 클래스들은 알고리즘군이다.
이러한 알고리즘군은 바뀔수도 있고, 새롭게 추가 될 수 있다.
해당 패턴에서는 새로운 기능이 추가될 때 다른 알고리즘에 영향을 끼치지 않는다.
- 1-3 항목 문제 해결
기능이 바뀌거나 버그가 생겨 코드가 변경되면 해당 알고리즘만 고치면 된다.
- 2-2 항목 문제 해결
5-1. 이제 다시 스트래티지 패턴의 정의를 읽어보자.
스트래티지 패턴(strategy pattern) 이란?
- 스트래티지 패턴에서는 알고리즘군을 정의하고 각각을 캡슐화하여 교환해서 이용할 수 있도록 만든다.
- 스트래티지를 활용하면 알고리즘을 사용하는 클라이언트의 영향 없이 독립적으로 알고리즘을 변경할 수 있다.
Head First Design Patterns 라는 책을 읽고 정리하였습니다.
디자인패턴 처음 공부할 때 읽기 좋은 책인 것 같습니다. 모든 디자인패턴을 다루진 않지만,
중요한 디자인패턴은 모두 다루네요.
GoF패턴책으로 공부하다가 너무 어려워서 포기했는데 이렇게 좋은책이 있었네요.
'Design pattern' 카테고리의 다른 글
데코레이터 패턴(decorator pattern) 정리 (0) | 2020.07.06 |
---|---|
옵저버 패턴(observer pattern) 정리 (0) | 2020.06.17 |
댓글