결제 방식마다 if-else가 끝없이 늘어나던 코드, 혹시 겪어보셨나요?
정렬 옵션, 할인 계산, 필터 로직이 하나의 거대한 switch문 안에서 엉키기 시작하면 손대기가 점점 무서워지죠.
이럴 때 꺼내 들 도구가 바로 Swift 전략 패턴입니다.
오늘은 프로토콜과 클로저로 알고리즘을 부품처럼 갈아끼우는 방법을 실무 관점에서 풀어볼게요.
핵심부터 짚고 갈게요.
전략 패턴은 ’무엇을 하느냐’와 ’어떻게 하느냐’를 분리해, 알고리즘을 외부에서 갈아끼울 수 있게 만드는 설계입니다.
Swift에서는 이걸 프로토콜(무거운 방식)이나 클로저(가벼운 방식) 두 가지로 구현할 수 있어요.
아래에서 하나씩 보여드릴게요.
전략 패턴이 뭔가요? 3줄 요약부터
복잡한 이론보다 뼈대부터 잡고 갈게요.
- 갈아끼울 알고리즘들을 하나의 공통 규격(프로토콜)으로 묶는다
- 실제 동작을 각각의 전략 객체나 클로저로 만든다
- 사용하는 쪽은 규격만 알고, 구체적인 내용은 몰라도 된다
비유하자면 게임 캐릭터의 무기 교체 같은 거예요.
캐릭터는 ’공격한다’만 알면 되고, 검이든 활이든 마법이든 장착된 무기가 알아서 처리합니다.
캐릭터 코드를 건드리지 않고 무기만 바꿔 끼우는 거죠.
이 ’갈아끼움’이 전략 패턴의 전부라고 봐도 무방합니다.
프로토콜로 구현하기 (정석 방식)
가장 교과서적인 프로토콜 방식부터 볼게요.
할인 계산을 예로 들어볼게요. 일반 회원, VIP 회원, 쿠폰 적용처럼 계산법이 여러 개인 상황입니다.
아래는 모든 전략이 지켜야 할 공통 규격과 실제 전략 하나를 정의한 코드예요.
// 모든 할인 전략이 따라야 할 공통 규격
protocol DiscountStrategy {
func discount(for price: Int) -> Int
}
// VIP 전략: 20% 할인
struct VIPDiscount: DiscountStrategy {
func discount(for price: Int) -> Int { price * 20 / 100 }
}
이제 이 전략을 품고 사용하는 쪽을 볼게요.
사용하는 객체는 어떤 전략이 들어오든 신경 쓰지 않아요.
struct Checkout {
var strategy: DiscountStrategy // 전략을 갈아끼울 수 있는 자리
func finalPrice(_ price: Int) -> Int {
price - strategy.discount(for: price)
}
}
// 결제 순간에 전략만 바꿔 끼운다
let cart = Checkout(strategy: VIPDiscount())
새로운 할인 정책이 생겨도 Checkout은 손대지 않아요.
DiscountStrategy를 따르는 구조체 하나만 새로 만들면 끝입니다.
기존 코드를 건드리지 않고 기능을 확장하는 것, 이게 실무에서 체감되는 가장 큰 장점이었어요.
클로저로 구현하기 (Swift다운 방식)
그런데 전략이 단순한데도 매번 구조체를 만드는 건 좀 번거롭죠.
단순한 로직까지 전부 프로토콜로 감싸면 코드가 오히려 장황해지기 쉽습니다.
이럴 때 Swift에서는 클로저가 훨씬 가볍고 자연스럽습니다.
struct Checkout {
// 전략 자체를 함수로 받는다
var discount: (Int) -> Int
func finalPrice(_ price: Int) -> Int {
price - discount(price)
}
}
// 전략을 그 자리에서 즉석으로 정의
let cart = Checkout(discount: { $0 * 20 / 100 })
별도 타입 선언 없이 전달하는 순간 로직을 바로 적어넣을 수 있어요.
정렬 기준, 필터 조건, 간단한 변환처럼 가벼운 알고리즘 교체엔 이 방식이 훨씬 산뜻합니다.
실제로 표준 라이브러리의 sorted(by:)가 바로 이 클로저 전략 패턴이에요.
정렬이라는 뼈대는 고정하고 비교 기준만 클로저로 갈아끼우는 구조니까요.
우리가 이미 매일 쓰고 있던 셈입니다.
프로토콜과 클로저, 언제 뭘 쓸까?
제가 실무에서 나눠 쓰는 기준을 표로 정리해봤어요.
| 구분 | 프로토콜 방식 | 클로저 방식 |
|---|---|---|
| 적합한 상황 | 전략이 복잡·상태 보유 | 전략이 짧고 단순 |
| 재사용성 | 여러 곳에서 재사용 쉬움 | 일회성에 유리 |
| 코드량 | 타입 선언 필요, 많음 | 즉석 정의, 적음 |
| 테스트 | 개별 타입으로 검증 쉬움 | 로직만 간단히 검증 |
| 가독성 | 이름으로 의도 드러남 | 짧으면 명확, 길면 난독 |
제 기준은 이래요.
전략이 내부 상태를 갖거나, 여러 화면에서 재사용되거나, 이름으로 의도를 분명히 드러내야 한다면 프로토콜이 좋습니다.
반대로 한두 줄짜리 로직을 그때그때 바꿔 끼우는 거라면 클로저가 정답이에요.
둘을 섞어 써도 전혀 문제없습니다. 저는 큰 정책은 프로토콜, 세부 옵션은 클로저로 나눠 쓰고 있어요.
자주 묻는 질문 (Q&A)
Q. 전략 패턴이랑 상속(오버라이딩)이랑 뭐가 다른가요?
상속은 부모 클래스에 묶여 컴파일 시점에 동작이 고정돼요.
반면 전략 패턴은 실행 중에 전략을 자유롭게 바꿔 끼울 수 있고, 클래스 계층에 얽매이지 않습니다.
Q. enum의 switch문으로 처리하는 거랑 뭐가 다른가요?
switch는 새 케이스가 생길 때마다 기존 코드를 열어 수정해야 해요.
전략 패턴은 새 전략을 ’추가’만 하면 되니, 기존 코드를 건드릴 일이 줄어듭니다.
결국 전략 패턴의 핵심은 하나예요. 바뀌는 부분을 밖으로 빼서 갈아끼우기 쉽게 만드는 것.
오늘 배운 프로토콜과 클로저, 두 방식을 상황에 맞게 꺼내 쓰신다면 그 지긋지긋한 if-else 지옥에서 분명 벗어나실 수 있을 거예요. 작은 코드부터 한번 리팩터링해보시길 응원합니다!
