
Alamofire는 iOS에서 네트워크 관리 시 자주 사용되는 라이브러리로,
기본 URLSession을 래핑하여 네트워크 요청과 응답 처리를 보다 간결하고 직관적으로 구성할 수 있도록 도와준다.
직접 사용해보며 체감한 Alamofire의 사용 이점을 정리하기 위해 쓰는 글
URLSession 쓸 때는 직접 Enum으로 구현해서 넣어줘야 했는데,
enum HTTPMethod: String {
case get = "GET"
case post = "POST"
case put = "PUT"
case delete = "DELETE"
}
var request = URLRequest(url: url)
request.httpMethod = method.rawValue
Alamofire에는 이미 타입으로 제공된다.
func request(
endpoint: String,
method: HTTPMethod = .get,
...
)
또한, URLSession 사용시 GET은 query string / 나머지는 body 를 다음과 같이 분기해야 했는데,
// GET일 경우
if method == .get, let queryItems = queryItems {
var components = URLComponents(url: url, resolvingAgainstBaseURL: false)
components?.queryItems = queryItems
request.url = components?.url
}
// POST일 경우
if method != .get, let body = body {
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try? encoder.encode(body)
}
Alamofire에서는 HTTP Method가 타입으로 분리되어 있어, 다음과 같이 간소화해서 표현할 수 있다.
AF.request(
url,
method: method,
parameters: parameters,
encoding: method == .get
? URLEncoding.default
: JSONEncoding.default,
headers: httpHeaders
)
request() 가 구현되어 있어서 url / method / parameters / encoding / headers를 한 번에 명시할 수 있다.
URLSession 사용시
request를 생성하고, httpMethod / header / body / task 등을 분리해서 이렇게 길게 작성해야 하던 것을...
guard let url = URL(string: baseURL + endpoint) else {
single(.failure(NetworkError.invalidURL))
return
}
var request = URLRequest(url: url)
request.httpMethod = method.rawValue
headers?.forEach { key, value in
request.setValue(value, forHTTPHeaderField: key)
}
if let body = body {
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try? encoder.encode(body)
}
let task = session.dataTask(with: request) { data, response, error in
...
}
task.resume()
Alamofire 사용시
request() 메서드 하나로 다음과 같이 간편하게 구현할 수 있다!
AF.request(
url,
method: method,
parameters: parameters,
encoding: encoding,
headers: httpHeaders
)
response()가 구현되어 있어서 응답받는 지점을 따로 명시적으로 관리 가능하다.
URLSession 사용시
다음과 같이 응답에 대한 처리가 dataTask 안에서 한번에 이루어지는데...
let task = session.dataTask(with: request) { data, response, error in
if let error = error {
single(.failure(error))
return
}
guard let data = data else {
single(.failure(NetworkError.noData))
return
}
guard let httpResponse = response as? HTTPURLResponse else {
single(.failure(NetworkError.unknown))
return
}
// 상태 코드 처리
switch httpResponse.statusCode {
case 200...299:
break
case 401:
single(.failure(NetworkError.unauthorized))
return
default:
single(.failure(NetworkError.serverError(httpResponse.statusCode)))
return
}
let decoded = try decoder.decode(T.self, from: data)
single(.success(decoded))
}
Alamofire 사용시
응답을 task와 분리하여 관리할 수 있어, 가독성이 매우 좋아진다.
AF.request(...)
.validate(statusCode: 200...599)
.responseData { response in
guard let status = response.response?.statusCode else {
single(.failure(NetworkError.unknown))
return
}
guard let data = response.data else {
single(.failure(NetworkError.noData))
return
}
let decoded = try decoder.decode(T.self, from: data)
single(.success(decoded))
}
참고: Alamofire의 여러 response* 메서드
- responseData
: 서버 응답을Data형태로 그대로 전달한다. 커스텀 디코딩이나 직접 파싱이 필요한 경우에 사용.- responseString
: 응답 데이터를String으로 변환해 제공하며, 로그 확인이나 텍스트 기반 응답 디버깅에 유용하다.- responseDecodable
: 응답을Decodable타입으로 자동 디코딩한다. 모델이 명확한 일반적인 API 응답 처리에 가장 많이 사용된다.- responseStream
: 서버에서 내려오는 데이터를 스트림 형태로 순차 처리한다. 대용량 데이터나 실시간 응답 처리에 적합함.
위에 예시에서도 봤지만, 상태 코드 검증을 요청 체인 중간 단계로 분리할 수 있다.
AF.request(...)
.validate(statusCode: 200...599)
.responseData { response in
...
}
지금은 200부터 599까지 전부 받고 있어서 사실상 이점이 없지만,
정책의 변경으로 200...299 까지 받는다면 저기서만 쉽게 관리할 수 있다.
이후 statusCode에 대한 분기 자체는 URLSession과 같음.
기존에는 print 문으로 응답을 확인하고 있었는데, 한 면접에서 “네트워크 요청 시 응답을 어떻게 파악하느냐”는 질문을 받았다.
당시에는 단순히 print로 찍어본다고 답했는데, 면접관께서 Alamofire 내부에 응답 로그를 관찰할 수 있는 기능이 있다고 언급해주셨다.
찾아보니 EventMonitor라는 기능이 있었고, 이게 말씀하신 기능이 아닐까 싶어 정리해보았다.
정확히 같은 의도인지는 확신할 수 없지만, 알게 된 김에 활용해보자는 생각으로 정리했다.
final class MyLogger: EventMonitor {
let queue = DispatchQueue(label: "network.logger")
func requestDidResume(_ request: Request) {
print("RESUME:", request.description)
}
func requestDidFinish(_ request: Request) {
print("FINISH:", request.description)
}
func requestDidCancel(_ request: Request) {
print("CANCEL:", request.description)
}
func requestIsRetrying(_ request: Request) {
print("RETRYING:", request.description)
}
}
requestDidResume(_ request: Request)requestDidFinish(_ request: Request)requestDidCancel(_ request: Request)Disposables.create { request.cancel() } 해 둔 구조라면, 화면 dismiss/dispose 타이밍에 많이 찍힌다.requestIsRetrying(_ request: Request)request(_:didParseResponse:)위의 EventMonitor를 session 만들 때 주입한다.
let session = Session(eventMonitors: [MyLogger()])
session.request("https://talet.site/...", method: .get)
.responseData { _ in }
기존에는 NetworkManager가 AF.request를 직접 쓰고 있어서,
EventMonitor를 제대로 쓰려면 보통 AF 대신 Session을 NetworkManager가 들고 있고
session.request(...)로 요청을 보내는 형태로 다음과 같이 리팩토링 해야 한다고 한다.
private let session: Session
private init() {
let logger = NetworkLogger()
self.session = Session(eventMonitors: [logger])
}
let request = self.session.request(
...
)
AF.request(...)
.responseData { response in
print("STATUS:", status)
print("RESPONSE:", json)
}
final class MyLogger: EventMonitor {
func requestDidResume(_ request: Request) { ... }
func requestDidCancel(_ request: Request) { ... }
func requestIsRetrying(_ request: Request) { ... }
}
이렇게 정의해두면, 모든 요청마다 EventMonitor에 정의한 로그가 자동으로 찍히게 된다.