전략 패턴(Strategy Pattern), 알고리즘을 객체로 캡슐화하기

2026. 7. 14. 10:08·🍞 BackEnd
반응형

1. 들어가며

기능을 하나씩 추가하다 보면 어느 순간 if-elif-else 블록이 감당하기 힘든 크기로 불어나 있는 걸 발견할 때가 있다. 특히 "조건에 따라 다른 방식으로 동작해야 하는" 로직—할인 정책, 정렬 방식, 결제 수단 처리 같은—을 다룰 때 이런 현상이 자주 나타난다.

이번에 학습하면서 이 문제를 정면으로 다루는 디자인 패턴이 바로 전략 패턴(Strategy Pattern) 이라는 걸 알게 되어 정리해본다.

2. 전략 패턴이란

전략 패턴은 알고리즘군을 각각 하나의 객체로 캡슐화하고, 이들을 서로 교환 가능하게 만드는 행위 패턴(Behavioral Pattern)이다. GoF의 정의를 풀어보면 다음과 같다.

동일한 목적을 가진 여러 알고리즘을 각각 별도의 클래스로 캡슐화하고, 이를 클라이언트가 런타임에 선택적으로 교체할 수 있도록 하는 패턴

핵심은 "무엇을 할지(what)"를 사용하는 쪽(Context)과 "어떻게 할지(how)"를 구현하는 쪽(Strategy)을 분리한다는 점이다. 이렇게 분리해두면 알고리즘이 추가되거나 변경되어도 그것을 사용하는 코드는 건드릴 필요가 없어진다.

3. 구조

전략 패턴은 보통 세 가지 요소로 구성된다.

구성 요소역할
Strategy 알고리즘군에 대한 공통 인터페이스 정의
ConcreteStrategy Strategy 인터페이스를 구현한 실제 알고리즘
Context Strategy 객체를 참조하며, 클라이언트 요청을 실제 알고리즘에 위임

Context는 자신이 어떤 ConcreteStrategy를 사용하는지 알 필요가 없다. 그저 Strategy 인터페이스만 바라보고 호출할 뿐이다.

4. 코드로 비교해보기

Before: 조건문으로 알고리즘을 분기하는 경우

class DiscountCalculator:
    def calculate(self, price: float, discount_type: str) -> float:
        if discount_type == "fixed":
            return price - 3000
        elif discount_type == "percentage":
            return price * 0.9
        elif discount_type == "vip":
            return price * 0.7
        else:
            return price

당장은 문제없어 보이지만, 할인 정책이 하나 추가될 때마다 elif가 늘어나고, 이 클래스를 수정해야 한다. 즉 새로운 기능을 추가하기 위해 기존 코드를 열어서 고쳐야 하는 상황이 반복된다. 이는 "확장에는 열려 있고, 변경에는 닫혀 있어야 한다"는 OCP(Open-Closed Principle)를 정면으로 위배하는 구조다.

After: 전략 패턴 적용

from abc import ABC, abstractmethod

# Strategy
class DiscountStrategy(ABC):
    @abstractmethod
    def apply(self, price: float) -> float:
        pass

# ConcreteStrategy
class FixedDiscount(DiscountStrategy):
    def apply(self, price: float) -> float:
        return price - 3000

class PercentageDiscount(DiscountStrategy):
    def apply(self, price: float) -> float:
        return price * 0.9

class VipDiscount(DiscountStrategy):
    def apply(self, price: float) -> float:
        return price * 0.7

# Context
class DiscountCalculator:
    def __init__(self, strategy: DiscountStrategy):
        self._strategy = strategy

    def calculate(self, price: float) -> float:
        return self._strategy.apply(price)

이제 새로운 할인 정책이 생기면 DiscountStrategy를 구현하는 클래스를 하나 추가하기만 하면 된다. DiscountCalculator는 전혀 수정할 필요가 없다. 알고리즘이 각각 독립된 객체로 캡슐화되었기 때문이다.

5. 런타임에 전략 갈아끼우기

전략 패턴의 또 다른 핵심은 이 알고리즘 객체를 컴파일 타임이 아니라 런타임에 주입하고 교체할 수 있다는 점이다.

calculator = DiscountCalculator(PercentageDiscount())
print(calculator.calculate(10000))  # 9000.0

# 실행 도중 전략을 교체
calculator._strategy = VipDiscount()
print(calculator.calculate(10000))  # 7000.0

사용자의 등급, 요청 파라미터, 설정값 등 실행 시점에만 알 수 있는 조건에 따라 알고리즘을 동적으로 바꿔 끼울 수 있다는 것이 이 패턴의 실용적인 강점이다. 생성자 주입, setter 주입, 팩토리를 통한 주입 등 다양한 방식으로 구현할 수 있다.

6. 장단점 정리

구분내용
장점 알고리즘을 독립적으로 캡슐화하여 단일 책임 원칙(SRP) 준수
장점 새로운 전략 추가 시 기존 코드 변경 불필요 → OCP 준수
장점 런타임에 알고리즘을 유연하게 교체 가능
장점 조건문(if-else) 분기를 제거해 코드 가독성 향상
단점 전략 클래스 수가 늘어나면서 클래스 개수 자체가 많아짐
단점 클라이언트가 각 전략의 차이를 알고 있어야 적절히 선택 가능
단점 전략이 단순한 경우 오히려 오버엔지니어링이 될 수 있음

7. 실무에서는 어떻게 쓰일까

전략 패턴은 생각보다 익숙한 곳에 이미 녹아 있다.

  • 결제 시스템: 카드/계좌이체/간편결제 등 결제 수단별 처리 로직을 각각의 전략으로 분리
  • 정렬/검색: 정렬 기준이나 알고리즘을 상황에 따라 교체 (예: Python의 sorted(key=...)도 넓게 보면 전략 패턴의 정신을 담고 있다)
  • 인증 방식: JWT, OAuth, 세션 기반 인증 등을 인터페이스로 추상화해두고 설정에 따라 교체
  • FastAPI의 의존성 주입: Depends()를 통해 요청 시점에 서로 다른 구현체를 주입하는 방식도 전략 패턴과 맞닿아 있는 부분이 있다

이렇게 놓고 보면 전략 패턴은 특정 언어나 프레임워크에 국한된 개념이라기보다, "행위를 객체로 다루고 교체 가능하게 만든다"는 하나의 설계 관점에 가깝다는 생각이 들었다.

8. 어댑터 패턴과의 비교

전략 패턴을 공부하다 보면 구조가 비슷해 보이는 어댑터 패턴(Adapter Pattern) 과 자주 비교된다. 둘 다 "인터페이스를 통해 실제 구현을 감싼다"는 형태를 띠기 때문인데, 목적 자체는 완전히 다르다.

어댑터 패턴이란

어댑터 패턴은 호환되지 않는 인터페이스를 가진 클래스를, 클라이언트가 기대하는 인터페이스로 변환해주는 구조 패턴(Structural Pattern)이다. 이미 존재하지만 그대로는 사용할 수 없는 외부 클래스나 레거시 코드를, 새 인터페이스에 맞춰 감싸는 용도로 쓴다.

# 기존에 존재하는, 인터페이스가 다른 외부 라이브러리 클래스
class LegacyPaymentGateway:
    def send_payment(self, amount_in_cents: int) -> str:
        return f"{amount_in_cents}센트 결제 처리 완료"

# 우리 시스템이 기대하는 인터페이스
class PaymentProcessor(ABC):
    @abstractmethod
    def pay(self, amount: float) -> str:
        pass

# 어댑터: 인터페이스 불일치를 해소
class LegacyPaymentAdapter(PaymentProcessor):
    def __init__(self, legacy_gateway: LegacyPaymentGateway):
        self._legacy_gateway = legacy_gateway

    def pay(self, amount: float) -> str:
        amount_in_cents = int(amount * 100)
        return self._legacy_gateway.send_payment(amount_in_cents)

PaymentProcessor 인터페이스만 알고 있는 클라이언트 입장에서는, 내부적으로 LegacyPaymentGateway를 쓰는지 전혀 알 필요가 없다. 어댑터가 그 차이를 흡수해준다.

전략 패턴과 무엇이 다른가

구분전략 패턴어댑터 패턴
분류 행위 패턴(Behavioral) 구조 패턴(Structural)
목적 동일한 인터페이스를 가진 여러 알고리즘을 교체 가능하게 함 서로 다른 인터페이스를 호환 가능하게 변환함
초점 "어떤 알고리즘을 쓸 것인가"의 선택과 교체 "기존 코드를 어떻게 재사용할 것인가"의 호환
구현체 관계 ConcreteStrategy들은 대등한 관계로, 처음부터 같은 인터페이스로 설계됨 Adaptee는 원래 다른 인터페이스를 가지고 있었고, 어댑터가 사후적으로 맞춰줌
전형적 사용 시점 새로운 알고리즘을 설계 단계에서부터 유연하게 넣고 싶을 때 이미 존재하는 외부 라이브러리나 레거시 코드를 그대로 재사용해야 할 때

정리하면, 전략 패턴은 "여러 알고리즘 중 하나를 골라 끼운다"는 선택의 문제를 다루고, 어댑터 패턴은 "이미 있는 것을 억지로라도 맞춰 쓴다"는 호환의 문제를 다룬다. 두 패턴이 코드 형태만 보면 비슷해 보이는 이유는, 둘 다 인터페이스를 사이에 두고 실제 구현을 감싸는 방식을 쓰기 때문이지 목적이 같아서가 아니다.

9. 마무리

전략 패턴을 학습하면서 가장 인상 깊었던 부분은, 이 패턴이 단순히 "if문을 줄이는 기법"이 아니라 변화가 예상되는 지점을 미리 식별하고, 그 지점을 객체 경계로 분리해두는 설계 습관에 가깝다는 점이었다. 조건 분기가 늘어나는 코드를 마주쳤을 때, "이 로직이 앞으로도 계속 늘어날 가능성이 있는가?"를 먼저 질문해보는 것이 전략 패턴을 적용할지 판단하는 기준이 될 수 있을 것 같다.

이번에 어댑터 패턴과 비교해보면서, 겉모습이 비슷한 패턴이라도 "왜 이 구조를 쓰는가"라는 목적을 기준으로 구분하는 습관이 필요하다는 걸 다시 느꼈다.

반응형
저작자표시 (새창열림)

'🍞 BackEnd' 카테고리의 다른 글

헥사고날 아키텍처(Hexagonal Architecture), 포트와 어댑터  (0) 2026.07.16
BFF(BackEnd For FrontEnd)를 적용하는 이유에 대해서  (1) 2025.09.18
[Postman] Postman으로 Mock Server 구축하기  (0) 2022.11.10
'🍞 BackEnd' 카테고리의 다른 글
  • 헥사고날 아키텍처(Hexagonal Architecture), 포트와 어댑터
  • BFF(BackEnd For FrontEnd)를 적용하는 이유에 대해서
  • [Postman] Postman으로 Mock Server 구축하기
박빵이
박빵이
칭찬은 박빵도 춤추게 한다
  • 박빵이
    기억보다 기록
    박빵이
  • 전체
    오늘
    어제
    • 분류 전체보기 (336) N
      • 🍞 AI (1)
      • 🍞 Cloud·Infra (1)
        • AWS SAA (1)
      • 🍞 FrontEnd (89)
        • HTML · CSS (4)
        • JavaScript (16)
        • TypeScript (3)
        • React (45)
        • Next.js (1)
        • Android (15)
      • 🍞 BackEnd (4) N
        • FastAPI (1)
        • Java (15)
        • Node.js (6)
        • SpringBoot (1)
      • 🍞 Algorithm (147)
        • C++ (4)
        • Baekjoon (41)
        • Programmers (97)
      • 🍞 Computer Science (18)
        • 운영체제 (1)
        • 네트워크 (6)
        • 데이터 통신 (6)
        • 데이터베이스 (1)
      • 🍞 대외활동 & 부트캠프 (42)
        • 삼성 청년 SW 아카데미 (1)
        • LG유플러스 유레카 (0)
        • 한국대학생IT경영학회 (1)
        • IT연합동아리 UMC (17)
        • IT연합동아리 피로그래밍 (3)
        • 길벗 블로깅 멘토 (18)
        • 개발 컨퍼런스 (2)
      • 🍞 개발 협업 & 생산성 (2)
  • 블로그 메뉴

    • 글쓰기
    • Admin
  • 링크

    • GitHub
  • 공지사항

  • 인기 글

  • 태그

    플로이드 와샬
    JavaScript
    Android
    길벗 블로깅 멘토링
    유니온파인드
    알고리즘
    프로그래머스
    코딩자율학습
    위상정렬
    코틀린
    Front
    백준
    level2
    안드로이드
    길벗 블로깅 멘토
    react
    umc
    Java
    level1
    C++
  • 최근 댓글

  • 최근 글

  • 반응형
  • hELLO· Designed By정상우.v4.10.6
박빵이
전략 패턴(Strategy Pattern), 알고리즘을 객체로 캡슐화하기
상단으로

티스토리툴바