전략 패턴 (Strategy Pattern): 알고리즘을 교체 가능한 부품처럼 사용하기

안녕하세요, 10년 경력의 소프트웨어 엔지니어이자 기술 교육자입니다. 오늘은 여러분이 실무에서 마주치는 복잡한 조건문을 단순화하고, 코드의 유연성과 확장성을 극대화할 수 있는 강력한 디자인 패턴인 **전략 패턴(Strategy Pattern)**에 대해 깊이 있게 알아보겠습니다. 이 패턴은 객체 지향 설계의 핵심 원칙을 자연스럽게 적용하여, 여러분의 코드를 더욱 견고하고 유지보수하기 쉽게 만들어 줄 것입니다.
개념 소개

정의
전략 패턴은 알고리즘군을 정의하고 각각을 캡슐화하여 런타임에 서로 교체 가능하게 만드는 디자인 패턴입니다. 즉, 특정 작업을 수행하는 여러 가지 방법(알고리즘)이 있을 때, 이들을 개별적인 '전략' 객체로 분리하고, 필요한 시점에 동적으로 원하는 전략을 선택하여 사용할 수 있도록 하는 방식입니다.
탄생 배경
소프트웨어 개발 과정에서 우리는 종종 특정 문제를 해결하기 위한 여러 가지 알고리즘이나 비즈니스 규칙을 마주하게 됩니다. 예를 들어, 결제 시스템이라면 신용카드, 페이팔, 계좌이체 등 다양한 결제 방식이 있을 수 있고, 배송 시스템이라면 일반 배송, 특급 배송, 국제 배송 등 여러 배송 정책이 있을 수 있습니다.
이러한 경우, 많은 초중급 개발자들은 if-else if-else 또는 switch-case 문을 사용하여 각 알고리즘을 조건에 따라 분기 처리하곤 합니다.
if payment_method == "credit_card":
# 신용카드 결제 로직
elif payment_method == "paypal":
# 페이팔 결제 로직
elif payment_method == "bank_transfer":
# 계좌이체 결제 로직
else:
# ...
이 방식은 처음에는 간단해 보이지만, 새로운 결제 방식이 추가되거나 기존 결제 방식의 로직이 변경될 때마다 기존 코드를 수정해야 합니다. 이는 **개방-폐쇄 원칙(Open/Closed Principle, OCP)**을 위반하며, 코드를 변경하기 어렵고 오류 발생 가능성이 높은 '취약한' 상태로 만듭니다. 전략 패턴은 이러한 문제점을 해결하기 위해 탄생했습니다.
왜 중요한가?
전략 패턴은 다음과 같은 이유로 현대 소프트웨어 개발에서 매우 중요합니다.
- 코드 유연성 및 확장성 향상: 새로운 알고리즘을 추가하거나 기존 알고리즘을 변경할 때, 전략을 사용하는 '문맥(Context)' 클래스의 코드를 수정할 필요가 없습니다. 이는 OCP를 완벽하게 준수하게 하여 시스템 확장을 용이하게 합니다.
- 복잡한 조건문 제거: 거대한
if-else또는switch-case문을 깔끔하게 제거하여 코드의 가독성을 크게 향상시키고, 유지보수를 단순화합니다. - 알고리즘 분리 및 캡슐화: 각 알고리즘이 독립적인 클래스로 캡슐화되어 있어, 다른 부분에 영향을 주지 않고 개별적으로 테스트하고 관리할 수 있습니다.
- 재사용성 증가: 한 번 구현된 전략은 다른 문맥에서도 필요에 따라 재사용될 수 있습니다.
핵심 원리 설명

전략 패턴의 핵심은 행위를 나타내는 알고리즘을 객체로 캡슐화하고, 이 객체들을 교체함으로써 행위를 동적으로 변경하는 것입니다. 이를 위해 세 가지 주요 구성 요소가 필요합니다.
- Strategy (전략 인터페이스 또는 추상 클래스): 모든 구체적인 전략들이 따라야 할 공통 인터페이스를 정의합니다. 이 인터페이스는 문맥(Context)이 구체적인 전략에 접근할 수 있는 통로 역할을 합니다.
- Concrete Strategy (구체적인 전략): Strategy 인터페이스를 구현하는 실제 알고리즘들입니다. 각 구체적인 전략 클래스는 특정 알고리즘의 구현을 담당합니다.
- Context (문맥): 전략을 사용하는 주체입니다. Context는 Strategy 인터페이스 타입의 객체를 가지고 있으며, 이 객체를 통해 실제 알고리즘을 실행합니다. 중요한 점은 Context는 구체적인 전략의 종류를 알지 못하고, 오직 Strategy 인터페이스에만 의존한다는 것입니다. 클라이언트가 Context에 어떤 Concrete Strategy를 주입할지 결정합니다.
비유: 배송비 계산기
여러분이 온라인 쇼핑몰의 배송비 계산 시스템을 만든다고 상상해 봅시다. 이 시스템은 일반 배송, 특급 배송, 국제 배송 등 여러 배송 방식에 따라 배송비를 다르게 계산해야 합니다.
- Strategy (전략 인터페이스):
배송전략인터페이스는배송비_계산(상품_정보)이라는 메서드를 정의합니다. - Concrete Strategy (구체적인 전략):
일반배송전략은배송전략인터페이스를 구현하여 일반 배송비를 계산하는 로직을 가집니다.특급배송전략은 특급 배송비를 계산하는 로직을 가집니다.국제배송전략은 국제 배송비를 계산하는 로직을 가집니다.
- Context (문맥):
배송비계산기클래스입니다. 이 클래스는배송전략타입의 객체를 내부적으로 가지고 있습니다.배송비계산기는배송전략객체의배송비_계산메서드를 호출하여 실제 배송비를 얻습니다.
동작 흐름:
- 고객(클라이언트)은 상품을 장바구니에 담고, 원하는 배송 방식을 선택합니다 (예: "특급 배송").
- 클라이언트는
배송비계산기객체를 생성하고, 선택한 배송 방식에 해당하는특급배송전략객체를배송비계산기에 주입합니다. 배송비계산기는 주입받은특급배송전략객체의배송비_계산메서드를 호출하여 최종 배송비를 사용자에게 보여줍니다.
이 구조 덕분에, 만약 새로운 "새벽 배송" 전략이 추가되더라도 배송비계산기 코드를 전혀 수정할 필요 없이, 단순히 새벽배송전략 클래스만 새로 만들어서 주입해주면 됩니다. 이것이 전략 패턴의 강력함입니다.
코드 예제 2개
Python 예제: 결제 처리 시스템
다양한 결제 수단(신용카드, 페이팔, 은행 송금)을 처리하는 시스템을 전략 패턴으로 구현해 보겠습니다.
import abc
# 1. Strategy (전략 인터페이스/추상 클래스)
# abc 모듈의 ABC(Abstract Base Class)를 사용하여 추상 클래스를 정의합니다.
class PaymentStrategy(abc.ABC):
"""결제 전략의 공통 인터페이스를 정의하는 추상 클래스"""
@abc.abstractmethod
def pay(self, amount: float):
"""지정된 금액을 결제하는 추상 메서드"""
pass
# 2. Concrete Strategy (구체적인 전략)
class CreditCardPayment(PaymentStrategy):
"""신용카드 결제 전략"""
def __init__(self, card_number: str, expiry_date: str, cvv: str):
self._card_number = card_number
self._expiry_date = expiry_date
self._cvv = cvv
print(f"신용카드 결제 전략 초기화: 카드 번호 ****{card_number[-4:]}")
def pay(self, amount: float):
print(f"{amount:.2f}원을 신용카드({self._card_number[-4:]})로 결제합니다.")
# 실제 결제 API 호출 로직 ...
return True
class PayPalPayment(PaymentStrategy):
"""페이팔 결제 전략"""
def __init__(self, email: str):
self._email = email
print(f"페이팔 결제 전략 초기화: 이메일 {email}")
def pay(self, amount: float):
print(f"{amount:.2f}원을 페이팔({self._email})로 결제합니다.")
# 실제 페이팔 API 호출 로직 ...
return True
class BankTransferPayment(PaymentStrategy):
"""은행 송금 결제 전략"""
def __init__(self, account_number: str, bank_name: str):
self._account_number = account_number
self._bank_name = bank_name
print(f"은행 송금 결제 전략 초기화: 계좌 번호 ****{account_number[-4:]}")
def pay(self, amount: float):
print(f"{amount:.2f}원을 {self._bank_name} 계좌({self._account_number[-4:]})로 송금합니다.")
# 실제 은행 송금 시스템 연동 로직 ...
return True
# 3. Context (문맥)
class ShoppingCart:
"""쇼핑 카트 (결제 전략을 사용하는 문맥)"""
def __init__(self):
self._items = []
self._payment_strategy: PaymentStrategy = None # 초기에는 전략이 없음
def add_item(self, item_name: str, price: float):
"""상품을 장바구니에 추가"""
self._items.append({"name": item_name, "price": price})
print(f"'{item_name}' (가격: {price:.2f}원)를 장바구니에 추가했습니다.")
def set_payment_strategy(self, strategy: PaymentStrategy):
"""결제 전략을 설정 (주입)"""
self._payment_strategy = strategy
print(f"결제 전략을 {type(strategy).__name__}으로 설정했습니다.")
def checkout(self):
"""장바구니 상품 결제"""
if not self._payment_strategy:
print("결제 전략이 설정되지 않았습니다. 결제를 진행할 수 없습니다.")
return
total_amount = sum(item["price"] for item in self._items)
print(f"\n총 결제 금액: {total_amount:.2f}원")
self._payment_strategy.pay(total_amount)
print("결제가 완료되었습니다.")
# 클라이언트 코드 (장바구니와 결제 전략을 사용)
if __name__ == "__main__":
cart = ShoppingCart()
cart.add_item("디자인 패턴 책", 25000.0)
cart.add_item("커피 원두", 15000.0)
# 신용카드 결제
print("\n--- 신용카드 결제로 진행 ---")
credit_card_strategy = CreditCardPayment("1234-5678-9012-3456", "12/25", "123")
cart.set_payment_strategy(credit_card_strategy)
cart.checkout()
# 페이팔 결제로 변경
print("\n--- 페이팔 결제로 변경 ---")
paypal_strategy = PayPalPayment("[email protected]")
cart.set_payment_strategy(paypal_strategy)
cart.checkout()
# 은행 송금 결제로 변경
print("\n--- 은행 송금 결제로 변경 ---")
bank_transfer_strategy = BankTransferPayment("987-654321-01", "우리은행")
cart.set_payment_strategy(bank_transfer_strategy)
cart.checkout()
JavaScript 예제: 할인 계산기
다양한 할인 정책(퍼센트 할인, 고정 금액 할인, 멤버십 할인)을 적용하는 할인 계산기를 전략 패턴으로 구현해 보겠습니다.
// 1. Strategy (전략 인터페이스 - JavaScript에서는 추상 클래스 대신 객체 리터럴 또는 클래스로 구현)
// 할인 전략의 공통 인터페이스를 정의합니다.
// 자바스크립트에서는 명시적인 인터페이스가 없으므로, 특정 메서드를 구현해야 한다는 "규약"으로 간주합니다.
class DiscountStrategy {
/**
* @param {number} originalPrice - 원본 가격
* @returns {number} 할인된 최종 가격
*/
calculateDiscountedPrice(originalPrice) {
throw new Error("calculateDiscountedPrice 메서드는 구현되어야 합니다.");
}
}
// 2. Concrete Strategy (구체적인 전략)
class PercentageDiscountStrategy extends DiscountStrategy {
/**
* @param {number} percentage - 할인율 (예: 10% 할인은 0.1)
*/
constructor(percentage) {
super();
this.percentage = percentage;
console.log(`퍼센트 할인 전략 초기화: ${percentage * 100}% 할인`);
}
calculateDiscountedPrice(originalPrice) {
if (originalPrice < 0) throw new Error("가격은 0보다 커야 합니다.");
return originalPrice * (1 - this.percentage);
}
}
class FixedAmountDiscountStrategy extends DiscountStrategy {
/**
* @param {number} amount - 고정 할인 금액
*/
constructor(amount) {
super();
this.amount = amount;
console.log(`고정 금액 할인 전략 초기화: ${amount}원 할인`);
}
calculateDiscountedPrice(originalPrice) {
if (originalPrice < 0) throw new Error("가격은 0보다 커야 합니다.");
return Math.max(0, originalPrice - this.amount); // 가격이 음수가 되지 않도록
}
}
class MembershipDiscountStrategy extends DiscountStrategy {
/**
* @param {string} memberTier - 멤버십 등급 (예: 'Gold', 'Silver')
*/
constructor(memberTier) {
super();
this.memberTier = memberTier;
console.log(`멤버십 할인 전략 초기화: ${memberTier} 등급`);
}
calculateDiscountedPrice(originalPrice) {
if (originalPrice < 0) throw new Error("가격은 0보다 커야 합니다.");
let discountRate = 0;
switch (this.memberTier) {
case 'Gold':
discountRate = 0.15; // 15% 할인
break;
case 'Silver':
discountRate = 0.10; // 10% 할인
break;
default:
discountRate = 0.05; // 기본 5% 할인
}
return originalPrice * (1 - discountRate);
}
}
// 3. Context (문맥)
class ProductPriceCalculator {
constructor() {
this.discountStrategy = null; // 초기에는 할인 전략이 없음
}
/**
* @param {DiscountStrategy} strategy - 사용할 할인 전략 객체
*/
setDiscountStrategy(strategy) {
if (!(strategy instanceof DiscountStrategy)) {
throw new Error("유효한 DiscountStrategy 객체를 제공해야 합니다.");
}
this.discountStrategy = strategy;
console.log(`할인 전략을 ${strategy.constructor.name}으로 설정했습니다.`);
}
/**
* @param {number} originalPrice - 원본 가격
* @returns {number} 최종 가격
*/
calculateFinalPrice(originalPrice) {
if (!this.discountStrategy) {
console.warn("할인 전략이 설정되지 않았습니다. 원본 가격을 반환합니다.");
return originalPrice;
}
return this.discountStrategy.calculateDiscountedPrice(originalPrice);
}
}
// 클라이언트 코드 (제품 가격 계산기와 할인 전략을 사용)
const calculator = new ProductPriceCalculator();
const productPrice = 100000;
console.log(`\n원본 상품 가격: ${productPrice}원`);
// 1. 10% 퍼센트 할인 적용
console.log("\n--- 10% 퍼센트 할인 적용 ---");
const percentageDiscount = new PercentageDiscountStrategy(0.10);
calculator.setDiscountStrategy(percentageDiscount);
let finalPrice1 = calculator.calculateFinalPrice(productPrice);
console.log(`최종 가격 (10% 할인): ${finalPrice1}원`); // 90000원
// 2. 20000원 고정 금액 할인 적용
console.log("\n--- 20000원 고정 금액 할인 적용 ---");
const fixedDiscount = new FixedAmountDiscountStrategy(20000);
calculator.setDiscountStrategy(fixedDiscount);
let finalPrice2 = calculator.calculateFinalPrice(productPrice);
console.log(`최종 가격 (20000원 고정 할인): ${finalPrice2}원`); // 80000원
// 3. Gold 멤버십 할인 적용
console.log("\n--- Gold 멤버십 할인 적용 ---");
const goldMembershipDiscount = new MembershipDiscountStrategy('Gold');
calculator.setDiscountStrategy(goldMembershipDiscount);
let finalPrice3 = calculator.calculateFinalPrice(productPrice);
console.log(`최종 가격 (Gold 멤버십 할인): ${finalPrice3}원`); // 85000원 (15% 할인)
// 전략을 변경하지 않고 계산하면 이전 전략이 유지됩니다.
console.log("\n--- 전략 변경 없이 다시 계산 ---");
let finalPrice4 = calculator.calculateFinalPrice(50000);
console.log(`새로운 상품 가격 50000원에 대한 최종 가격 (Gold 멤버십 할인): ${finalPrice4}원`); // 42500원
실무 적용 사례
전략 패턴은 비즈니스 로직의 유연성이 요구되는 다양한 상황에서 매우 유용하게 사용됩니다.
- 데이터 정렬 알고리즘: 사용자가 원하는 정렬 방식(오름차순, 내림차순, 특정 필드 기준 등)에 따라 퀵 정렬, 병합 정렬, 버블 정렬 등의 알고리즘을 동적으로 선택하여 적용할 수 있습니다.
- 파일 압축/암호화 방식: 파일을 압축하거나 암호화할 때 ZIP, GZ, TAR 등 다양한 포맷이나 AES, RSA 등 여러 암호화 알고리즘을 선택적으로 적용할 수 있습니다.
- 이미지 처리 필터: 사진 편집 애플리케이션에서 흑백, 세피아, 블러, 대비 조절 등 다양한 필터 효과를 선택하여 이미지에 적용할 때 각 필터를 전략으로 구현할 수 있습니다.
- 세금 계산 방식: 온라인 쇼핑몰에서 상품의 종류, 배송 국가, 고객의 등급 등에 따라 달라지는 복잡한 세금 계산 로직을 전략으로 분리하여 관리할 수 있습니다.
- 로그 기록 방식: 애플리케이션의 로그를 파일, 데이터베이스, 콘솔, 원격 서버 등 다양한 대상으로 기록할 때 각 기록 방식을 전략으로 구현하여 유연하게 전환할 수 있습니다.
- 유효성 검사 (Validation): 사용자 입력 필드에 대해 이메일 형식, 비밀번호 강도, 최소/최대 길이 등 다양한 유효성 검사 규칙을 전략으로 적용할 수 있습니다.
이 외에도, 동일한 목표를 달성하기 위해 여러 가지 방법이 존재하고, 그 방법들을 런타임에 동적으로 변경해야 할 필요가 있는 모든 상황에서 전략 패턴을 고려해 볼 수 있습니다.
자주 하는 실수와 해결법
1. 과도한 적용 (Over-engineering)
실수: 너무 단순한 로직이나 변경 가능성이 거의 없는 코드에도 전략 패턴을 도입하여 코드를 불필요하게 복잡하게 만드는 경우입니다. 예를 들어, if 문 하나로 충분히 처리할 수 있는 경우에도 전략 패턴을 적용하면, 클래스의 개수만 늘어나고 오히려 가독성이 떨어질 수 있습니다.
해결법: 전략 패턴은 주로 변경 가능성이 높은 알고리즘, 복잡한 조건 분기가 예상될 때, 또는 알고리즘을 재사용할 필요가 있을 때 고려하는 것이 좋습니다. 처음부터 완벽한 패턴 적용을 목표하기보다는, 단순한 if-else로 시작하고, 복잡성이 증가할 때 리팩토링을 통해 전략 패턴을 적용하는 것이 현명합니다. YAGNI (You Ain't Gonna Need It) 원칙을 기억하세요.
2. Context가 Concrete Strategy에 의존하는 경우
실수: Context 클래스(예: ShoppingCart나 ProductPriceCalculator)가 특정 ConcreteStrategy 클래스(예: CreditCardPayment나 PercentageDiscountStrategy)를 직접 생성하거나, if-else 또는 switch-case 문을 사용하여 어떤 구체적인 전략을 사용할지 결정하는 경우입니다.
# 잘못된 예시: Context가 Concrete Strategy에 직접 의존
class BadShoppingCart:
def checkout(self, payment_method: str, amount: float):
if payment_method == "credit_card":
strategy = CreditCardPayment("...", "...", "...") # Context가 직접 생성
strategy.pay(amount)
elif payment_method == "paypal":
strategy = PayPalPayment("...")
strategy.pay(amount)
# ... 이렇게 되면 OCP 위반
이것은 전략 패턴의 핵심 목표 중 하나인 OCP를 위반하며, Context의 결합도를 높여 새로운 전략이 추가될 때마다 Context 코드를 수정해야 합니다.
해결법: Context는 오직 **Strategy 인터페이스(추상화)**에만 의존해야 합니다. 구체적인 전략 객체는 외부(클라이언트 코드)에서 생성하여 Context에 **주입(Dependency Injection)**해야 합니다. 위 예제의 ShoppingCart.set_payment_strategy() 메서드처럼 외부에서 전략 객체를 받아 설정하는 방식을 사용해야 합니다.
3. 전략 객체가 상태를 가지는 경우 (Stateless vs. Stateful)
실수: 전략 객체가 내부적으로 변경 가능한 상태(예: count, current_value 등)를 가지도록 설계하는 경우입니다. 만약 여러 Context 인스턴스가 하나의 동일한 전략 객체를 공유하게 되면, 이 상태 때문에 예기치 않은 동작이나 스레드 안전성 문제가 발생할 수 있습니다.
해결법: 전략은 가능한 한 **무상태(stateless)**로 설계하는 것이 가장 좋습니다. 즉, 전략의 메서드가 호출될 때 필요한 모든 데이터는 인자로 전달받고, 전략 객체 자체는 내부 상태를 변경하지 않도록 합니다. 만약 전략이 불가피하게 상태를 가져야 한다면, 각 Context 인스턴스마다 새로운 전략 인스턴스를 생성하여 주입하거나, 전략 객체 내부에서 상태를 안전하게 관리할 수 있는 방법을(예: 락(lock) 사용) 고려해야 합니다. 일반적으로는 무상태 전략을 지향하여 단순성과 재사용성을 높이는 것이 좋습니다.
더 공부할 리소스 추천
전략 패턴은 디자인 패턴의 기본 중 하나이며, 객체 지향 설계 원칙을 이해하는 데 큰 도움이 됩니다. 더 깊이 있는 학습을 위해 다음 리소스들을 추천합니다.
- "GoF의 디자인 패턴 (Design Patterns: Elements of Reusable Object-Oriented Software)": 디자인 패턴의 고전 중 고전입니다. 전략 패턴을 포함한 23가지 디자인 패턴에 대한 원본 설명과 예제를 제공합니다. 다소 이론적일 수 있지만, 패턴의 본질을 이해하는 데 필수적입니다.
- "Head First Design Patterns": GoF 책보다 훨씬 쉽고 재미있게 디자인 패턴을 설명합니다. 초중급 개발자에게 강력히 추천하는 책입니다. 전략 패턴을 아주 직관적인 비유와 함께 설명해 줍니다.
- Refactoring Guru (웹사이트): 디자인 패턴에 대한 훌륭한 온라인 리소스입니다. 각 패턴에 대한 명확한 설명, UML 다이어그램, 다양한 언어(Python, JavaScript, Java 등)의 예제를 제공합니다. 전략 패턴 섹션은 특히 시각적으로 이해하기 쉽게 구성되어 있습니다.
- "클린 코드 (Clean Code)" / "클린 아키텍처 (Clean Architecture)" (로버트 C. 마틴 저): 이 책들은 전략 패턴이 기반하는 객체 지향 설계 원칙(SOLID)에 대한 깊이 있는 이해를
