개요
Alamofire는 Swift로 작성된 HTTP 네트워킹 라이브러리로, Apple의 URLSession과 Foundation 네트워킹을 기반으로 구축된 고급 라이브러리. URLSession의 복잡성을 추상화하여 더 간단하고 직관적인 API를 제공
설치 방법
https://github.com/Alamofire/Alamofire.git 주소를 Xcode file -> Add Package Dependencies로 추가
핵심 아키텍처
주요 컴포넌트
Session
- 모든 네트워킹 작업의 중심점
- URLSession을 래핑하여 설정과 관리 담당
- 기본 세션(
AF)과 커스텀 세션 생성 가능
Request 타입들
DataRequest: 일반적인 HTTP 요청DownloadRequest: 파일 다운로드 요청UploadRequest: 파일 업로드 요청DataStreamRequest: 스트리밍 요청
Response 처리
- 응답 검증, 직렬화, 에러 처리 담당
- JSON, String, Data 등 다양한 형태로 응답 파싱
내부 동작 원리
- Request 생성: URL과 파라미터를 바탕으로 URLRequest 객체 생성
- Session 처리: URLSession을 통해 실제 네트워크 요청 실행
- Response 검증: 상태 코드, 콘텐츠 타입 등 검증
- 데이터 직렬화: 응답 데이터를 원하는 형태로 변환
- Completion Handler 실행: 결과를 메인 큐 또는 지정된 큐에서 처리
기본 HTTP 요청
GET 요청
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| import Alamofire
// 기본 GET 요청
AF.request("https://httpbin.org/get")
.response { response in
debugPrint(response)
}
// JSON 응답 처리
AF.request("https://jsonplaceholder.typicode.com/users")
.responseJSON { response in
switch response.result {
case .success(let value):
print("JSON: \(value)")
case .failure(let error):
print("Error: \(error)")
}
}
// Codable로 직접 파싱
struct User: Codable {
let id: Int
let name: String
let email: String
}
AF.request("https://jsonplaceholder.typicode.com/users/1")
.responseDecodable(of: User.self) { response in
switch response.result {
case .success(let user):
print("User: \(user.name)")
case .failure(let error):
print("Error: \(error)")
}
}
|
POST 요청
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| // JSON 파라미터
let parameters: [String: Any] = [
"name": "John Doe",
"email": "john@example.com",
"age": 30
]
AF.request("https://httpbin.org/post",
method: .post,
parameters: parameters,
encoding: JSONEncoding.default)
.responseJSON { response in
print(response.result)
}
// URL 인코딩 파라미터
AF.request("https://httpbin.org/post",
method: .post,
parameters: parameters,
encoding: URLEncoding.default)
.responseString { response in
print(response.result)
}
|
PUT, PATCH, DELETE 요청
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| // PUT 요청
AF.request("https://jsonplaceholder.typicode.com/users/1",
method: .put,
parameters: parameters,
encoding: JSONEncoding.default)
.responseJSON { response in
print(response.result)
}
// PATCH 요청
AF.request("https://jsonplaceholder.typicode.com/users/1",
method: .patch,
parameters: ["name": "Updated Name"],
encoding: JSONEncoding.default)
.responseJSON { response in
print(response.result)
}
// DELETE 요청
AF.request("https://jsonplaceholder.typicode.com/users/1",
method: .delete)
.response { response in
print("Status Code: \(response.response?.statusCode ?? 0)")
}
|
고급 기능
헤더 설정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // 기본 헤더
let headers: HTTPHeaders = [
"Authorization": "Bearer your-token",
"Content-Type": "application/json",
"Accept": "application/json"
]
AF.request("https://api.example.com/data",
headers: headers)
.responseJSON { response in
print(response.result)
}
// 동적 헤더 추가
var dynamicHeaders = HTTPHeaders()
dynamicHeaders.add(name: "X-API-Key", value: "your-api-key")
dynamicHeaders.add(name: "X-Request-ID", value: UUID().uuidString)
|
파라미터 인코딩
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // URL 파라미터 (쿼리 스트링)
let urlParameters = ["page": 1, "limit": 20]
AF.request("https://api.example.com/users",
parameters: urlParameters,
encoding: URLEncoding.queryString)
// JSON 바디
let jsonParameters = ["user": ["name": "John", "age": 30]]
AF.request("https://api.example.com/users",
method: .post,
parameters: jsonParameters,
encoding: JSONEncoding.default)
// 커스텀 인코딩
struct CustomParameterEncoding: ParameterEncoding {
func encode(_ urlRequest: URLRequestConvertible,
with parameters: Parameters?) throws -> URLRequest {
var urlRequest = try urlRequest.asURLRequest()
// 커스텀 인코딩 로직
return urlRequest
}
}
|
응답 검증
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| AF.request("https://httpbin.org/status/200")
.validate(statusCode: 200..<300)
.validate(contentType: ["application/json"])
.responseJSON { response in
switch response.result {
case .success:
print("Validation Successful")
case .failure(let error):
print("Validation Failed: \(error)")
}
}
// 커스텀 검증
AF.request("https://api.example.com/data")
.validate { request, response, data in
// 커스텀 검증 로직
if response.statusCode == 200 {
return .success(Void())
} else {
return .failure(AFError.responseValidationFailed(reason: .unacceptableStatusCode(code: response.statusCode)))
}
}
.responseJSON { response in
print(response.result)
}
|
파일 업로드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| // 단일 파일 업로드
let fileURL = Bundle.main.url(forResource: "image", withExtension: "jpg")!
AF.upload(fileURL, to: "https://httpbin.org/post")
.uploadProgress { progress in
print("Upload Progress: \(progress.fractionCompleted)")
}
.responseJSON { response in
print(response.result)
}
// 멀티파트 업로드
AF.upload(multipartFormData: { multipartFormData in
multipartFormData.append(fileURL, withName: "file")
multipartFormData.append("John Doe".data(using: .utf8)!, withName: "name")
}, to: "https://httpbin.org/post")
.uploadProgress { progress in
print("Upload Progress: \(progress.fractionCompleted)")
}
.responseJSON { response in
print(response.result)
}
// 데이터 업로드
let imageData = UIImage(named: "profile")?.jpegData(compressionQuality: 0.8)
AF.upload(imageData!, to: "https://httpbin.org/post")
.responseJSON { response in
print(response.result)
}
|
파일 다운로드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| // 기본 다운로드
AF.download("https://httpbin.org/image/png")
.downloadProgress { progress in
print("Download Progress: \(progress.fractionCompleted)")
}
.response { response in
if let data = response.value {
let image = UIImage(data: data)
// 이미지 처리
}
}
// 특정 위치에 다운로드
let destination: DownloadRequest.Destination = { _, _ in
let documentsURL = FileManager.default.urls(for: .documentDirectory,
in: .userDomainMask)[0]
let fileURL = documentsURL.appendingPathComponent("downloaded_file.png")
return (fileURL, [.removePreviousFile, .createIntermediateDirectories])
}
AF.download("https://httpbin.org/image/png", to: destination)
.response { response in
if response.error == nil, let imagePath = response.fileURL?.path {
print("File downloaded to: \(imagePath)")
}
}
// 이어받기 다운로드
let resumeData: Data? = // 이전에 저장된 resume data
if let resumeData = resumeData {
AF.download(resumingWith: resumeData)
.response { response in
print("Resume download completed")
}
}
|
인증 처리
Basic Authentication
1
2
3
4
5
6
7
8
| let user = "username"
let password = "password"
AF.request("https://httpbin.org/basic-auth/\(user)/\(password)")
.authenticate(username: user, password: password)
.responseJSON { response in
print(response.result)
}
|
Bearer Token
1
2
3
4
5
6
7
| let token = "your-bearer-token"
AF.request("https://api.example.com/protected",
headers: ["Authorization": "Bearer \(token)"])
.responseJSON { response in
print(response.result)
}
|
OAuth 2.0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| class OAuth2Handler: AuthenticationCredential {
let accessToken: String
let refreshToken: String
let expiration: Date
var requiresRefresh: Bool {
Date(timeIntervalSinceNow: 60) > expiration
}
init(accessToken: String, refreshToken: String, expiration: Date) {
self.accessToken = accessToken
self.refreshToken = refreshToken
self.expiration = expiration
}
}
let credential = OAuth2Handler(accessToken: "access-token",
refreshToken: "refresh-token",
expiration: Date(timeIntervalSinceNow: 3600))
let authenticator = OAuth2Authenticator()
let interceptor = AuthenticationInterceptor(authenticator: authenticator,
credential: credential)
AF.request("https://api.example.com/protected",
interceptor: interceptor)
.responseJSON { response in
print(response.result)
}
|
세션 관리
커스텀 세션 생성
1
2
3
4
5
6
7
8
9
10
11
12
| // 기본 설정 커스터마이징
let configuration = URLSessionConfiguration.default
configuration.timeoutIntervalForRequest = 30
configuration.timeoutIntervalForResource = 60
configuration.httpAdditionalHeaders = ["User-Agent": "MyApp/1.0"]
let session = Session(configuration: configuration)
session.request("https://api.example.com/data")
.responseJSON { response in
print(response.result)
}
|
인터셉터 사용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
| // 요청 어댑터
class APIKeyAdapter: RequestAdapter {
private let apiKey: String
init(apiKey: String) {
self.apiKey = apiKey
}
func adapt(_ urlRequest: URLRequest,
for session: Session,
completion: @escaping (Result<URLRequest, Error>) -> Void) {
var urlRequest = urlRequest
urlRequest.setValue(apiKey, forHTTPHeaderField: "X-API-Key")
completion(.success(urlRequest))
}
}
// 요청 재시도
class RetryPolicy: RequestRetrier {
func retry(_ request: Request,
for session: Session,
dueTo error: Error,
completion: @escaping (RetryResult) -> Void) {
if let response = request.task?.response as? HTTPURLResponse,
response.statusCode == 401 {
completion(.retryWithDelay(1.0))
} else {
completion(.doNotRetry)
}
}
}
let adapter = APIKeyAdapter(apiKey: "your-api-key")
let retrier = RetryPolicy()
let interceptor = Interceptor(adapter: adapter, retrier: retrier)
let session = Session(interceptor: interceptor)
|
응답 처리 및 직렬화
커스텀 Response Serializer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| extension DataRequest {
@discardableResult
func responseCustom<T: Decodable>(
of type: T.Type,
queue: DispatchQueue = .main,
completionHandler: @escaping (AFDataResponse<T>) -> Void
) -> Self {
return responseDecodable(of: type, queue: queue) { response in
// 커스텀 처리 로직
completionHandler(response)
}
}
}
// 사용 예
struct APIResponse<T: Codable>: Codable {
let success: Bool
let data: T
let message: String?
}
AF.request("https://api.example.com/users")
.responseCustom(of: APIResponse<[User]>.self) { response in
switch response.result {
case .success(let apiResponse):
if apiResponse.success {
print("Users: \(apiResponse.data)")
} else {
print("API Error: \(apiResponse.message ?? "Unknown error")")
}
case .failure(let error):
print("Network Error: \(error)")
}
}
|
Combine 지원
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| import Combine
class NetworkService {
private var cancellables = Set<AnyCancellable>()
func fetchUsers() -> AnyPublisher<[User], AFError> {
return AF.request("https://jsonplaceholder.typicode.com/users")
.publishDecodable(type: [User].self)
.value()
.eraseToAnyPublisher()
}
func loadData() {
fetchUsers()
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { completion in
switch completion {
case .finished:
print("Request completed successfully")
case .failure(let error):
print("Request failed: \(error)")
}
},
receiveValue: { users in
print("Received \(users.count) users")
}
)
.store(in: &cancellables)
}
}
|
Router 패턴
API Router 구현
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
| enum APIRouter: URLRequestConvertible {
case getUsers
case getUser(id: Int)
case createUser(parameters: Parameters)
case updateUser(id: Int, parameters: Parameters)
case deleteUser(id: Int)
static let baseURLString = "https://jsonplaceholder.typicode.com"
var method: HTTPMethod {
switch self {
case .getUsers, .getUser:
return .get
case .createUser:
return .post
case .updateUser:
return .put
case .deleteUser:
return .delete
}
}
var path: String {
switch self {
case .getUsers:
return "/users"
case .getUser(let id):
return "/users/\(id)"
case .createUser:
return "/users"
case .updateUser(let id, _):
return "/users/\(id)"
case .deleteUser(let id):
return "/users/\(id)"
}
}
var parameters: Parameters? {
switch self {
case .createUser(let parameters), .updateUser(_, let parameters):
return parameters
default:
return nil
}
}
func asURLRequest() throws -> URLRequest {
let url = try APIRouter.baseURLString.asURL()
var urlRequest = URLRequest(url: url.appendingPathComponent(path))
urlRequest.httpMethod = method.rawValue
switch self {
case .getUsers, .getUser, .deleteUser:
urlRequest = try URLEncoding.default.encode(urlRequest, with: parameters)
case .createUser, .updateUser:
urlRequest = try JSONEncoding.default.encode(urlRequest, with: parameters)
}
return urlRequest
}
}
// Router 사용
class APIService {
func getUsers(completion: @escaping ([User]?) -> Void) {
AF.request(APIRouter.getUsers)
.responseDecodable(of: [User].self) { response in
completion(response.value)
}
}
func createUser(name: String, email: String, completion: @escaping (User?) -> Void) {
let parameters: Parameters = ["name": name, "email": email]
AF.request(APIRouter.createUser(parameters: parameters))
.responseDecodable(of: User.self) { response in
completion(response.value)
}
}
}
|
에러 처리
상세한 에러 처리
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| AF.request("https://api.example.com/data")
.responseJSON { response in
switch response.result {
case .success(let value):
print("Success: \(value)")
case .failure(let error):
if let afError = error.asAFError {
switch afError {
case .invalidURL(let url):
print("Invalid URL: \(url)")
case .parameterEncodingFailed(let reason):
print("Parameter encoding failed: \(reason)")
case .multipartEncodingFailed(let reason):
print("Multipart encoding failed: \(reason)")
case .responseValidationFailed(let reason):
print("Response validation failed: \(reason)")
case .responseSerializationFailed(let reason):
print("Response serialization failed: \(reason)")
default:
print("Other AF Error: \(afError)")
}
} else {
print("Unknown error: \(error)")
}
}
}
|
커스텀 에러 타입
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
| enum APIError: Error, LocalizedError {
case invalidResponse
case noData
case decodingError
case serverError(Int)
case networkError(Error)
var errorDescription: String? {
switch self {
case .invalidResponse:
return "Invalid response"
case .noData:
return "No data received"
case .decodingError:
return "Failed to decode response"
case .serverError(let code):
return "Server error with code: \(code)"
case .networkError(let error):
return "Network error: \(error.localizedDescription)"
}
}
}
extension DataRequest {
func responseAPI<T: Codable>(
of type: T.Type,
completion: @escaping (Result<T, APIError>) -> Void
) -> Self {
return responseData { response in
switch response.result {
case .success(let data):
do {
let decodedObject = try JSONDecoder().decode(type, from: data)
completion(.success(decodedObject))
} catch {
completion(.failure(.decodingError))
}
case .failure(let error):
if let statusCode = response.response?.statusCode, statusCode >= 400 {
completion(.failure(.serverError(statusCode)))
} else {
completion(.failure(.networkError(error)))
}
}
}
}
}
|
캐싱
URL 캐시 설정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| let cache = URLCache(memoryCapacity: 20 * 1024 * 1024, // 20MB
diskCapacity: 100 * 1024 * 1024, // 100MB
diskPath: "alamofire_cache")
let configuration = URLSessionConfiguration.default
configuration.urlCache = cache
configuration.requestCachePolicy = .returnCacheDataElseLoad
let session = Session(configuration: configuration)
session.request("https://api.example.com/data")
.cacheResponse(using: ResponseCacher.cache)
.responseJSON { response in
print(response.result)
}
|
성능 최적화
연결 풀링 및 재사용
1
2
3
4
5
6
| let configuration = URLSessionConfiguration.default
configuration.httpMaximumConnectionsPerHost = 5
configuration.urlCache = URLCache(memoryCapacity: 50 * 1024 * 1024,
diskCapacity: 200 * 1024 * 1024)
let session = Session(configuration: configuration)
|
요청 취소
1
2
3
4
5
6
7
8
9
| let request = AF.request("https://api.example.com/data")
.responseJSON { response in
print(response.result)
}
// 5초 후 요청 취소
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
request.cancel()
}
|
동시 요청 제한
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| class RequestManager {
private let queue = OperationQueue()
init() {
queue.maxConcurrentOperationCount = 3
}
func performRequest(url: String, completion: @escaping (Result<Data, Error>) -> Void) {
queue.addOperation {
AF.request(url)
.responseData { response in
DispatchQueue.main.async {
completion(response.result.mapError { $0 as Error })
}
}
}
}
}
|
실제 사용 예제
MVVM 패턴과 함께 사용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| import Combine
class UserViewModel: ObservableObject {
@Published var users: [User] = []
@Published var isLoading = false
@Published var errorMessage: String?
private var cancellables = Set<AnyCancellable>()
private let apiService = APIService()
func loadUsers() {
isLoading = true
errorMessage = nil
apiService.fetchUsers()
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { [weak self] completion in
self?.isLoading = false
if case .failure(let error) = completion {
self?.errorMessage = error.localizedDescription
}
},
receiveValue: { [weak self] users in
self?.users = users
}
)
.store(in: &cancellables)
}
}
class APIService {
func fetchUsers() -> AnyPublisher<[User], Error> {
return AF.request(APIRouter.getUsers)
.publishDecodable(type: [User].self)
.value()
.mapError { $0 as Error }
.eraseToAnyPublisher()
}
}
|
이미지 다운로드 및 캐싱
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| class ImageLoader: ObservableObject {
@Published var image: UIImage?
@Published var isLoading = false
private static let cache = NSCache<NSString, UIImage>()
func loadImage(from url: String) {
let cacheKey = NSString(string: url)
if let cachedImage = Self.cache.object(forKey: cacheKey) {
self.image = cachedImage
return
}
isLoading = true
AF.request(url)
.responseData { [weak self] response in
DispatchQueue.main.async {
self?.isLoading = false
switch response.result {
case .success(let data):
if let image = UIImage(data: data) {
Self.cache.setObject(image, forKey: cacheKey)
self?.image = image
}
case .failure(let error):
print("Image loading failed: \(error)")
}
}
}
}
}
|
모니터링 및 로깅
EventMonitor 사용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| class NetworkLogger: EventMonitor {
let queue = DispatchQueue(label: "network.logger")
func requestDidResume(_ request: Request) {
print("🚀 Request started: \(request.description)")
}
func request<Value>(_ request: DataRequest, didParseResponse response: DataResponse<Value, AFError>) {
print("📥 Response received:")
print(" URL: \(request.request?.url?.absoluteString ?? "Unknown")")
print(" Status Code: \(response.response?.statusCode ?? 0)")
print(" Duration: \(response.metrics?.taskInterval.duration ?? 0)s")
if let error = response.error {
print(" ❌ Error: \(error)")
} else {
print(" ✅ Success")
}
}
}
let logger = NetworkLogger()
let session = Session(eventMonitors: [logger])
|
테스팅
Mock 세션 생성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
| class MockSession {
static func create(with data: Data?, statusCode: Int = 200) -> Session {
let configuration = URLSessionConfiguration.af.default
configuration.protocolClasses = [MockURLProtocol.self]
MockURLProtocol.responseData = data
MockURLProtocol.statusCode = statusCode
return Session(configuration: configuration)
}
}
class MockURLProtocol: URLProtocol {
static var responseData: Data?
static var statusCode: Int = 200
override class func canInit(with request: URLRequest) -> Bool {
return true
}
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
return request
}
override func startLoading() {
let response = HTTPURLResponse(url: request.url!,
statusCode: Self.statusCode,
httpVersion: nil,
headerFields: nil)!
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
if let data = Self.responseData {
client?.urlProtocol(self, didLoad: data)
}
client?.urlProtocolDidFinishLoading(self)
}
override func stopLoading() {}
}
// 테스트에서 사용
func testAPICall() {
let mockData = """
[{"id": 1, "name": "John", "email": "john@example.com"}]
""".data(using: .utf8)!
let mockSession = MockSession.create(with: mockData)
mockSession.request("https://api.example.com/users")
.responseDecodable(of: [User].self) { response in
XCTAssertTrue(response.result.isSuccess)
XCTAssertEqual(response.value?.count, 1)
}
}
|
보안 고려사항
SSL Pinning
1
2
3
4
5
6
7
| let evaluators = [
"api.example.com": PinnedCertificatesTrustEvaluator(),
"cdn.example.com": PublicKeysTrustEvaluator()
]
let manager = ServerTrustManager(evaluators: evaluators)
let session = Session(serverTrustManager: manager)
|
Certificate Pinning
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| class CustomTrustEvaluator: ServerTrustEvaluating {
func evaluate(_ trust: SecTrust, forHost host: String) throws {
// 커스텀 인증서 검증 로직
let policy = SecPolicyCreateSSL(true, host as CFString)
SecTrustSetPolicies(trust, policy)
var result: SecTrustResultType = .invalid
let status = SecTrustEvaluate(trust, &result)
guard status == errSecSuccess else {
throw AFError.serverTrustEvaluationFailed(reason: .trustEvaluationFailed(error: nil))
}
guard result == .unspecified || result == .proceed else {
throw AFError.serverTrustEvaluationFailed(reason: .trustEvaluationFailed(error: nil))
}
}
}
|
성능 모니터링
네트워크 메트릭 수집
```swift class PerformanceMonitor: EventMonitor { let queue = DispatchQueue(label: “performance.monitor”)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| func request<Value>(_ request: DataRequest, didParseResponse response: DataResponse<Value, AFError>) {
guard let metrics = response.metrics else { return }
let taskMetrics = metrics.taskMetrics.first
print("📊 Performance Metrics:")
print(" Domain Lookup: \(taskMetrics?.domainLookupTime ?? 0)s")
print(" Connect Time: \(taskMetrics?.connectTime ?? 0)s")
print(" Secure Connection: \(taskMetrics?.secureConnectionTime ?? 0)s")
print(" Request Time: \(taskMetrics?.requestTime ?? 0)s")
print(" Response Time: \(taskMetrics?.responseTime ?? 0)s")
print(" Total Time: \(metrics.taskInterval.duration)s")
if let transferSize = taskMetrics?.responseTransferSize {
print(" Transfer Size: \(transferSize) bytes")
}
} }
|