전략패턴

2025. 9. 29. 21:47·디자인패턴

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
'디자인패턴' 카테고리의 다른 글
  • 커맨드 패턴
  • 데코레이터패턴
  • 파사드 패턴(Facade Pattern)
  • 팩토리 패턴
2료일
2료일
좌충우돌 모든것을 다 정리하려고 노력하는 J가 되려고 하는 세미개발자의 블로그입니다. 편하게 보고 가세요
  • 2료일
    GPT에게서 살아남기
    2료일
  • 전체
    오늘
    어제
    • 분류 전체보기 (133) N
      • SWIFT개발일지 (28)
        • ARkit (1)
        • Vapor-Server with swift (3)
        • UIkit (2)
      • 알고리즘 (25)
      • Design (6)
      • iOS (42) N
        • 반응형프로그래밍 (12)
      • 디자인패턴 (6)
      • CS (3)
      • 도서관 (2)
  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
2료일
전략패턴
상단으로

티스토리툴바