전략패턴은 알고리즘군을 정의하고 캡슐화해서 각각의 알고리즘군을 수정해서 쓸수 있게 해준다.
전략 패턴을 사용하면 클라이언트로부터 알고리즘을 분리해서 독립적으로 변경이 가능하다.
행동을 사용하는 쪽(Context)과 실제 구현(Strategy)을 분리해서, 상황에 따라 다른 로직을 쉽게 바꿔 끼울 수 있게 하는 것이 핵심이다. 아래 3가지의 핵심구조를 가진다.
Strategy 인터페이스
공통 동작 정의
Concrete Strategy
실제 알고리즘 구현
Context
전략을 주입받아 실행만 담당
Context
└─ strategy.execute()
Strategy (interface)
├─ flyWithWing
├─ flyWithRocket
└─ canNotFly
"무엇을 할지"는 Context가 알고 "어떻게 할지"는 Strategy가 결정한다
아래처럼 오리를 만드는 class가 있다.
class Duck {
quack() {
console.log("꽥꽥");
}
display() {
console.log("오리 모양");
}
}
class RedheadDuck extends Duck {
display() {
console.log("붉은머리 오리 모양");
}
}
위의 코드에 오리가 날수 있는 기능을 추가해달라는 요구 사항이 들어왔다. + 고무 오리, 모형 오리가 추가되었다.
class Duck_1 {
quack() {
console.log("꽥꽥");
}
display() {
console.log("오리 모양");
}
fly() {
console.log("날기");
}
}
class RedheadDuck_1 extends Duck_1 {
display() {
console.log("붉은머리 오리 모양");
}
}
class RubberDuck_1 extends Duck_1 {
display() {
console.log("고무 오리 모양");
}
quack() {
console.log("삑삑");
}
fly() {
console.log("날 수 없음");
}
}
class DecoyDuck_1 extends Duck_1 {
display() {
console.log("모형 오리 모양");
}
quack() {
console.log("소리낼 수 없음");
}
fly() {
console.log("날 수 없음");
}
}
위의 코드의 문제점:
- 날수 없는 오리나 소리내는 방식이 다른 오리를 추가시 항상 override 해야 함
- 오리의 종류가 추가될수록 코드가 복잡해짐
- 오리가 어떤 기능을 가지고 있는지 코드만으로 파악하기 어려움
interface QuackBehavior {
quack(): void;
}
interface FlyBehavior {
fly(): void;
}
class Duck_2 {
display(){
console.log('오리 모양');
}
}
class RedheadDuck_2 extends Duck_2 implements QuackBehavior, FlyBehavior {
display() {
console.log('붉은머리 오리 모양');
}
quack(): void {
console.log('꽥꽥');
}
fly(): void {
console.log('날기');
}
}
class RubberDuck_2 extends Duck_2 implements QuackBehavior {
display() {
console.log('고무 오리 모양');
}
quack(){
console.log('삑삑');
}
}
class DecoyDuck_2 extends Duck_2 {
display() {
console.log('모형 오리 모양');
}
}
위의 코드의 문제점:
fly, quack 메서드를 재사용하지 않으므로 동작을 바꾸기 위해서는 모든 오리 클래스를 고쳐야함
// 구체적인 행동을 구현하는 클래스
class FlyWithWings implements FlyBehavior {
fly(): void {
console.log('오리 날아요~');
}
}
class FlyNoWay implements FlyBehavior {
fly(): void {
console.log('날 수 없어요');
}
}
class Quack implements QuackBehavior {
quack(): void {
console.log('꽥꽥!');
}
}
class Spueak implements QuackBehavior {
quack(): void {
console.log('삑삑!');
}
}
class MuteQuack implements QuackBehavior {
quack(): void {
console.log('무음');
}
}
// Context 클래스
class Duck_3 {
quackBehavior: QuackBehavior;
flyBehavior: FlyBehavior;
constructor(quackBehavior: QuackBehavior, flyBehavior: FlyBehavior) {
this.quackBehavior = quackBehavior;
this.flyBehavior = flyBehavior;
}
performQuack() {
this.quackBehavior.quack();
}
performFly() {
this.flyBehavior.fly();
}
display() {
console.log('오리 모양');
}
}
// 오리들
class RedheadDuck_3 extends Duck_3 {
constructor() {
super(new Quack(), new FlyWithWings());
}
display() {
console.log('붉은머리 오리 모양');
}
}
class RubberDuck_3 extends Duck_3 {
constructor() {
super(new Spueak(), new FlyNoWay());
}
display() {
console.log('고무 오리 모양');
}
}
class DecoyDuck_3 extends Duck_3 {
constructor() {
super(new MuteQuack(), new FlyNoWay());
}
display() {
console.log('모형 오리 모양');
}
}
위의 코드에서 동적으로 행동을 지정할 수 있게 하려면 setter을 사용하면 된다.
class Duck_4 {
quackBehavior: QuackBehavior;
flyBehavior: FlyBehavior;
constructor(quackBehavior: QuackBehavior, flyBehavior: FlyBehavior) {
this.quackBehavior = quackBehavior;
this.flyBehavior = flyBehavior;
}
setQuackBehavior(quackBehavior: QuackBehavior) {
this.quackBehavior = quackBehavior;
}
setFlyBehavior(flyBehavior: FlyBehavior) {
this.flyBehavior = flyBehavior;
}
performQuack() {
this.quackBehavior.quack();
}
performFly() {
this.flyBehavior.fly();
}
display() {
console.log('오리 모양');
}
}
class RubberDuck_4 extends Duck_4 {
constructor() {
super(new Spueak(), new FlyNoWay());
}
display() {
console.log('고무 오리 모양');
}
}
const duck_4 = new RubberDuck_4();
duck_4.performQuack();
duck_4.performFly();
duck_4.setQuackBehavior(new Quack());
duck_4.setFlyBehavior(new FlyWithWings());
duck_4.performQuack();
duck_4.performFly();
// 출력:
// 삑삑!
// 날 수 없어요
// 꽥꽥!
// 오리 날아요~
type QuackBehaviorFn = () => void;
type FlyBehaviorFn = () => void;
const flyBehaviors = {
withWings: (): void => console.log('오리 날아요~'),
noWay: (): void => console.log('날 수 없어요'),
} as const;
const quackBehaviors = {
quack: (): void => console.log('꽥꽥!'),
squeak: (): void => console.log('삑삑!'),
mute: (): void => console.log('무음'),
} as const;
const createDuck = (
name: string,
quackBehavior: QuackBehaviorFn,
flyBehavior: FlyBehaviorFn
) => ({
name,
quack: quackBehavior,
fly: flyBehavior,
display: () => console.log(`${name} 오리 모양`),
});
const prettyDuck = createDuck(
"예쁜",
quackBehaviors.quack,
flyBehaviors.withWings,
);
prettyDuck.display();
prettyDuck.quack();
prettyDuck.fly();
// 예쁜 오리 모양
// 꽥꽥
// 오리 날아요~
위의 예시는 책의 java 예제를 js버전으로 만든거라 이번엔 프론트에서 쓸법한 예제를 가져와 보았다.
결제 모듈 구현
interface PaymentStrategy {
pay(amount: number): void;
}
class CardPayment implements PaymentStrategy {
pay(amount: number) {
console.log(`카드 결제: ${amount}`);
}
}
class KakaoPayment implements PaymentStrategy {
pay(amount: number) {
console.log(`카카오페이 결제: ${amount}`);
}
}
class NaverPayment implements PaymentStrategy {
pay(amount: number) {
console.log(`네이버페이 결제: ${amount}`);
}
}
class PaymentContext {
constructor(private strategy: PaymentStrategy) {}
setStrategy(strategy: PaymentStrategy) {
this.strategy = strategy;
}
execute(amount: number) {
this.strategy.pay(amount);
}
}
const context = new PaymentContext(new CardPayment());
context.execute(10000);
context.setStrategy(new KakaoPayment());
context.execute(20000);
위의 class형 예시를 함수형으로 만들어 보았다.
결제 모듈 구현
type PaymentStrategy = (amount: number) => void;
const paymentStractegy = {
cardPay: (amount: number) => console.log(`카드 결제: ${amount}`);
kakaoPay: (amount: number) => console.log(`카카오페이 결제: ${amount}`);
naverPay: (amount: number) => console.log(`네이버페이 결제: ${amount}`);
}
const payment = (amount: number, strategy: PaymentStrategy) => strategy(amount);
payment(1000, paymentStractegy.naverPay);
✔ 조건문이 많아질 때
if / switch 분기 폭증
✔ 알고리즘이 자주 바뀔 때
할인 정책, 결제 방식, 정렬 방식, 인증 로직
✔ 런타임에 동작을 바꿔야 할 때
사용자 선택 기반