iOS 개발을 하다 보면 네트워크 요청을 하게 되고 URLRequest를 만들 때 이런 경우들이 있을 수 있죠:
- 그냥 아무 데이터도 안 보내는 단순 GET
- 쿼리 파라미터를 붙여서 GET
- JSON Body를 넣는 POST
개발 초기에는 이런 케이스 몇개만 처리하면 충분합니다. 보통은 이렇게 enum으로 정의하고 switch로 분기했어요
enum HTTPTask {
case plain
case query([String: String])
case json(Encodable)
}
그리고 URLRequestBuilder에서 이런 식으로…
switch task {
case .plain: ...
case .query(let params): ...
case .json(let body): ...
}
딱 여기까지는 좋아요.
그런데 문제는 Form Data로 보내야 한다면?
파일 업로드를 추가한다면? ....
OCP 위반한 제 자신을 발견할 수 있었어요 😭
그래서 '전략 패턴'이 뭔데? ♟️
"알고리즘들을 각각의 클래스로 캡슐화하고, 동적으로 교체할 수 있게 만드는 디자인 패턴"
예를 들어, 온라인 쇼핑몰의 결제를 생각해 볼까요?
- Context(문맥): “결제하기”
- Strategy(전략): “결제 방식”
- Concrete Strategy(구체 전략): 신용카드, 카카오페이, 네이버페이, 페이코
사용자는 그냥 결제 버튼만 누릅니다. 내부적으로 어떤 전략을 쓰는지는 Context가 알아서 처리하죠.
네트워크 코드도 똑같이 적용할 수 있어요:)
- Context: URLRequestBuilder
- Strategy: HTTPTaskStrategy
- Concrete Strategy: Plain, URLQuery, JSONBody, FormData …
1단계: 공통 인터페이스(프로토콜) 정의
// "너희는 모두 URLRequest를 만드는 방법을 알아야 해!"
protocol HTTPTaskStrategy {
func build(from baseRequest: URLRequest, url: URL) throws -> URLRequest
}
이 프로토콜은 "어떤 전략이든 build라는 기능을 제공해야 한다"는 규칙을 만듭니다.
2단계: 각 전략을 독립적인 객체로 구현
이제 각 enum 케이스의 로직을 별도의 struct로 분리합니다. 각 객체는 오직 자신의 책임에만 집중합니다.
// 아무 것도 안 하는 전략
struct PlainTaskStrategy: HTTPTaskStrategy {
func build(from baseRequest: URLRequest, url: URL) throws -> URLRequest {
baseRequest
}
}
// URLQuery 전략
struct URLQueryTaskStrategy: HTTPTaskStrategy {
let query: [String: String]
func build(from baseRequest: URLRequest, url: URL) throws -> URLRequest {
var components = URLComponents(url: url, resolvingAgainstBaseURL: false)
components?.queryItems = query.map { URLQueryItem(name: $0.key, value: $0.value) }
var req = baseRequest
req.url = components?.url
return req
}
}
// JSONBody 전략
struct JSONBodyTaskStrategy: HTTPTaskStrategy {
let body: Encodable
func build(from baseRequest: URLRequest, url: URL) throws -> URLRequest {
var req = baseRequest
req.httpBody = try JSONEncoder().encode(body)
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
return req
}
}
여기서 중요한 포인트:
👉 각 전략은 자기 일만 합니다.
👉 서로가 서로를 모릅니다. (JSON이 쿼리의 존재를 알 필요가 없는거져!)
3단계: Context 객체
이제 이 전략들을 담고, 사용하기 편하게 만들어줄 '문맥' 객체가 필요합니다. 기존의 HTTPTask를 재활용해 봅시다.
struct HTTPTask {
let strategy: HTTPTaskStrategy
static var plain: HTTPTask { HTTPTask(strategy: PlainTaskStrategy()) }
static func query(_ q: [String: String]) -> HTTPTask {
HTTPTask(strategy: URLQueryTaskStrategy(query: q))
}
static func json<T: Encodable>(_ b: T) -> HTTPTask {
HTTPTask(strategy: JSONBodyTaskStrategy(body: b))
}
}
사용법은 기존 enum과 거의 똑같습니다. .query(params) 처럼요. 사용 편의성은 그대로 유지됩니다.
4단계: URLRequestBuilder
struct URLRequestBuilder {
static func build(from endpoint: Endpoint) throws -> URLRequest {
guard let url = endpoint.baseURL?.appendingPathComponent(endpoint.path) else {
throw NetworkError.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = endpoint.method.rawValue
endpoint.headers?.forEach { request.setValue($1, forHTTPHeaderField: $0) }
// 🔥 switch문 사라짐!
return try endpoint.task.strategy.build(from: request, url: url)
}
}
URLRequestBuilder는 더 이상 switch로 알빠가 아닙니다.
그저 "네가 어떤 전략이든, 너의 build 메서드를 실행할게" 라고 말할 뿐입니다. 책임이 완벽하게 분리되었습니다.
그래서 정확히 뭐가 좋아진 건데?
새로운 요청 타입(ex: Form Data)을 추가하는 시나리오를 다시 생각해 봅시다.
// 1. 새로운 전략만 추가 (기존 코드 수정 X)
struct FormDataTaskStrategy: HTTPTaskStrategy {
let formData: Data
func build(from baseRequest: URLRequest, url: URL) throws -> URLRequest {
var newRequest = baseRequest
newRequest.httpBody = formData
newRequest.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
return newRequest
}
}
// 2. HTTPTask에 편의 메서드만 추가 (확장)
extension HTTPTask {
static func formData(_ data: Data) -> HTTPTask {
HTTPTask(strategy: FormDataTaskStrategy(data: data))
}
}
// 3. 끝! 바로 사용 가능
struct LoginEndpoint: Endpoint {
var task: HTTPTask {
.formData(loginFormData)
}
}
URLRequestBuilder를 건드렸나요? 아니요.
기존의 PlainTaskStrategy, JSONBodyTaskStrategy에 영향이 있나요? 전혀요.
테스트는 어떻게 하죠? FormDataTaskStrategy만 독립적으로 테스트하면 됩니다.
이것이 바로 전략 패턴이 주는 핵심적인 장점들입니다.
- OCP 개선: 핵심 로직은 수정할 필요 없고, 새로운 것이 추가되어도 다른것들에 영향을 주지 않습니다.
- SRP (단일 책임 원칙) 향상: 각 전략 클래스는 '특정 타입의 요청을 만드는 방법'이라는 단 하나의 책임만 가집니다.
- 향상된 테스트 용이성: 각 전략을 독립적으로, 완벽하게 격리하여 테스트할 수 있습니다.
- 가독성 및 유지보수성: switch 지옥에서 벗어나 각 로직이 명확하게 분리되어 코드를 이해하고 수정하기 쉬워집니다.
무조건 전략 패턴이 정답일까?
아니요.
요청 타입이 딱 두세 개로 고정이라면? 그냥 switch가 더 단순하고 효율적입니다.
전략 패턴은 변화가 많이 일어날 곳에서만 힘을 발휘합니다.
네트워크 레이어는 어떨까요?
OAuth, 파일 업로드, 스트리밍, WebSocket… 변화가 거의 확정된 영역이죠.
👉 바로 이런 곳에서 전략 패턴의 장점이 나온다고 생각합니다:)
'디자인패턴' 카테고리의 다른 글
커맨드 패턴 (0) | 2025.05.20 |
---|---|
데코레이터패턴 (1) | 2025.05.19 |
파사드 패턴(Facade Pattern) (0) | 2025.03.25 |
팩토리 패턴 (0) | 2025.03.25 |
Adaptor Pattern (구조적 디자인패턴) (0) | 2025.03.24 |