[Swift] Alamofire를 사용하는 이유 (URLSession과 비교)

팔랑이·2025년 11월 23일

iOS/Swift

목록 보기
80/84

Alamofire는 iOS에서 네트워크 관리 시 자주 사용되는 라이브러리로,
기본 URLSession을 래핑하여 네트워크 요청과 응답 처리를 보다 간결하고 직관적으로 구성할 수 있도록 도와준다.
직접 사용해보며 체감한 Alamofire의 사용 이점을 정리하기 위해 쓰는 글


1. HTTP Method 내장

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
)

2. request() 메서드 제공

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
)

3. response() 메서드 제공

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
    : 서버에서 내려오는 데이터를 스트림 형태로 순차 처리한다. 대용량 데이터나 실시간 응답 처리에 적합함.

4. validate(statusCode:)로 응답 검증을 1차 분리

위에 예시에서도 봤지만, 상태 코드 검증을 요청 체인 중간 단계로 분리할 수 있다.

AF.request(...)
    .validate(statusCode: 200...599)
    .responseData { response in
        ...
    }

지금은 200부터 599까지 전부 받고 있어서 사실상 이점이 없지만,
정책의 변경으로 200...299 까지 받는다면 저기서만 쉽게 관리할 수 있다.

이후 statusCode에 대한 분기 자체는 URLSession과 같음.

5. 💫 (추가) EventMonitor 사용

기존에는 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)
    요청이 실제로 resume되어 전송 시작될 때 호출된다.
    “요청이 생성만 되고 안 나감” vs “실제로 전송됨” 구분할 때 가장 유용하다.
  • requestDidFinish(_ request: Request)
    요청이 종료될 때 호출된다. 성공/실패 여부와 관계 없이 “끝났다”는 신호.
    (실제 결과는 response 이벤트/response handler에서 확인)
  • requestDidCancel(_ request: Request)
    요청이 cancel()로 취소될 때 호출된다.
    Rx로 Disposables.create { request.cancel() } 해 둔 구조라면, 화면 dismiss/dispose 타이밍에 많이 찍힌다.
  • requestIsRetrying(_ request: Request)
    RequestInterceptor를 붙여서 retry를 구현한 경우, 재시도 결정 시점에 호출된다.
    “401인데 왜 retry가 안 탔지?” 같은 디버깅에 매우 좋음.
  • request(_:didParseResponse:)
    Alamofire가 받은 응답을 responseData / responseDecodable 같은 체인에서 파싱 단계로 넘겼을 때 호출된다.
    “서버 응답은 왔는데 디코딩에서 터짐” 같은 케이스를 분리해 볼 수 있음.

사용 방법

위의 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(
...
)

사용하는 이점

기존 방식(request 내부 print):

AF.request(...)
    .responseData { response in
        print("STATUS:", status)
        print("RESPONSE:", json)
    }
  • 특정 API 한 곳만 볼 수 있음
  • 요청마다 로그 코드를 반복해서 써야 함
  • 요청이 왜/언제/어디서 끊겼는지는 알기 어려움

반면, EventMonitor 사용 시

final class MyLogger: EventMonitor {
    func requestDidResume(_ request: Request) { ... }
    func requestDidCancel(_ request: Request) { ... }
    func requestIsRetrying(_ request: Request) { ... }
}

이렇게 정의해두면, 모든 요청마다 EventMonitor에 정의한 로그가 자동으로 찍히게 된다.

  • 모든 네트워크 요청을 중앙에서 관찰
  • 요청 생명주기 전체를 시간 순서대로 파악 가능
  • retry / cancel / resume 같은 “행동”이 명확히 보임
  • 로그가 비즈니스 로직(NetworkManager)에 섞이지 않아, 역할이 명확하게 분리된다.
profile
정체되지 않는 성장

0개의 댓글