전략패턴(헤드퍼스트 디자인패턴 책 스터디)

지리·2026년 2월 21일

전략 패턴

개념

전략패턴은 알고리즘군을 정의하고 캡슐화해서 각각의 알고리즘군을 수정해서 쓸수 있게 해준다.
전략 패턴을 사용하면 클라이언트로부터 알고리즘을 분리해서 독립적으로 변경이 가능하다.

행동을 사용하는 쪽(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("날 수 없음");
  }
}

위의 코드의 문제점:

  1. 날수 없는 오리나 소리내는 방식이 다른 오리를 추가시 항상 override 해야 함
  2. 오리의 종류가 추가될수록 코드가 복잡해짐
  3. 오리가 어떤 기능을 가지고 있는지 코드만으로 파악하기 어려움

해결방법1: quack 과 fly 메서드를 인터페이스로 분리

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 메서드를 재사용하지 않으므로 동작을 바꾸기 위해서는 모든 오리 클래스를 고쳐야함

해결방법2: 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();

// 예쁜 오리 모양
// 꽥꽥
// 오리 날아요~

예시(Class형)

위의 예시는 책의 java 예제를 js버전으로 만든거라 이번엔 프론트에서 쓸법한 예제를 가져와 보았다.

결제 모듈 구현

  1. 전략 인터페이스
interface PaymentStrategy {
  pay(amount: number): void;
}
  1. 전략 구현체
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}`);
  }
}
  1. Context
class PaymentContext {
  constructor(private strategy: PaymentStrategy) {}

  setStrategy(strategy: PaymentStrategy) {
    this.strategy = strategy;
  }

  execute(amount: number) {
    this.strategy.pay(amount);
  }
}
  1. 사용
const context = new PaymentContext(new CardPayment());
context.execute(10000);

context.setStrategy(new KakaoPayment());
context.execute(20000);

예시(함수형)

위의 class형 예시를 함수형으로 만들어 보았다.

결제 모듈 구현

  1. 전략 인터페이스
type PaymentStrategy = (amount: number) => void;
  1. 전략 구현체
const paymentStractegy = {
    cardPay: (amount: number) => console.log(`카드 결제: ${amount}`);
    kakaoPay: (amount: number) => console.log(`카카오페이 결제: ${amount}`);
    naverPay: (amount: number) => console.log(`네이버페이 결제: ${amount}`);
}
  1. Context
const payment = (amount: number, strategy: PaymentStrategy) => strategy(amount);
  1. 사용
payment(1000, paymentStractegy.naverPay);

언제 사용하면 좋은가?

✔ 조건문이 많아질 때

if / switch 분기 폭증

✔ 알고리즘이 자주 바뀔 때

할인 정책, 결제 방식, 정렬 방식, 인증 로직

✔ 런타임에 동작을 바꿔야 할 때

사용자 선택 기반

profile
공부한것들, 경험한 것들을 기록하려 노력합니다✨

0개의 댓글