Alamofire

GUNDY·2일 전
1

iOS

목록 보기
4/4

Alamofire

오늘 알아볼 것은 스스로 Swift의 우아한 네트워킹을 자처하는 알라모파이어.

깃허브 수치를 보면 자처 할 만 해

Alamofire의 핵심 동작의 원천을 파악하는 것이 중요하므로 URL 로딩 시스템에 대한 이해가 필수적입니다.

앞서 Swift의 기본적인 URL 로딩 시스템에 대해 알아봤으므로 이 글을 작성한다.

오늘은 아무래도 코드가 많을 예정이니 주의를 요한다.


Introduction

자체 HTTP 네트워킹 기능을 구현하지 않고, Foundation 프레임워크에서 제공하는 Apple의 URL 로딩 시스템을 기반으로 합니다.

우선 Alamofirer가 자체적으로 네트워크 기능을 구현한 것은 아님을 알 수 있다.

그럼 왜 이 라이브러리를 만들었을까?

Alamofire는 URL 로딩 시스템의 여러 API를 사용하기 쉬운 인터페이스로 래핑하고 HTTP 네트워킹을 사용하는 최신 애플리케이션 개발에 필요한 다양한 기능을 제공합니다.

기본적으로 URL 로딩 시스템의 API는 복잡하고, 알잘딱하게 사용하려면 불편함을 감수해야 한다.

그래서 Alamofire는 사용하기 쉬운 인터페이스와 요즘 앱 개발에 필요한 다양한 기능을 제공하는 것을 목표로 한다.

단, Alamofire는 HTTP/HTTPS 전용 라이브러리이므로 다른 스킴을 사용한다면 URLSession을 사용해야 한다.

예시

URL 로딩 시스템

let url = URL(string: "https://example.com/api/login")!
var request = URLRequest(url: url)

request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")

let body: [String: Any] = ["username": "test", "password": "1234"]

request.httpBody = try? JSONSerialization.data(withJSONObject: body)

let task = URLSession.shared.dataTask(with: request) { data, response, error in
    if let error = error {
        print("Error: \(error)")
        return
    }

    guard let data = data else { return }
    
    do {
        let json = try JSONSerialization.jsonObject(with: data)
        
        print(json)
    } catch {
        print("JSON parsing error: \(error)")
    }
}

task.resume()

Post를 수행하는 예시이다. 지시하는 내용은 어렵지 않은데, 이를 수행하기 위한 코드는 길고 복잡하다.

Alamofire

import Alamofire

let parameters = ["username": "test", "password": "1234"]

AF.request("https://example.com/api/login",
           method: .post,
           parameters: parameters,
           encoding: JSONEncoding.default)
    .responseJSON { response in
        switch response.result {
        case .success(let json):
            print(json)
        case .failure(let error):
            print("Error: \(error)")
        }
    }

앞서 다양하게 메서드를 호출하고 프로퍼티도 할당해줘야 했던 URL 로딩 시스템과 달리 Alamofire의 특징인 composable로 한 번에 해결할 수 있다.

Alamofire에서 이야기하는 composable
메서드 체이닝을 통해 여러 기능을 직관적이고 유연하게 조합할 수 있는 방식

SwiftUI에 익숙한 사람이라면 수정자 체인을 떠올릴지도 모르겠다.

예시 코드는 ChatGPT를 통해 생성했으므로 오류가 있을 수 있으나, 여기서 얘기하고자 하는 것은 Almofire의 편의성이기 때문에 중요한 부분은 아니다.


Session

open class Session: @unchecked Sendable

URLSession과 마찬가지로 요청을 생성하는 역할을 하는 세션이다.

public let session: URLSession

앞서 말했든 내부적으로 URL 로딩 시스템을 사용하기 때문에 URLSession 타입의 프로퍼티를 갖고 있다.

public static let `default` = Session()

URLSession.shared와 마찬가지로 공유하는 싱글톤 객체가 있다.

public let AF = Session.default

Alamofire 라이브러리는 이 Session.default 싱글톤 객체를 AF라는 전역 변수로 사용한다. 그래서 공식 문서에 작성된 샘플 코드들은 보통 이 AF의 메서드를 호출하는 것으로 시작한다.


Request

AF.request("https://httpbin.org/get").response { response in
    debugPrint(response)
}

이건 Alamofire 공식 문서에 나오는 샘플인데, URL로 변환할 수 있는 String을 제공하는 가장 간단한 방법이다.

이는 실제로 Alamofire의 Session 타입에 있는 두 가지 최상위 API 중 하나로, 요청을 처리하는 데 사용됩니다.

requestSession의 두 가지 최상위 API 중 하나이다.

open func request<Parameters: Encodable>(_ convertible: URLConvertible,
                                         method: HTTPMethod = .get,
                                         parameters: Parameters? = nil,
                                         encoder: ParameterEncoder = URLEncodedFormParameterEncoder.default,
                                         headers: HTTPHeaders? = nil,
                                         interceptor: RequestInterceptor? = nil) -> DataRequest

URLConvertible의 매개변수 convertible을 제외하고는 모두 매개변수 기본값을 갖고 있다.

그래서 URLConvertible이 뭔데?

URLConvertible

public protocol URLConvertible : Sendable

URLConvertible 프로토콜을 채택한 타입은 URL을 구성하는 데 사용할 수 있으며, 이를 사용하여 URLRequest를 구성할 수 있습니다.

어떤 타입이든 이 프로토콜을 준수한다면 Alamofire에서 URL처럼 사용할 수 있다.

func asURL() throws -> URL

asURL 메서드를 필요로 한다. 만약 커스텀 타입을 URLConvertible로 만들고 싶다면 asURL 메서드만 구현해주면 된다.

준수 타입

Almofire 라이브러리에선 몇몇 타입이 이 URLConvertible을 채택하도록 extension 되었는데, StringURL, URLComponents 등이 있다.

extension String: URLConvertible {
    /// Returns a `URL` if `self` can be used to initialize a `URL` instance, otherwise throws.
    ///
    /// - Returns: The `URL` initialized with `self`.
    /// - Throws:  An `AFError.invalidURL` instance.
    public func asURL() throws -> URL {
        guard let url = URL(string: self) else { throw AFError.invalidURL(url: self) }

        return url
    }
}

Alamofire에 작성되어 있는 String 코드.


open func request(_ urlRequest: URLRequestConvertible, 
                  interceptor: RequestInterceptor? = nil) -> DataRequest

request의 두 번째 버전은 보다 심플하다. 6개의 파라미터에서 2개의 파라미터로 요구하는 파라미터의 수가 줄었다.

당연히 URLRequestConvertible도 알아봐야겠지?

URLRequestConvertible

public protocol URLRequestConvertible : Sendable

URLRequestConvertible 프로토콜을 채택한 타입은 URLRequest를 안전하게 구성하는 데 사용할 수 있습니다.

URL 로딩 시스템에서의 URLURLRequest의 차이를 생각해보면 URLRequestConvertible을 받는 메서드가 왜 더 파라미터가 간단한지 알 수 있을 것이다.

func asURLRequest() throws -> URLRequest

마찬가지로 asURLRequest 메서드를 필요로 한다.

준수 타입

extension을 통해 채택하도록 구현된 URLRequest가 있다. 자기 자신을 asURLRequest()의 반환값으로 한다. 그 밖에도 Session.RequestConvertible도 있고, 더 있을 수도 있는데 다 뒤져보지는 않았다. 확인은 여러분들의 몫으로.


open func request(_ convertible: any URLConvertible,
                  method: HTTPMethod = .get,
                  parameters: Parameters? = nil,
                  encoding: any ParameterEncoding = URLEncoding.default,
                  headers: HTTPHeaders? = nil,
                  interceptor: (any RequestInterceptor)? = nil,
                  requestModifier: RequestModifier? = nil) -> DataRequest {
    let convertible = RequestConvertible(url: convertible,
                                         method: method,
                                         parameters: parameters,
                                         encoding: encoding,
                                         headers: headers,
                                         requestModifier: requestModifier)

    return request(convertible, interceptor: interceptor)
}

앞선 request의 구현부를 보면 이 메서드도 내부적으로는 2개의 파라미터를 받는 버전을 호출함을 알 수 있다.

그럼 내부적으로 어떻게 동작하는지 알아보기 위해서는 request(_:interceptor:)를 알아봐야겠지?

open func request(_ convertible: any URLRequestConvertible, interceptor: (any RequestInterceptor)? = nil) -> DataRequest {
    let request = DataRequest(convertible: convertible,
                              underlyingQueue: rootQueue,
                              serializationQueue: serializationQueue,
                              eventMonitor: eventMonitor,
                              interceptor: interceptor,
                              delegate: self)

    perform(request)

    return request
}

convertibleinterceptor를 써서 DataRequest 타입의 request 변수를 만들고 perform을 호출한 후 request를 반환한다.

public class DataRequest: Request, @unchecked Sendable

DataRequest는 참조타입이기 때문에 해당 request에 대한 다양한 후속 처리를 할 수 있게 된다.

perform은 요청된 작업을 시작하는 메서드 정도로만 알고 있으면 될 것 같다. 내부에서는 비동기 작업을 비롯한 다양한 작업을 수행하는데 이 포스팅에서 다룰 주제는 아닌 것 같다.

HTTP Methods

HTTP/HTTPS 전용 라이브러리답게 HTTP Method를 표현하는 타입이 있다.

public struct HTTPMethod: RawRepresentable, Equatable, Hashable {
    public static let connect = HTTPMethod(rawValue: "CONNECT")
    public static let delete = HTTPMethod(rawValue: "DELETE")
    public static let get = HTTPMethod(rawValue: "GET")
    public static let head = HTTPMethod(rawValue: "HEAD")
    public static let options = HTTPMethod(rawValue: "OPTIONS")
    public static let patch = HTTPMethod(rawValue: "PATCH")
    public static let post = HTTPMethod(rawValue: "POST")
    public static let put = HTTPMethod(rawValue: "PUT")
    public static let query = HTTPMethod(rawValue: "QUERY")
    public static let trace = HTTPMethod(rawValue: "TRACE")

    public let rawValue: String

    public init(rawValue: String) {
        self.rawValue = rawValue
    }
}

RFC 7231 §4.3에 나온 HTTP Method를 나열하지만 열거형이 아닌 구조체이다.

HTTP 메서드를 나타내는 유형입니다. 원시 String 값은 대소문자를 구분하여 저장되고 비교됩니다.
HTTPMethod.get != HTTPMethod(rawValue: "get").

타입 설명에서 원시값 문자열은 대소문자를 구분하기 때문에 HTTPMethod(rawValue: "get")HTTPMethod.get과 다르다고 설명한다. 당연하겠지 String인데...

AF.request("https://httpbin.org/get")
AF.request("https://httpbin.org/post", method: .post)
AF.request("https://httpbin.org/put", method: .put)
AF.request("https://httpbin.org/delete", method: .delete)

하지만 사용하는 모습을 보면 영락없는 열거형같다. ㅋㅋ

extension HTTPMethod {
    static let custom = HTTPMethod(rawValue: "CUSTOM")
}

AF.request("https://httpbin.org/headers", method: .custom)

Alamofire의 HTTPMethod 타입이 지원하지 않는 HTTP 메서드를 사용해야 하는 경우 타입을 확장하여 사용자 정의 값을 추가할 수 있습니다.

extension URLRequest {
    /// Returns the `httpMethod` as Alamofire's `HTTPMethod` type.
    public var method: HTTPMethod? {
        get { httpMethod.flatMap(HTTPMethod.init) }
        set { httpMethod = newValue?.rawValue }
    }
}

커스텀 메서드 정의를 안내하는 부분이나, URLRequest를 확장하여 HTTPMethod? 타입의 method 프로퍼티를 추가한 것을 보면 HTTPMethod 타입을 왜 구조체로 만들었는지 알 수 있다. 확실히 편리함을 고려한 설계인 것 같다.

RequestModifier

open func request<Parameters: Encodable & Sendable>(_ convertible: any URLConvertible,
                                                    method: HTTPMethod = .get,
                                                    parameters: Parameters? = nil,
                                                    encoder: any ParameterEncoder = URLEncodedFormParameterEncoder.default,
                                                    headers: HTTPHeaders? = nil,
                                                    interceptor: (any RequestInterceptor)? = nil,
                                                    requestModifier: RequestModifier? = nil) -> DataRequest

기본 버전의 매개변수로는 충분하지 않을 때 RequestModifier를 전달하는 버전의 request 메서드를 사용할 수도 있다.

public typealias RequestModifier = @Sendable (inout URLRequest) throws -> Void

RequestModifier는 단순히 URLRequest를 받는 클로저이다. 이를 통해 리퀘스트에 대한 수정이 가능하다.

AF.request("https://httpbin.org/get", requestModifier: { $0.timeoutInterval = 5 }).response(...)

공식문서가 제공하는 timeoutInterval을 5초로 수정하는 샘플이다.

그밖에도 설명하지 않은 리퀘스트와 관련한 다양한 매개변수나 프로퍼티가 있지만, URL 로딩 시스템에서도 그런 것들을 하나하나 다루지는 않았다.

궁금하다면 자발적으로 더 찾아보자!


마무리

URL 로딩 시스템에 대한 포스팅에서도 응답에 대한 내용은 거의 작성하지 않았기 때문에 일단 오늘 Alamofire 포스팅도 요청에 대한 내용 위주로 담았다.
뭐 글이 길어지는 게 싫기도 하고...

그런데 사실 응답에 대한 코드도 무척이나 많다. 예를 들면 응답에 대한 검증, 캐싱 및 기타 등등.

응답에 대한 포스팅을 할지 말지는 참 고민이지만, 일단 오늘의 포스팅 정도로 Alamofire라는 라이브러리에 대한 기본적인 내용은 공유가 되지 않았을까 싶기도 하다.

뻔한 결론: HTTP/HTTPS 요청을 보낼 때, URL 로딩 시스템보다 Alamofire가 요청 만들기 쉽다.

profile
개발자할건디?

0개의 댓글