apple/iOS, UIKit, Documentation

Swift URLProtocol (URLSessionConfiguration)

lgvv 2024. 12. 23. 21:55

Swift URLProtocol (URLSessionConfiguration)

 

최근 서비스에서 애드가드를 막기 위한 방법을 고민하고 있는데, 네트워크 관련 부분을 학습함.

학습 후에 테스트코드, 퍼널 분석을 위한 로그 및 시스템 os로그 기록 등 더 다양하게 활용할 수 있을 것으로 기대.

 

학습 예제 파일 (SPM - Currency)

ArchitectureExample.zip
0.67MB

목차

  • Overview
  • URLProtocol 간단 요약
  • 코드 구현 예제
  • 주요 메서드 설명
  • 코드 사용 예제
  • 활용할 수 있는 포인트

 

Overview

Apple의 공식 문서에서 제공하는 URLProtocol은 URL 로딩 시스템의 요청을 처리하는 데 사용되는 강력한 커스텀 프로토콜.

  • URLProtocol은 애플의 URL 로딩 시스템이 네트워크 요청을 처리하는 방식을 커스터마이즈하거나 대체할 수 있도록 설계된 클래스
  • 이를 통해 URL 요청의 중간 단계에서 데이터를 가로채거나, 가상의 응답을 제공하거나, 특정한 방식으로 요청을 수정할 수 있습니다.

참고사항

  • watchOS 2 이상에서는 해당 클래스를 사용하여 URL 스킴 및 프로토콜을 정의할 수 없음

서브클래싱 주의사항

  • 애플의 URL 로딩 시스템은 작업 기반 메서드(task-based methods)를 요청 기반 메서드(request-based methods)보다 더 우선적으로 사용
  • 메서드 재정의: init
    • 요청 기반 초기화 메서드: init(request:cachedResponse:client:)
    • 작업 기반 초기화 메서드: init(task:cachedResponse:client:)
  • 메서드 재정의: canInit(with: )
    • 요청 기반 메서드: canInit(with request: URLRequest)
    • 작업 기반 메서드: canInit(with task: URLSessionTask)

 

URLProtocol 간단 요약

역할

  • 네트워크 요청 처리의 중간 계층으로 작동하며, 요청을 가로채거나 변환하는 데 유용함.

활용 예제와 이점

  • 테스트 환경에서 Mock 서버 역할.
  • 실시간 데이터 로깅 및 변조 가능.
  • 커스텀 프로토콜(예: ftp://, custom://) 구현 가능.

 

 

코드 구현 예제

URLProtocol을 채택해서 커스텀 클래스를 만듦

import Foundation

class MockURLProtocol: URLProtocol {
    static var mockResponses: [URL: (Data?, HTTPURLResponse?)] = [:]

    override class func canInit(with request: URLRequest) -> Bool {
        return true // 모든 요청을 처리
    }

    override class func canonicalRequest(for request: URLRequest) -> URLRequest {
        return request // 요청 그대로 반환
    }

    override func startLoading() {
        guard let url = request.url else {
            client?.urlProtocol(self, didFailWithError: URLError(.badURL))
            return
        }

        if let (data, response) = MockURLProtocol.mockResponses[url] {
            if let response = response {
                client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
            }

            if let data = data {
                client?.urlProtocol(self, didLoad: data)
            }
        } else {
            client?.urlProtocol(self, didFailWithError: URLError(.unsupportedURL))
        }
        
        // 로딩이 끝났다는 것을 명시적으로 작성해주어야 함.
        client?.urlProtocolDidFinishLoading(self)
    }

    override func stopLoading() {
        // 요청 중단 시 리소스 정리
    }
}


주요 메서드 설명

canInit(with:)

  • 커스텀 클래스가 특정 URL 요청을 처리할 수 있는지 여부를 결정.
  • URL 로딩 시스템이 요청을 처리할 때, 먼저 이 메서드를 호출하여 프로토콜이 요청을 처리할 수 있는지 확인.
  • 반환값이 true라면 해당 요청은 URLProtocol 인스턴스에서 처리

canonicalRequest(for:)

  • 지정한 요청의 표준(canonical) 버전을 반환
  • 같은 요청이라도 약간의 변형이 있을 수 있기 때문에 요청을 표준화하여 캐싱 시스템이나 네트워크 계층에서 중복 처리를 방지.

startLoading()

  • 요청 처리를 시작.
  • 요청 데이터를 처리하거나, 캐싱된 데이터를 반환하거나, 네트워크 요청을 시작함.
  • 클라이언트에게 데이터를 반환하기 위해서는 해당 메서드 내에서 URLProtocolClient 메서드를 호출.
  • 명시적으로 로딩을 끝내주어야 함.

stopLoading()

  • 요청 처리를 중단.
  • 요청이 취소되거나 완료된 경우 리소스를 해제하고 네트워크 작업을 중지.


코드 사용 예제

아래는 코드 사용 예제이며, 간단하고 URLSession Configuration에 ephemeral(임시의)를 넣어주면 됨.

let mockURL = URL(string: "https://example.com/api")!
let mockData = """
{
    "message": "Hello, World!"
}
""".data(using: .utf8)

MockURLProtocol.mockResponses = [
    mockURL: (
        mockData,
        HTTPURLResponse(
            url: mockURL,
            statusCode: 200,
            httpVersion: nil,
            headerFields: nil
        )
    )
]

let configuration = URLSessionConfiguration.ephemeral
configuration.protocolClasses = [MockURLProtocol.self]
let session = URLSession(configuration: configuration)
let task = session.dataTask(with: mockURL) { data, response, error in
    if let data = data {
        print("Mock Response:", String(data: data, encoding: .utf8) ?? "")
    }
}
task.resume()

 

 

활용할 수 있는 포인트

  • Mock 네트워크 응답 생성
    • 테스트 환경에서 실제 네트워크 요청을 보내지 않고, 미리 정의된 응답 데이터를 반환.
  • 데이터 가로채기
    • URL 요청의 데이터를 중간에서 변조하거나 로깅할 수 있음.
  • 보안 강화
    • 요청 헤더를 검사하고, 허가되지 않은 요청을 차단하는데 이용 가능.
  • 커스텀 로딩 로직
    • 특정 URL 요청에 대해 로컬 파일을 반환하거나 특수한 프로토콜을 처리하는 로직 구현 가능

 

 

나만의 코드 예제

import Foundation

final class CurrnecyURLSessionConfigurationMock {
    
    func configuration() -> URLSessionConfiguration {
        let configuration = URLSessionConfiguration.ephemeral
        configuration.protocolClasses = [CurrencyURLProtocol.self]
        
        return configuration
    }
    
    func setup() {
        let response: [String: Any] = [
            "data": [
                [
                    "country": "USA",
                    "currency": "USD",
                    "exchangeRate": 1.0
                ],
                [
                    "country": "South Korea",
                    "currency": "KRW",
                    "exchangeRate": 1450.5
                ],
                [
                    "country": "Japan",
                    "currency": "JPY",
                    "exchangeRate": 950.2
                ]
            ]
        ]
        
        let data = try! JSONSerialization.data(
            withJSONObject: response, options: []
        )
        
        CurrencyURLProtocol.successMock = [
            "/api/v1/data1": (200, data)
        ]
    }
}

final class CurrencyURLProtocol: URLProtocol {
    typealias Path = String
    typealias MockResponse = (statusCode: Int, data: Data?)
    
    static var successMock: [Path: MockResponse] = [:]
    static var failureErrors: [Path: Error] = [:]
    
    override class func canInit(with request: URLRequest) -> Bool {
        return true
    }
    
    override class func canonicalRequest(for request: URLRequest) -> URLRequest {
        return request
    }
    
    override func startLoading() {
        if let path = request.url?.path {
            if let mockResponse = CurrencyURLProtocol.successMock[path] {
                client?.urlProtocol(
                    self,
                    didReceive: HTTPURLResponse(
                        url: request.url!,
                        statusCode: mockResponse.statusCode,
                        httpVersion: nil,
                        headerFields: nil
                    )!,
                    cacheStoragePolicy: .notAllowed)
                mockResponse.data.map {
                    client?.urlProtocol(self, didLoad: $0)
                }
            } else if let error = CurrencyURLProtocol.failureErrors[path] {
                client?.urlProtocol(self, didFailWithError: error)
            } else {
                client?.urlProtocol(self, didFailWithError: URLError(.unsupportedURL))
            }
        } else {
            client?.urlProtocol(self, didFailWithError: URLError(.badURL))
        }
        
        client?.urlProtocolDidFinishLoading(self)
    }
    
    override func stopLoading() {
        //
    }
}


// MARK: - Usage

final class APIClient {
    func fetchAPI(url: URL) -> AnyPublisher<APIResponse, Error> {
        let mock = CurrnecyURLSessionConfigurationMock()
        let config = mock.configuration()
        mock.setup()
        
        return URLSession(configuration: config).dataTaskPublisher(for: url)
            .map(\.data) // 응답에서 데이터만 추출
            .decode(type: APIResponse.self, decoder: JSONDecoder()) // JSON 디코딩
            .receive(on: DispatchQueue.main) // 결과를 메인 스레드로 전환
            .eraseToAnyPublisher()
    }
}

// MARK: - Model

struct APIResponse: Decodable {
    var data: [DataClass]
    
    struct DataClass: Decodable {
        var country: String
        var currency: String
        var exchangeRate: Float
    }
}

 

 

(참고)

https://developer.apple.com/documentation/foundation/urlprotocol