Swift URLProtocol (URLSessionConfiguration)
최근 서비스에서 애드가드를 막기 위한 방법을 고민하고 있는데, 네트워크 관련 부분을 학습함.
학습 후에 테스트코드, 퍼널 분석을 위한 로그 및 시스템 os로그 기록 등 더 다양하게 활용할 수 있을 것으로 기대.
학습 예제 파일 (SPM - Currency)
목차
- 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
'apple > iOS, UIKit, Documentation' 카테고리의 다른 글
iOS 히치(hitch)에 대해서 알아보기 (2) | 2024.12.26 |
---|---|
iOS 최적화된 디스크 쓰기 관리 (1) | 2024.12.24 |
Swift Mixin and Trait (1) | 2024.11.18 |
[Swift] Timer + RunLoop, backgroundQueue (swift-corelibs-foundation) (3) | 2024.10.15 |
iOS CoreData Relationships (0) | 2024.10.13 |