Strategy(전략)는 상호 교환 가능한 알고리즘군을 정의하고 각각을 별도의 클래스(Strategy)에 배치하는 행동 디자인 패턴입니다. 이 패턴은 실행 중에 알고리즘을 선택할 수 있게 합니다. 클라이언트 코드는 공통 Strategy 인터페이스를 통해 작동하며, 구체적인 구현은 런타임에 대체됩니다. iOS에서는 Protocol + 전략 클래스를 통해 구현되고, Android에서는 Interface + 구현을 통해 구현됩니다. Strategy는 23개의 GoF 패턴 중 하나이며, 결제 처리, 유효성 검사, 정렬 및 데이터 필터링에 널리 사용됩니다. 자세한 내용은 원본 GoF 설명을 참조하세요.
핵심 포인트
Strategy는 23개의 GoF(Gang of Four) 패턴 중 하나로, "Design Patterns: Elements of Reusable Object-Oriented Software"(1994) 책에 설명되어 있습니다. 이 패턴은 런타임에 알고리즘을 선택하는 문제를 해결합니다. 많은 조건문(if-else, switch)이 있는 단일 클래스를 작성하는 대신, Strategy는 각 알고리즘을 공통 인터페이스를 가진 별도의 클래스로 추출할 것을 제안합니다. 컨텍스트(전략을 사용하는 클래스)는 Strategy 인터페이스에 대한 참조를 보유하고 실행을 구체적인 전략에 위임합니다.
패턴의 구조는 세 가지 요소로 구성됩니다: Context(컨텍스트)는 Strategy에 대한 참조를 보유하고 그 메서드를 호출합니다; Strategy(인터페이스)는 모든 알고리즘에 대한 공통 메서드를 선언합니다; ConcreteStrategy(구체적인 전략)는 인터페이스를 구현하고 구체적인 알고리즘을 포함합니다. 클라이언트는 원하는 전략을 생성하고 생성자, 세터 또는 메서드 매개변수를 통해 컨텍스트에 전달합니다. 컨텍스트는 어떤 특정 전략이 실행되고 있는지 알지 못합니다 — 인터페이스로만 작업합니다.
| 구성 요소 | 역할 | 예시 |
|---|---|---|
| Context | Strategy에 대한 참조 보유 | PaymentProcessor, Sorter |
| Strategy | 알고리즘을 위한 공통 인터페이스 | Protocol PaymentStrategy |
| ConcreteStrategy | 알고리즘의 구체적인 구현 | CardPayment, PayPalPayment |
Open/Closed 원칙 — Strategy의 주요 장점입니다. 시스템은 확장에 대해 열려 있고(새 전략 추가 가능) 수정에 대해 닫혀 있습니다(컨텍스트 코드를 변경할 필요 없음). 패턴이 없으면 새 알고리즘을 추가하기 위해 기존 클래스를 변경해야 하며, 이는 OCP를 위반하고 회귀 오류의 위험을 증가시킵니다. Strategy는 또한 클래스 크기를 줄입니다: switch-case가 있는 200줄 클래스 대신 각각 20줄인 6개의 클래스를 얻을 수 있습니다.
Swift의 Strategy는 Protocol(전략 인터페이스)과 전략 클래스 또는 구조체를 통해 구현됩니다. Swift 프로토콜은 연관 타입과 제네릭 제약 조건을 지원하여 전략을 설계할 때 유연성을 제공합니다. 컨텍스트는 일반적으로 init 또는 속성을 통해 전략을 수락하는 ViewModel 클래스 또는 서비스입니다. 이 패턴은 iOS 프로젝트에서 이벤트 처리, 애니메이션, 데이터 형식 지정 및 UI 전략에 널리 사용됩니다.
// 1. Protocol Strategy
protocol PaymentStrategy {
func pay(amount: Decimal) async throws -> PaymentResult
}
// 2. Concrete Strategies
struct CardPaymentStrategy: PaymentStrategy {
let cardNumber: String
let cvv: String
func pay(amount: Decimal) async throws -> PaymentResult {
// 은행 API에 요청 보내기
return PaymentResult(status: .success, transactionId: "tx_\(UUID())")
}
}
struct PayPalPaymentStrategy: PaymentStrategy {
let email: String
func pay(amount: Decimal) async throws -> PaymentResult {
// PayPal SDK로 리디렉션
return PaymentResult(status: .success, transactionId: "pp_\(UUID())")
}
}
// 3. Context
class PaymentProcessor {
private var strategy: PaymentStrategy
init(strategy: PaymentStrategy) {
self.strategy = strategy
}
func setStrategy(_: PaymentStrategy) {
strategy = strategy
}
func processPayment(amount: Decimal) async throws -> PaymentResult {
return try await strategy.pay(amount: amount)
}
}
// 사용법
let processor = PaymentProcessor(strategy: CardPaymentStrategy(cardNumber: "4111...", cvv: "123"))
let result = try await processor.processPayment(amount: 99.99)
SwiftUI의 Strategy — 패턴은 MVVM과 자연스럽게 통합됩니다. ViewModel에는 전략 속성이 포함되어 있으며 사용자 작업 시 해당 메서드를 호출합니다. SwiftUI View는 @Published 또는 @State를 통해 데이터를 수신합니다 — 전략은 View에서 구현 세부 정보를 숨깁니다. 예를 들어, 텍스트 유효성 검사 전략(emailValidator, phoneValidator)은 입력 필드 유형에 따라 교체됩니다. Strategy를 SwiftUI와 결합하면 UIKit에서 상속하지 않고도 유연성을 얻을 수 있습니다.
Kotlin의 Strategy는 언어 수준에서 Interface를 사용하고 단순화를 위해 함수형 인터페이스(SAM)를 사용합니다. Kotlin은 람다를 지원하여 별도의 전략 클래스를 선언하지 않고 함수로 알고리즘을 전달할 수 있습니다. Android에서는 ViewModel 및 Use Cases에서 데이터 로딩, 캐싱 및 오류 처리 알고리즘을 격리하는 데 패턴이 사용됩니다. Clean Architecture를 사용하는 Android 프로젝트는 플래그(mock, real, cache)에 따라 다른 리포지토리 구현을 주입하기 위해 Strategy를 사용합니다.
// 1. Interface Strategy
interface PaymentStrategy {
suspend fun pay(amount: BigDecimal): PaymentResult
}
// 2. Concrete Strategies
class CardPaymentStrategy(
private val cardNumber: String,
private val cvv: String
) : PaymentStrategy {
override suspend fun pay(amount: BigDecimal): PaymentResult {
// Retrofit을 통한 은행 API
return PaymentResult(success = true, transactionId = "tx_${UUID.randomUUID()}")
}
}
class PayPalPaymentStrategy(
private val email: String
) : PaymentStrategy {
override suspend fun pay(amount: BigDecimal): PaymentResult {
// PayPal SDK 통합
return PaymentResult(success = true, transactionId = "pp_${UUID.randomUUID()}")
}
}
// 3. Context
class PaymentProcessor(
private val strategy: PaymentStrategy
) {
fun setStrategy(strategy: PaymentStrategy): PaymentProcessor {
return PaymentProcessor(strategy)
}
suspend fun processPayment(amount: BigDecimal): PaymentResult {
return strategy.pay(amount)
}
}
// ViewModel에서 사용
class CheckoutViewModel : ViewModel() {
private var processor = PaymentProcessor(CardPaymentStrategy("4111...", "123"))
fun payWithCard() {
viewModelScope.launch {
val result = processor.processPayment(BigDecimal("99.99"))
// 결과 처리
}
}
}
Hilt/Dagger를 사용한 Strategy — Android 프로젝트에서 전략은 종종 DI를 통해 주입됩니다. Hilt는 @Binds 또는 @Provides를 통해 PaymentStrategy의 구체적인 구현을 제공합니다. 이를 통해 컨텍스트 코드를 변경하지 않고 전략을 변경할 수 있습니다 — 다른 빌드(debug/release)에 대해 DI 모듈만 변경하면 됩니다. 예를 들어, 디버깅을 위해 MockPaymentStrategy가 주입되고, 프로덕션을 위해 실제 은행 전략이 주입됩니다. Strategy + DI의 조합은 최대의 유연성을 제공합니다.
Strategy vs State — 구조적으로 패턴은 동일합니다: 둘 다 인터페이스와 구체적인 클래스와의 합성을 사용합니다. 차이점은 목적에 있습니다: Strategy는 독립적인 알고리즘을 선택하고, State는 상태에 따라 객체 동작을 제어합니다. State에서는 컨텍스트 자체가 상태가 변경될 때 전략을 변경합니다; Strategy에서는 컨텍스트가 전환을 제어하지 않습니다 — 클라이언트가 명시적으로 알고리즘을 설정합니다. 전략은 서로에 대해 알지 못하지만, 상태는 서로 전환할 수 있습니다.
Strategy vs Command — Command는 단일 작업을 객체로 캡슐화하고, Strategy는 상호 교환 가능한 알고리즘 집합을 캡슐화합니다. Command는 "무엇을 할 것인가"(단일 execute 호출), Strategy는 "어떻게 할 것인가"(여러 단계의 알고리즘)입니다. Command는 큐, 지연 실행, 실행 취소/다시 실행에 사용됩니다. Strategy는 런타임에 작업을 수행하는 방법을 선택하는 데 사용됩니다. 명령은 전략으로 매개변수화할 수 있어 두 패턴을 결합할 수 있습니다.
| 특성 | Strategy | State | Command | Template Method |
|---|---|---|---|---|
| 목적 | 상호 교환 가능한 알고리즘 | 상태에 따른 동작 | 요청 캡슐화 | 알고리즘 뼈대 |
| 전환 | 클라이언트가 명시적으로 | 컨텍스트가 자동으로 | 클라이언트 또는 큐 | 상속으로 |
| 수준 | 객체 (합성) | 객체 (합성) | 객체 | 클래스 (상속) |
Strategy vs Template Method — 두 패턴 모두 알고리즘을 정의하지만 방식이 다릅니다. Template Method는 상속을 사용합니다: 기본 클래스가 알고리즘 뼈대(템플릿 메서드)를 정의하고, 하위 클래스가 개별 단계를 재정의합니다. Strategy는 합성을 사용합니다: 알고리즘이 완전히 별도의 클래스로 외부화됩니다. Template Method는 고정된 알고리즘 구조의 경우 더 간단하고, Strategy는 알고리즘이 완전히 다르고 동적으로 변경될 수 있는 경우에 적합합니다.
결제 처리 — Strategy의 전형적인 예입니다. 온라인 스토어의 쇼핑 카트에는 항목 목록이 포함되어 있으며, 결제 방법은 사용자가 선택합니다. 각 방법(카드, PayPal, Apple Pay, Google Pay, 암호화폐)은 공통 pay(amount) 서명을 가진 별도의 전략입니다. PaymentProcessor 컨텍스트는 결제가 정확히 어떻게 처리되는지 알지 못합니다 — 공통 메서드를 호출할 뿐입니다. 새 결제 방법을 추가해도 카트 코드를 변경할 필요가 없습니다.
데이터 유효성 검사 — Strategy는 동일한 필드의 다른 유효성 검사 규칙에 사용됩니다. EmailValidatorStrategy, PhoneValidatorStrategy, AgeValidatorStrategy는 validate(input) 메서드가 있는 공통 ValidationStrategy 인터페이스를 구현합니다. 등록 양식은 각 필드를 확인하기 위해 전략 집합을 사용합니다. 유효성 검사 전략은 체인(Chain of Responsibility)으로 결합하거나 루프에서 한 번에 모두 적용할 수 있습니다. 이렇게 하면 긴 if-else 검사를 다형성 유효성 검사기 컬렉션으로 대체합니다.
// 정렬 전략
protocol SortingStrategy {
func sort<T>(_ items: [T]) -> [T] where T: Comparable
}
struct QuickSortStrategy: SortingStrategy {
func sort<T>(_ items: [T]) -> [T] { /* quicksort */ items }
}
struct MergeSortStrategy: SortingStrategy {
func sort<T>(_ items: [T]) -> [T] { /* mergesort */ items }
}
class SortedDataSource<T> {
private var strategy: SortingStrategy
func display(_ items: [T]) { let sorted = strategy.sort(items) }
}
인증 — 모바일 앱에서는 제공자에 따라 인증 전략이 전환됩니다. AuthStrategy는 login(), logout(), getToken() 메서드로 EmailPasswordAuth, GoogleAuth, AppleAuth, BiometricAuth에 대해 구현됩니다. AuthManager 컨텍스트는 DI 또는 팩토리를 통해 전략을 수락합니다. 이를 통해 로그인 화면을 변경하지 않고 새 인증 제공자를 추가할 수 있습니다. Strategy 패턴은 많은 OAuth 라이브러리와 Firebase Authentication의 기초입니다.
자주 묻는 질문
Strategy는 변경되거나 확장될 수 있는 3개 이상의 알고리즘이 있을 때 적합합니다. 2개의 알고리즘이 있고 안정적이라면 간단한 if-else가 덜 비용이 듭니다. 알고리즘이 애플리케이션의 다른 부분에서 사용될 때, 런타임에 알고리즘을 교체해야 할 때, 또는 각 알고리즘이 자체 종속성과 테스트를 필요로 할 때 Strategy를 사용하세요.
아니요, 구조는 비슷하지만 다른 패턴입니다. Strategy — 클라이언트가 명시적으로 알고리즘을 선택하고 전략은 독립적입니다. State — 내부 상태가 변경되면 객체 자체가 동작을 변경하며, 상태는 서로 전환될 수 있습니다. State에서는 컨텍스트가 상태 변경을 관리하고, Strategy에서는 클라이언트 코드가 관리합니다.
네, Swift와 Kotlin에서는 전략을 클로저나 람다로 전달할 수 있습니다. Swift: typealias PaymentHandler = (Decimal) async throws -> PaymentResult. Kotlin: typealias PaymentFun = suspend (BigDecimal) -> PaymentResult. 이렇게 하면 간단한 경우 코드가 단순화되지만 명명과 문서화가 손실됩니다. 1-2개의 알고리즘에는 클로저로 충분하고, 4개 이상에는 별도의 클래스가 더 좋습니다.
각 전략은 모크 종속성을 사용하여 별도의 단위 테스트로 테스트됩니다. 컨텍스트는 모크 전략으로 테스트되어 컨텍스트가 전략 메서드를 호출하고 올바른 매개변수를 전달하는지 확인합니다. Swift에서는 XCTest + 프로토콜을 모크에 사용하고, Kotlin에서는 MockK 또는 Mockito를 사용합니다. 주요 장점: 각 전략이 복잡한 설정 없이 격리되어 테스트됩니다.
네, Strategy는 "Design Patterns: Elements of Reusable Object-Oriented Software"(Gamma, Helm, Johnson, Vlissides, 1994) 책에 설명된 23개 패턴 중 하나입니다. 행동 패턴 그룹에 속합니다. 별칭: Policy. Smalltalk-80의 원본 예제 코드는 GoF 원서에서 확인할 수 있습니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.