뒤로가기

전략 패턴: 알고리즘을 런타임에 선택하는 행위 패턴

design-pattern

전략 패턴(Strategy Pattern)은 알고리즘 군을 정의하고 각각을 캡슐화하여 교환 가능하게 만드는 행위 디자인 패턴입니다. 이 패턴을 사용하면 알고리즘을 사용하는 클라이언트와 독립적으로 알고리즘을 변경할 수 있으며, 런타임에 객체의 행위를 동적으로 변경할 수 있습니다.

이 글에서는 전략 패턴의 구조, 구현 방법, 실전 사용 사례, 그리고 다른 패턴과의 비교를 상세히 다룹니다.

전략 패턴이 해결하는 문제

조건문 남용의 문제

전략 패턴 없이 여러 알고리즘을 처리하려면 조건문을 사용해야 하며, 이는 여러 문제를 야기합니다.

문제 상황:

class OrderProcessor {
    fun processPayment(amount: Double, paymentType: String) {
        when (paymentType) {
            "CREDIT_CARD" -> {
                println("신용카드로 $amount 결제 처리")
                // 신용카드 검증 로직
                // 신용카드 결제 API 호출
                // 영수증 발급
            }
            "PAYPAL" -> {
                println("PayPal로 $amount 결제 처리")
                // PayPal 인증 로직
                // PayPal API 호출
                // 확인 이메일 발송
            }
            "CRYPTO" -> {
                println("암호화폐로 $amount 결제 처리")
                // 지갑 주소 검증
                // 블록체인 트랜잭션 생성
                // 컨펌 대기
            }
            else -> throw IllegalArgumentException("지원하지 않는 결제 방식")
        }
    }
}

문제점:

  1. 개방-폐쇄 원칙(OCP) 위반: 새로운 결제 방식 추가 시 기존 코드 수정 필요
  2. 코드 중복: 각 분기마다 비슷한 구조 반복
  3. 테스트 어려움: 모든 분기를 테스트해야 함
  4. 복잡도 증가: 결제 방식이 늘어날수록 when 블록이 비대화
  5. 단일 책임 원칙(SRP) 위반: 한 클래스가 모든 결제 로직 담당

전략 패턴 적용 후

// 전략 인터페이스
interface PaymentStrategy {
    fun pay(amount: Double)
}
 
// 구체적 전략들
class CreditCardStrategy : PaymentStrategy {
    override fun pay(amount: Double) {
        println("신용카드로 $amount 결제 처리")
        // 신용카드 특화 로직
    }
}
 
class PayPalStrategy : PaymentStrategy {
    override fun pay(amount: Double) {
        println("PayPal로 $amount 결제 처리")
        // PayPal 특화 로직
    }
}
 
// 컨텍스트
class PaymentProcessor(private var strategy: PaymentStrategy) {
    fun processPayment(amount: Double) {
        strategy.pay(amount)
    }
 
    fun setStrategy(strategy: PaymentStrategy) {
        this.strategy = strategy
    }
}

개선점:

  • 새 결제 방식 추가 시 기존 코드 수정 불필요
  • 각 전략이 독립적으로 테스트 가능
  • 런타임에 전략 변경 가능

전략 패턴의 구조

UML 다이어그램

┌─────────────┐              ┌──────────────────┐
│   Context   │─────────────>│    Strategy      │
├─────────────┤              ├──────────────────┤
│ strategy    │              │ + execute()      │
├─────────────┤              └──────────────────┘
│ execute()   │                       ▲
└─────────────┘                       │
                                      │
                    ┌─────────────────┼─────────────────┐
                    │                 │                 │
            ┌──────────────┐  ┌──────────────┐  ┌──────────────┐
            │ ConcreteA    │  │ ConcreteB    │  │ ConcreteC    │
            ├──────────────┤  ├──────────────┤  ├──────────────┤
            │ + execute()  │  │ + execute()  │  │ + execute()  │
            └──────────────┘  └──────────────┘  └──────────────┘

주요 구성 요소

1. Strategy (전략 인터페이스)

  • 모든 구체적 전략이 구현해야 할 공통 인터페이스
  • 알고리즘의 시그니처 정의

2. ConcreteStrategy (구체적 전략)

  • Strategy 인터페이스를 구현하는 클래스
  • 실제 알고리즘 로직 포함

3. Context (컨텍스트)

  • Strategy 인터페이스에 대한 참조 보유
  • 클라이언트가 상호작용하는 진입점
  • 전략을 동적으로 변경 가능

실전 구현: 사용자 인증 시스템

문제 상황

다양한 인증 방식(로컬, OAuth, SAML, 생체인증)을 지원하는 시스템을 설계합니다.

전략 인터페이스 정의

interface AuthenticationStrategy {
    fun authenticate(credentials: Map<String, String>): AuthResult
    fun supports(method: String): Boolean
}
 
data class AuthResult(
    val success: Boolean,
    val userId: String?,
    val token: String?,
    val errorMessage: String?
)

구체적 전략 구현

class LocalAuthStrategy : AuthenticationStrategy {
    override fun authenticate(credentials: Map<String, String>): AuthResult {
        val username = credentials["username"] ?: return AuthResult(
            success = false,
            userId = null,
            token = null,
            errorMessage = "Username required"
        )
        val password = credentials["password"] ?: return AuthResult(
            success = false,
            userId = null,
            token = null,
            errorMessage = "Password required"
        )
 
        // 데이터베이스에서 사용자 검증
        val isValid = validateCredentials(username, password)
 
        return if (isValid) {
            AuthResult(
                success = true,
                userId = username,
                token = generateJWT(username),
                errorMessage = null
            )
        } else {
            AuthResult(
                success = false,
                userId = null,
                token = null,
                errorMessage = "Invalid credentials"
            )
        }
    }
 
    override fun supports(method: String) = method == "local"
 
    private fun validateCredentials(username: String, password: String): Boolean {
        // 실제 구현에서는 bcrypt 등으로 해시 검증
        return true
    }
 
    private fun generateJWT(username: String): String {
        // JWT 토큰 생성 로직
        return "jwt_token_for_$username"
    }
}
 
class OAuth2Strategy : AuthenticationStrategy {
    override fun authenticate(credentials: Map<String, String>): AuthResult {
        val code = credentials["code"] ?: return AuthResult(
            success = false,
            userId = null,
            token = null,
            errorMessage = "OAuth code required"
        )
 
        // OAuth 제공자에게 토큰 교환 요청
        val accessToken = exchangeCodeForToken(code)
        val userInfo = fetchUserInfo(accessToken)
 
        return AuthResult(
            success = true,
            userId = userInfo.id,
            token = accessToken,
            errorMessage = null
        )
    }
 
    override fun supports(method: String) = method == "oauth2"
 
    private fun exchangeCodeForToken(code: String): String {
        // OAuth 제공자 API 호출
        return "access_token"
    }
 
    private fun fetchUserInfo(token: String): UserInfo {
        // 사용자 정보 조회
        return UserInfo(id = "oauth_user_123", email = "user@example.com")
    }
 
    data class UserInfo(val id: String, val email: String)
}
 
class SAMLAuthStrategy : AuthenticationStrategy {
    override fun authenticate(credentials: Map<String, String>): AuthResult {
        val assertion = credentials["saml_assertion"] ?: return AuthResult(
            success = false,
            userId = null,
            token = null,
            errorMessage = "SAML assertion required"
        )
 
        // SAML 응답 검증
        val isValid = validateSAMLAssertion(assertion)
 
        return if (isValid) {
            val userId = extractUserIdFromAssertion(assertion)
            AuthResult(
                success = true,
                userId = userId,
                token = generateSessionToken(userId),
                errorMessage = null
            )
        } else {
            AuthResult(
                success = false,
                userId = null,
                token = null,
                errorMessage = "Invalid SAML assertion"
            )
        }
    }
 
    override fun supports(method: String) = method == "saml"
 
    private fun validateSAMLAssertion(assertion: String): Boolean {
        // SAML 서명 검증
        return true
    }
 
    private fun extractUserIdFromAssertion(assertion: String): String {
        return "saml_user_456"
    }
 
    private fun generateSessionToken(userId: String): String {
        return "session_$userId"
    }
}

컨텍스트 구현

class AuthenticationService {
    private val strategies = mutableListOf<AuthenticationStrategy>()
 
    init {
        // 지원하는 전략들 등록
        registerStrategy(LocalAuthStrategy())
        registerStrategy(OAuth2Strategy())
        registerStrategy(SAMLAuthStrategy())
    }
 
    fun registerStrategy(strategy: AuthenticationStrategy) {
        strategies.add(strategy)
    }
 
    fun authenticate(method: String, credentials: Map<String, String>): AuthResult {
        val strategy = strategies.find { it.supports(method) }
            ?: return AuthResult(
                success = false,
                userId = null,
                token = null,
                errorMessage = "Unsupported authentication method: $method"
            )
 
        return strategy.authenticate(credentials)
    }
}

사용 예시

fun main() {
    val authService = AuthenticationService()
 
    // 로컬 인증
    val localResult = authService.authenticate(
        method = "local",
        credentials = mapOf(
            "username" to "john_doe",
            "password" to "secret123"
        )
    )
    println("Local auth: ${localResult.success}, Token: ${localResult.token}")
 
    // OAuth 인증
    val oauthResult = authService.authenticate(
        method = "oauth2",
        credentials = mapOf(
            "code" to "oauth_authorization_code"
        )
    )
    println("OAuth auth: ${oauthResult.success}, UserId: ${oauthResult.userId}")
 
    // SAML 인증
    val samlResult = authService.authenticate(
        method = "saml",
        credentials = mapOf(
            "saml_assertion" to "encoded_saml_response"
        )
    )
    println("SAML auth: ${samlResult.success}, Token: ${samlResult.token}")
}

TypeScript 구현 예시

데이터 검증 전략

// 전략 인터페이스
interface ValidationStrategy {
  validate(value: string): ValidationResult;
}
 
interface ValidationResult {
  isValid: boolean;
  errors: string[];
}
 
// 구체적 전략들
class EmailValidationStrategy implements ValidationStrategy {
  validate(value: string): ValidationResult {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    const isValid = emailRegex.test(value);
 
    return {
      isValid,
      errors: isValid ? [] : ['Invalid email format']
    };
  }
}
 
class PasswordValidationStrategy implements ValidationStrategy {
  validate(value: string): ValidationResult {
    const errors: string[] = [];
 
    if (value.length < 8) {
      errors.push('Password must be at least 8 characters');
    }
    if (!/[A-Z]/.test(value)) {
      errors.push('Password must contain at least one uppercase letter');
    }
    if (!/[a-z]/.test(value)) {
      errors.push('Password must contain at least one lowercase letter');
    }
    if (!/[0-9]/.test(value)) {
      errors.push('Password must contain at least one number');
    }
    if (!/[^A-Za-z0-9]/.test(value)) {
      errors.push('Password must contain at least one special character');
    }
 
    return {
      isValid: errors.length === 0,
      errors
    };
  }
}
 
class PhoneNumberValidationStrategy implements ValidationStrategy {
  validate(value: string): ValidationResult {
    // 한국 전화번호 형식 (010-XXXX-XXXX)
    const phoneRegex = /^010-\d{4}-\d{4}$/;
    const isValid = phoneRegex.test(value);
 
    return {
      isValid,
      errors: isValid ? [] : ['Invalid phone number format (expected: 010-XXXX-XXXX)']
    };
  }
}
 
// 컨텍스트
class FormValidator {
  private strategies: Map<string, ValidationStrategy> = new Map();
 
  registerStrategy(fieldName: string, strategy: ValidationStrategy) {
    this.strategies.set(fieldName, strategy);
  }
 
  validateField(fieldName: string, value: string): ValidationResult {
    const strategy = this.strategies.get(fieldName);
 
    if (!strategy) {
      return {
        isValid: true,
        errors: []
      };
    }
 
    return strategy.validate(value);
  }
 
  validateForm(formData: Record<string, string>): Record<string, ValidationResult> {
    const results: Record<string, ValidationResult> = {};
 
    for (const [fieldName, value] of Object.entries(formData)) {
      results[fieldName] = this.validateField(fieldName, value);
    }
 
    return results;
  }
}
 
// 사용 예시
const formValidator = new FormValidator();
formValidator.registerStrategy('email', new EmailValidationStrategy());
formValidator.registerStrategy('password', new PasswordValidationStrategy());
formValidator.registerStrategy('phone', new PhoneNumberValidationStrategy());
 
const results = formValidator.validateForm({
  email: 'user@example.com',
  password: 'Weak123!',
  phone: '010-1234-5678'
});
 
console.log('Validation results:', results);
// {
//   email: { isValid: true, errors: [] },
//   password: { isValid: true, errors: [] },
//   phone: { isValid: true, errors: [] }
// }

전략 패턴 vs 다른 패턴

vs 상태 패턴 (State Pattern)

특징 전략 패턴 상태 패턴
목적 알고리즘 교체 상태에 따른 행위 변경
전략/상태 전환 클라이언트가 명시적으로 선택 내부에서 자동으로 전환
전략/상태 간 관계 독립적 상태 간 전이 규칙 존재
사용 시점 알고리즘이 여러 개일 때 객체 상태가 변할 때

전략 패턴 예시:

// 클라이언트가 전략 선택
val processor = PaymentProcessor(CreditCardStrategy())
processor.setStrategy(PayPalStrategy()) // 명시적 변경

상태 패턴 예시:

// 내부에서 상태 자동 전환
class Order {
    private var state: OrderState = PendingState()
 
    fun process() {
        state.process(this) // 상태가 내부적으로 변경됨
    }
}

vs 팩토리 패턴 (Factory Pattern)

특징 전략 패턴 팩토리 패턴
목적 행위 선택 객체 생성
관심사 알고리즘 실행 방법 어떤 객체를 생성할지
런타임 변경 가능 생성 후 불가
클라이언트 역할 전략 선택 및 실행 객체 생성 요청

vs 템플릿 메서드 패턴

특징 전략 패턴 템플릿 메서드 패턴
구현 방식 인터페이스 (조합) 상속
유연성 높음 (런타임 변경) 낮음 (컴파일 타임 결정)
알고리즘 구조 전략마다 다를 수 있음 템플릿이 구조 정의
결합도 낮음 높음 (부모 클래스 의존)

실전 사용 사례

1. 정렬 알고리즘 선택

interface SortStrategy<T> {
    fun sort(list: MutableList<T>, comparator: Comparator<T>)
}
 
class QuickSortStrategy<T> : SortStrategy<T> {
    override fun sort(list: MutableList<T>, comparator: Comparator<T>) {
        // Quick Sort 구현
        quickSort(list, 0, list.size - 1, comparator)
    }
 
    private fun quickSort(list: MutableList<T>, low: Int, high: Int, comparator: Comparator<T>) {
        if (low < high) {
            val pi = partition(list, low, high, comparator)
            quickSort(list, low, pi - 1, comparator)
            quickSort(list, pi + 1, high, comparator)
        }
    }
 
    private fun partition(list: MutableList<T>, low: Int, high: Int, comparator: Comparator<T>): Int {
        val pivot = list[high]
        var i = low - 1
 
        for (j in low until high) {
            if (comparator.compare(list[j], pivot) < 0) {
                i++
                list[i] = list[j].also { list[j] = list[i] }
            }
        }
 
        list[i + 1] = list[high].also { list[high] = list[i + 1] }
        return i + 1
    }
}
 
class MergeSortStrategy<T> : SortStrategy<T> {
    override fun sort(list: MutableList<T>, comparator: Comparator<T>) {
        if (list.size > 1) {
            mergeSort(list, 0, list.size - 1, comparator)
        }
    }
 
    private fun mergeSort(list: MutableList<T>, left: Int, right: Int, comparator: Comparator<T>) {
        if (left < right) {
            val middle = (left + right) / 2
            mergeSort(list, left, middle, comparator)
            mergeSort(list, middle + 1, right, comparator)
            merge(list, left, middle, right, comparator)
        }
    }
 
    private fun merge(list: MutableList<T>, left: Int, middle: Int, right: Int, comparator: Comparator<T>) {
        // Merge 로직 구현
    }
}
 
class Sorter<T>(private var strategy: SortStrategy<T>) {
    fun sort(list: MutableList<T>, comparator: Comparator<T>) {
        strategy.sort(list, comparator)
    }
 
    fun setStrategy(strategy: SortStrategy<T>) {
        this.strategy = strategy
    }
}
 
// 사용
val numbers = mutableListOf(64, 34, 25, 12, 22, 11, 90)
val sorter = Sorter<Int>(QuickSortStrategy())
 
// 작은 데이터셋: Quick Sort
sorter.sort(numbers, Comparator.naturalOrder())
 
// 큰 데이터셋: Merge Sort로 변경
val largeNumbers = (1..10000).shuffled().toMutableList()
sorter.setStrategy(MergeSortStrategy())
sorter.sort(largeNumbers, Comparator.naturalOrder())

2. 가격 할인 전략

interface DiscountStrategy {
    fun calculateDiscount(price: Double): Double
}
 
class NoDiscountStrategy : DiscountStrategy {
    override fun calculateDiscount(price: Double) = price
}
 
class PercentageDiscountStrategy(private val percentage: Double) : DiscountStrategy {
    override fun calculateDiscount(price: Double): Double {
        return price * (1 - percentage / 100)
    }
}
 
class FixedAmountDiscountStrategy(private val amount: Double) : DiscountStrategy {
    override fun calculateDiscount(price: Double): Double {
        return maxOf(0.0, price - amount)
    }
}
 
class BuyOneGetOneStrategy : DiscountStrategy {
    override fun calculateDiscount(price: Double): Double {
        return price / 2 // 50% 할인
    }
}
 
// 조합 전략
class CombinedDiscountStrategy(
    private val strategies: List<DiscountStrategy>
) : DiscountStrategy {
    override fun calculateDiscount(price: Double): Double {
        return strategies.fold(price) { currentPrice, strategy ->
            strategy.calculateDiscount(currentPrice)
        }
    }
}
 
class ShoppingCart {
    private var discountStrategy: DiscountStrategy = NoDiscountStrategy()
    private val items = mutableListOf<CartItem>()
 
    fun setDiscountStrategy(strategy: DiscountStrategy) {
        this.discountStrategy = strategy
    }
 
    fun addItem(item: CartItem) {
        items.add(item)
    }
 
    fun getTotalPrice(): Double {
        val subtotal = items.sumOf { it.price * it.quantity }
        return discountStrategy.calculateDiscount(subtotal)
    }
}
 
data class CartItem(val name: String, val price: Double, val quantity: Int)
 
// 사용 예시
val cart = ShoppingCart()
cart.addItem(CartItem("Laptop", 1000.0, 1))
cart.addItem(CartItem("Mouse", 50.0, 2))
 
// VIP 고객: 20% 할인
cart.setDiscountStrategy(PercentageDiscountStrategy(20.0))
println("VIP price: ${cart.getTotalPrice()}") // 880.0
 
// 블랙 프라이데이: 20% + 100원 추가 할인
cart.setDiscountStrategy(
    CombinedDiscountStrategy(
        listOf(
            PercentageDiscountStrategy(20.0),
            FixedAmountDiscountStrategy(100.0)
        )
    )
)
println("Black Friday price: ${cart.getTotalPrice()}") // 780.0

3. 압축 알고리즘 선택

interface CompressionStrategy {
  compress(data: string): string;
  decompress(data: string): string;
}
 
class GzipCompressionStrategy implements CompressionStrategy {
  compress(data: string): string {
    // Gzip 압축 로직
    console.log(`Compressing ${data.length} bytes with GZIP`);
    return `gzip:${data}`; // 실제로는 zlib 사용
  }
 
  decompress(data: string): string {
    return data.replace('gzip:', '');
  }
}
 
class LZ4CompressionStrategy implements CompressionStrategy {
  compress(data: string): string {
    // LZ4 압축 (빠른 압축)
    console.log(`Compressing ${data.length} bytes with LZ4`);
    return `lz4:${data}`;
  }
 
  decompress(data: string): string {
    return data.replace('lz4:', '');
  }
}
 
class ZstdCompressionStrategy implements CompressionStrategy {
  compress(data: string): string {
    // Zstandard 압축 (높은 압축률)
    console.log(`Compressing ${data.length} bytes with Zstandard`);
    return `zstd:${data}`;
  }
 
  decompress(data: string): string {
    return data.replace('zstd:', '');
  }
}
 
class FileCompressor {
  private strategy: CompressionStrategy;
 
  constructor(strategy: CompressionStrategy) {
    this.strategy = strategy;
  }
 
  setStrategy(strategy: CompressionStrategy) {
    this.strategy = strategy;
  }
 
  compressFile(data: string): string {
    return this.strategy.compress(data);
  }
 
  decompressFile(data: string): string {
    return this.strategy.decompress(data);
  }
}
 
// 사용: 파일 크기에 따라 전략 선택
function compressWithOptimalStrategy(fileData: string): string {
  const compressor = new FileCompressor(new GzipCompressionStrategy());
 
  if (fileData.length < 1000) {
    // 작은 파일: 압축 안 함
    compressor.setStrategy(new GzipCompressionStrategy());
  } else if (fileData.length < 10000) {
    // 중간 파일: LZ4 (빠른 압축)
    compressor.setStrategy(new LZ4CompressionStrategy());
  } else {
    // 큰 파일: Zstandard (높은 압축률)
    compressor.setStrategy(new ZstdCompressionStrategy());
  }
 
  return compressor.compressFile(fileData);
}

안티패턴과 주의사항

1. 과도한 전략 생성

[주의] 잘못된 예:

// 단순한 차이를 위해 불필요한 전략 생성
class AddOneStrategy : MathStrategy {
    override fun calculate(x: Int) = x + 1
}
 
class AddTwoStrategy : MathStrategy {
    override fun calculate(x: Int) = x + 2
}
 
class AddThreeStrategy : MathStrategy {
    override fun calculate(x: Int) = x + 3
}

[권장] 개선된 예:

// 매개변수로 처리 가능한 경우 단일 전략 사용
class AddStrategy(private val amount: Int) : MathStrategy {
    override fun calculate(x: Int) = x + amount
}
 
// 사용
val addOne = AddStrategy(1)
val addTwo = AddStrategy(2)

2. 컨텍스트와 전략 간 과도한 통신

[주의] 잘못된 예:

// 전략이 컨텍스트의 내부 상태에 과도하게 의존
class ReportGenerator(private var strategy: ReportStrategy) {
    var data: List<Data> = emptyList()
    var format: String = "PDF"
    var template: Template? = null
 
    fun generate() {
        // 전략이 컨텍스트의 모든 상태를 알아야 함
        strategy.generate(this)
    }
}

[권장] 개선된 예:

// 필요한 데이터만 전달
class ReportGenerator(private var strategy: ReportStrategy) {
    fun generate(data: List<Data>, format: String, template: Template): Report {
        return strategy.generate(data, format, template)
    }
}

3. 전략 선택 로직의 복잡화

[주의] 잘못된 예:

// 클라이언트가 전략 선택 로직을 알아야 함
fun processPayment(amount: Double, method: String) {
    val strategy = when {
        method == "credit" && amount > 1000 -> PremiumCreditStrategy()
        method == "credit" -> StandardCreditStrategy()
        method == "paypal" && userCountry == "US" -> USPayPalStrategy()
        method == "paypal" -> InternationalPayPalStrategy()
        else -> DefaultStrategy()
    }
 
    val processor = PaymentProcessor(strategy)
    processor.process(amount)
}

[권장] 개선된 예:

// 팩토리 패턴과 결합하여 전략 선택 로직 캡슐화
class PaymentStrategyFactory {
    fun createStrategy(method: String, amount: Double, country: String): PaymentStrategy {
        return when {
            method == "credit" && amount > 1000 -> PremiumCreditStrategy()
            method == "credit" -> StandardCreditStrategy()
            method == "paypal" && country == "US" -> USPayPalStrategy()
            method == "paypal" -> InternationalPayPalStrategy()
            else -> DefaultStrategy()
        }
    }
}
 
// 클라이언트 코드 단순화
fun processPayment(amount: Double, method: String) {
    val factory = PaymentStrategyFactory()
    val strategy = factory.createStrategy(method, amount, getUserCountry())
    val processor = PaymentProcessor(strategy)
    processor.process(amount)
}

성능 고려사항

1. 전략 인스턴스 재사용

상태를 갖지 않는 전략은 싱글톤이나 객체 풀로 재사용:

object StrategyPool {
    val creditCard: PaymentStrategy = CreditCardStrategy()
    val paypal: PaymentStrategy = PayPalStrategy()
    val crypto: PaymentStrategy = CryptoStrategy()
}
 
// 매번 새 인스턴스 생성 대신 재사용
val processor = PaymentProcessor(StrategyPool.creditCard)

2. 전략 지연 로딩

class ReportGenerator {
    private val strategies = mutableMapOf<String, ReportStrategy>()
 
    fun getStrategy(format: String): ReportStrategy {
        return strategies.getOrPut(format) {
            when (format) {
                "PDF" -> PDFReportStrategy()
                "HTML" -> HTMLReportStrategy()
                "CSV" -> CSVReportStrategy()
                else -> throw IllegalArgumentException("Unknown format: $format")
            }
        }
    }
}

3. 성능 벤치마크

import kotlin.system.measureTimeMillis
 
fun benchmarkStrategies() {
    val data = (1..100000).toList()
    val sorter = Sorter<Int>()
 
    // Quick Sort 성능 측정
    val quickTime = measureTimeMillis {
        sorter.setStrategy(QuickSortStrategy())
        repeat(100) {
            sorter.sort(data.shuffled().toMutableList(), Comparator.naturalOrder())
        }
    }
 
    // Merge Sort 성능 측정
    val mergeTime = measureTimeMillis {
        sorter.setStrategy(MergeSortStrategy())
        repeat(100) {
            sorter.sort(data.shuffled().toMutableList(), Comparator.naturalOrder())
        }
    }
 
    println("Quick Sort: ${quickTime}ms")
    println("Merge Sort: ${mergeTime}ms")
}

전략 패턴 체크리스트

구현 시 확인해야 할 사항:

  • 전략 인터페이스가 모든 구체적 전략에 적합한가?
  • 컨텍스트가 전략의 내부 구현에 의존하지 않는가?
  • 전략 간 전환이 런타임에 가능한가?
  • 전략 선택 로직이 적절히 캡슐화되었는가?
  • 상태 없는 전략을 재사용하고 있는가?
  • 새로운 전략 추가 시 기존 코드 수정이 불필요한가?

결론

전략 패턴은 알고리즘을 캡슐화하고 런타임에 동적으로 선택할 수 있게 하는 강력한 디자인 패턴입니다.

핵심 장점:

  1. 개방-폐쇄 원칙 준수: 새 전략 추가 시 기존 코드 수정 불필요
  2. 단일 책임 원칙: 각 전략이 독립적 책임
  3. 조건문 제거: if/when 블록 대신 다형성 활용
  4. 런타임 전환: 실행 중 알고리즘 변경 가능
  5. 테스트 용이성: 전략별 독립 테스트

적용 시기:

  • 여러 알고리즘 중 하나를 선택해야 할 때
  • 알고리즘이 자주 변경되거나 확장될 때
  • 조건문이 복잡하게 중첩될 때
  • 런타임에 행위를 변경해야 할 때

전략 패턴을 적절히 활용하면 유연하고 확장 가능한 코드를 작성할 수 있으며, 새로운 요구사항에 빠르게 대응할 수 있습니다.

관련 아티클