오늘 알아볼 것은 스스로 Swift의 우아한 네트워킹을 자처하는 알라모파이어.
깃허브 수치를 보면 자처 할 만 해
Alamofire의 핵심 동작의 원천을 파악하는 것이 중요하므로 URL 로딩 시스템에 대한 이해가 필수적입니다.
앞서 Swift의 기본적인 URL 로딩 시스템에 대해 알아봤으므로 이 글을 작성한다.
오늘은 아무래도 코드가 많을 예정이니 주의를 요한다.
자체 HTTP 네트워킹 기능을 구현하지 않고, Foundation 프레임워크에서 제공하는 Apple의 URL 로딩 시스템을 기반으로 합니다.
우선 Alamofirer가 자체적으로 네트워크 기능을 구현한 것은 아님을 알 수 있다.
그럼 왜 이 라이브러리를 만들었을까?
Alamofire는 URL 로딩 시스템의 여러 API를 사용하기 쉬운 인터페이스로 래핑하고 HTTP 네트워킹을 사용하는 최신 애플리케이션 개발에 필요한 다양한 기능을 제공합니다.
기본적으로 URL 로딩 시스템의 API는 복잡하고, 알잘딱하게 사용하려면 불편함을 감수해야 한다.
그래서 Alamofire는 사용하기 쉬운 인터페이스와 요즘 앱 개발에 필요한 다양한 기능을 제공하는 것을 목표로 한다.
단, Alamofire는 HTTP/HTTPS 전용 라이브러리이므로 다른 스킴을 사용한다면 URLSession을 사용해야 한다.
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
를 수행하는 예시이다. 지시하는 내용은 어렵지 않은데, 이를 수행하기 위한 코드는 길고 복잡하다.
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의 편의성이기 때문에 중요한 부분은 아니다.
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
의 메서드를 호출하는 것으로 시작한다.
AF.request("https://httpbin.org/get").response { response in
debugPrint(response)
}
이건 Alamofire 공식 문서에 나오는 샘플인데, URL
로 변환할 수 있는 String
을 제공하는 가장 간단한 방법이다.
이는 실제로 Alamofire의 Session 타입에 있는 두 가지 최상위 API 중 하나로, 요청을 처리하는 데 사용됩니다.
request
는 Session
의 두 가지 최상위 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
이 뭔데?
public protocol URLConvertible : Sendable
URLConvertible
프로토콜을 채택한 타입은 URL을 구성하는 데 사용할 수 있으며, 이를 사용하여URLRequest
를 구성할 수 있습니다.
어떤 타입이든 이 프로토콜을 준수한다면 Alamofire에서 URL처럼 사용할 수 있다.
func asURL() throws -> URL
asURL
메서드를 필요로 한다. 만약 커스텀 타입을 URLConvertible
로 만들고 싶다면 asURL
메서드만 구현해주면 된다.
Almofire 라이브러리에선 몇몇 타입이 이 URLConvertible
을 채택하도록 extension
되었는데, String
과 URL
, 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
도 알아봐야겠지?
public protocol URLRequestConvertible : Sendable
URLRequestConvertible
프로토콜을 채택한 타입은URLRequest
를 안전하게 구성하는 데 사용할 수 있습니다.
URL 로딩 시스템에서의 URL
과 URLRequest
의 차이를 생각해보면 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
}
convertible
과 interceptor
를 써서 DataRequest
타입의 request
변수를 만들고 perform
을 호출한 후 request
를 반환한다.
public class DataRequest: Request, @unchecked Sendable
이 DataRequest
는 참조타입이기 때문에 해당 request에 대한 다양한 후속 처리를 할 수 있게 된다.
perform
은 요청된 작업을 시작하는 메서드 정도로만 알고 있으면 될 것 같다. 내부에서는 비동기 작업을 비롯한 다양한 작업을 수행하는데 이 포스팅에서 다룰 주제는 아닌 것 같다.
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
타입을 왜 구조체로 만들었는지 알 수 있다. 확실히 편리함을 고려한 설계인 것 같다.
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가 요청 만들기 쉽다.