본문 바로가기
소프트웨어/디자인 패턴

[3] Strategy Pattern

by Riverandeye 2020. 10. 25.

Strategy Pattern은 "변화하는 로직"을 분리하여 구성하는 방식 중 하나입니다.

이 패턴의 핵심은 달라지는 부분을 찾아내고, 달라지지 않는 부분으로부터 분리시킵니다. 

대표적으로 다음과 같은 상황에 Strategy Pattern을 적용합니다. 

 

1. 진짜 변한다 

- 비즈니스 로직이 요구사항에 대해 변하는 경우, 이를 유연하게 대응하기 위함

 

2. 개별 클래스에 대해 달라야 한다

- 동일한 메소드가 개별 클래스에 대해 그 동작이 달라지고, 재사용해야 하는 경우

 

개별 행위를 클래스로 캡슐화하여, 동적으로 행위를 변경할 수 있게 구성하는 것이 Strategy Pattern입니다. 

메소드를 담고 있는 인터페이스를 정의하고

생성자를 통해 그 구현체를 주입하거나, Setter 메소드를 이용해서 동적으로 변경할 수 있습니다.

 

예시

HFDP에 나온 간단한 예시를 구현해보았습니다. 예시를 보며 같이 이해해봅시다. 

다양한 종류의 오리를 만들어야 하는데, 이 오리간 완전히 공통되는 메소드가 없는 것으로 생각이 됩니다. 

예를 들어, 날수 없고 말도 못하는 오리, 날수 있지만 말은 못하는 오리, 날수 없지만 말은 할 수 있는 오리 (닭) 을 구현해야 합니다. 

 

날수 없음을 표현하는 메소드를 공통으로 구성하고 싶고

말할 수 있음을 표현하는 메소드를 공통으로 구성하고 싶은데

Duck이라는 클래스에 개별 메소드를 구현한 후 하위 클래스들이 이를 상속해서 사용하는 것은 바람직하지 않아 보입니다. 

일부 객체는 fly라는 속성이 없을 수도 있기 때문입니다. 

 

이럴 때, 개별 속성을 가지고 있는 인터페이스를 주입하여

해당 인터페이스를 사용하는 방식으로 이를 구현하게 되면

주입한 객체의 속성을 그대로 사용할 수 있어, 재사용성과 유연함을 동시에 잡을 수 있습니다. 

 

IFlyBehavior, IQuackBehavior

export default interface IFlyBehavior {
  fly(): void;
}

export default interface IQuackBehavior {
  quack(): void;
}

날고 꽥꽥대는 행위에 대한 객체를 분리하기 위해 해당 인터페이스를 먼저 생성합니다.

그 후, 개별 인터페이스의 구현체를 생성합니다. 

 

export default class CanQuackBehavior implements IQuackBehavior {
  public quack() {
    console.log("Quack Quack!");
  }
}

export default class MuteQuackBehavior implements IQuackBehavior {
  public quack() {
    console.log("...");
  }
}

export default class CanFlyBehavior implements IFlyBehavior {
  public fly() {
    console.log("I can fly");
  }
}

export default class CannotFlyBehavior implements IFlyBehavior {
  public fly() {
    console.log("I can't fly");
  }
}

다음과 같이 개별 인터페이스에 대한 구현체를 생성합니다. 

이 개별 구현체는, 서로 다른 클래스에 주입되어 생성될 계획입니다. 

 

Duck

import IFlyBehavior from "./fly/interface";
import IQuackBehavior from "./quack/interface";

export default class Duck {
  public constructor(
    private readonly flyBehavior: IFlyBehavior,
    private readonly quackBehavior: IQuackBehavior,
  ) {}

  public fly() {
    this.flyBehavior.fly();
  }

  public quack() {
    this.quackBehavior.quack();
  }
}

Duck에서는 개별 behavior를 주입받고, 메소드에서는 주입받은 behavior를 이용해서 해당 메소드를 구현합니다. 

이렇게 하면, flyBehavior랑 quackBehavior를 동적으로 지정할 수 있을 것입니다. 

 

개별 하위 클래스들은 다음과 같이 Duck을 상속한 후 그 특징을 구현해주면 됩니다. 

 

FlyingDuck

import Duck from ".";

import CanFlyBehavior from "./fly/can-fly-behavior";
import CanQuackBehavior from "./quack/can-quack-behavior";

export default class FlyingDuck extends Duck {
  constructor() {
    super(new CanFlyBehavior(), new CanQuackBehavior());
  }
}

 

여러가지 종류의 오리가 만들어지겠지만, 일부는 공통된 메소드를 공유하게 될 것입니다.

그런 공통된 로직(알고리즘)을 재사용할 수 있게끔 구성하는 것이 이 Strategy Pattern 입니다. 

 

Strategy Pattern (출처 : https://www.hanbit.co.kr/store/books/look.php?p_code=B9860513241)

다이어그램으로 보면 다음과 같습니다.

Duck이라는 녀석이 가져야 할 Behavior는 다른 인터페이스로 추상화 되어있고

실제 주입은 구상 클래스들이 생성자에 들어가게 됩니다. 

 

이 문제를 해결하면서 적용한 디자인 원칙은 다음과 같습니다 .

 

- 어플리케이션에서 달라지는 부분을 찾아내고, 달라지지 않는 부분으로부터 분리한다

- 상속보다는 Composition 을 활용한다

- 구현이 아닌 인터페이스에 맞춰서 프로그래밍한다

 

구현한 소스코드는 다음 링크에서 확인하실 수 있습니다.

github.com/riverandeye/Investment/tree/master/Design_Pattern/strategy/TypeScript

 

정리하면

스트래티지 패턴은 알고리즘을 정의하고 각각을 캡슐화하여 교환해서 사용할 수 있도록 만듭니다. 

 

Reference

Head First Design Pattern

'소프트웨어 > 디자인 패턴' 카테고리의 다른 글

[6] 싱글턴 패턴  (0) 2020.11.05
[5] 팩토리 메소드 패턴  (0) 2020.10.29
[4] Decorator Pattern  (0) 2020.10.28
[2] Observer Pattern  (0) 2020.10.22
[1] Reactor Pattern  (4) 2020.10.19

댓글