HIT해

[iOS/Swift] Alamofire 본문

Swift/Swift CS

[iOS/Swift] Alamofire

힛해 2024. 8. 22. 14:53
728x90

https://github.com/Alamofire/Alamofire

 

GitHub - Alamofire/Alamofire: Elegant HTTP Networking in Swift

Elegant HTTP Networking in Swift. Contribute to Alamofire/Alamofire development by creating an account on GitHub.

github.com

 

Alamofire란?

Swift로 작성된 HTTP 네트워킹 라이브러리

특징

  • 간편한 요청 및 응답 처리가 가능하다
  • ios13 이상에서 작동
  • URL / JSON 매개변수 인코딩
  • HTTP 응답 검증
  • 네트워크 상태관리 용이

URLSession 기반인데 굳이 왜?

⇒ 어려운 네트워킹 작업을 감춰주어 주요 로직에 집중할 수 있게 해준다.

이를 통해 쉽게 통신이 가능해지고 가독성이 향상된다

Alamofire 주요 함수

  • AF.upload : 스트림, 파일 메소드 등을 통해 파일을 업로드한다
  • AF.download : 파일을 다운로드 하거나, 이미 진행중인 다운로드를 재개한다.
  • AF.request : 파일 전송과 관련없는 HTTP 요청

Alamofire 메소드는 전역 메소드기 때문애 이들을 사용하기 위해 클래스 인스턴스를 따로 생성할 필요는 없다.

MVVM 패턴상의 viewController에 구현예시를 알아보자

GET 통신 예시 코드

import Alamofire

AF.request("요청할 주소")
	.response { response in
			debugPring(response)
			

보통 이렇게 반환받은 JSON 데이터는 복잡하기때문애 바로 사용하기에는 무리가있다.

따라서 이 데이터를 저장할 model을 만들 필요가 있다.

Alamofire의 Request 와 Response

요청은 request 응답은 response ( 당연한 얘기인가..? )

  1. request

요청으로 보낼 수 있는 HTTPMethod를 AF에서는 아래와 같이 enum으로 정의해두었습니다.

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

    public let rawValue: String

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

RawRepresentable란?

Swift의 프로토콜로 타입이 원시 값과 직접적으로 연관될 수 있도록 해주는 프로토콜.

원시 값을 사용하여 초기화하거나 변환할 수 있는 기능을 제공

  1. rawValue
  • 타입의 원시 값을 나타내는 프로퍼티
  1. init?(rawValue:)
  • 원시 값으로부터 타입의 인스턴스를 초기화하는 이니셜라이저. 원시값이 유효하지 않을 경우 nil을 반환
enum Direction: String {
    case north = "North"
    case south = "South"
    case east = "East"
    case west = "West"
}

// 원시 값으로부터 열거형 인스턴스를 생성
if let direction = Direction(rawValue: "North") {
    print("방향: \\(direction)")  // 출력: 방향: north
}

// 열거형 인스턴스의 원시 값 가져오기
let directionString = Direction.north.rawValue
print("방향의 원시 값: \\(directionString)")  // 출력: 방향의 원시 값: North

Direction 열거형은 String을 원시 값으로 사용하고

rawValue 프로퍼티를 사용하여 열거형의 원시 값을 가져오거나 init 초기화 구문을 사용하여 원시 값으로 부터 열거형 인스턴스를 생성할 수 있다.

이를 통해 데이터의 직렬화, 네트워크 통신, 타입 안전성을 보장하는데 매우 유용하다.

Hashable

객체를 해시값으로 변환할 수 있는 타입을 정의한다.

해시 값은 객체를 고유하게 식별하는 정수 값이며 주로 해시 기반 컬렉션 Set Dictionary 에서 객체를 효율적으로 저장하고 검색하는데에 사용

Hashable 프로토콜의 역할

  1. 해시 값 생성:
    • 객체의 해시 값을 생성하여 고유하게 식별할 수 있도록 합니다. 이 해시 값은 객체의 동등성을 검사하거나 해시 기반 컬렉션에서 객체를 저장하는 데 사용됩니다.
  2. 집합 및 딕셔너리의 키:
    • Hashable을 채택한 타입은 Set이나 Dictionary와 같은 해시 기반 컬렉션에서 키로 사용할 수 있습니다. 해시 값 덕분에 이러한 컬렉션은 객체를 빠르게 찾을 수 있습니다.
  3. 해시 충돌 처리:
    • 해시 값이 충돌(다양한 객체가 동일한 해시 값을 가짐)할 수 있지만, Swift의 해시 기반 컬렉션은 이러한 충돌을 효율적으로 처리합니다.

Hashhable 구현

struct Person: Hashable {
    let name: String
    let age: Int

    // hash(into:) 메서드를 구현하여 해시 값을 생성
    func hash(into hasher: inout Hasher) {
        hasher.combine(name)
        hasher.combine(age)
    }

    // Equatable 프로토콜의 == 연산자 구현
    static func ==(lhs: Person, rhs: Person) -> Bool {
        return lhs.name == rhs.name && lhs.age == rhs.age
    }
}

사용예시

Hashable을 채택한 타입은 Set이나 Dictionary의 키로 사용될 수 있다.

var peopleSet: Set<Person> = []

let person1 = Person(name: "Alice", age: 30)
let person2 = Person(name: "Bob", age: 25)

peopleSet.insert(person1)
peopleSet.insert(person2)

print(peopleSet.contains(person1))  // 출력: true
print(peopleSet.contains(Person(name: "Alice", age: 30)))  // 출력: true

Hashable 프로토콜은 Swift에서 객체를 해시 값으로 변환할 수 있는 기능을 제공하여, 해시 기반 컬렉션에서 객체를 효율적으로 저장하고 검색할 수 있게 합니다. 이를 통해 Set, Dictionary와 같은 데이터 구조에서 빠르고 효과적인 데이터 관리가 가능해집니다. Hashable을 올바르게 구현하면, 객체의 유일성과 일관성을 보장하면서 성능을 최적화할 수 있습니다.

이렇게 정의된 요청을 아래와 같이 request메서드를 통해 원하는 API 서버에 찔러줄 수 있다.

AF.request("<https://green.com/get>")
AF.request("<https://green.com/post>", method: .post)
AF.request("<https://green.com/delete>", method: .delete)

Get 요청은 바디에 값을 넣어 요청할 수 없다!

  1. response

요청문장 뒤에 오는 메서드다.

AF.request("<https://green.com/get>")
  .response()
  1. respnse
  • 단순히 요청에 의해 반환된 응답값을 전달
AF.request("<https://green.com/get>")
  .response { data in
  // data 후속 처리 
}
  1. responseData

아래 코드와 같이 정의되어 있으며 응답으로 넘어온 데이터를 유효성을 체크하고 데이터로 전달

public func responseData(
  queue: DispatchQueue = .main,
  dataPreprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor,
  emptyResponseCodes: Set<Int> = DataResponseSerializer.defaultEmptyResponseCodes,
  emptyRequestMethods: Set<HTTPMethod> = DataResponseSerializer.defaultEmptyRequestMethods,
  completionHandler: @escaping (AFDataResponse<Data>) -> Void
) -> Self {
  response(
    queue: queue,
    responseSerializer: DataResponseSerializer(
      dataPreprocessor: dataPreprocessor,
      emptyResponseCodes: emptyResponseCodes,
      emptyRequestMethods: emptyRequestMethods
    ),
    completionHandler: completionHandler
  )
}

사용예시

AF.request("<https://green.com/get>")
  .responseData { data in
  // data 후속 처리 
}
  1. responseString
  • 위 responseData와 비슷하게 응답으로 넘어온 데이터를 인코딩을 시켜 String 즉 문자열로 전달
public func responseString(
  queue: DispatchQueue = .main,
  dataPreprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor,
  encoding: String.Encoding? = nil,
  emptyResponseCodes: Set<Int> = StringResponseSerializer.defaultEmptyResponseCodes,
  emptyRequestMethods: Set<HTTPMethod> = StringResponseSerializer.defaultEmptyRequestMethods,
  completionHandler: @escaping (AFDataResponse<String>) -> Void
) -> Self {
  response(
    queue: queue,
    responseSerializer: StringResponseSerializer(
      dataPreprocessor: dataPreprocessor,
      encoding: encoding,
      emptyResponseCodes: emptyResponseCodes,
      emptyRequestMethods: emptyRequestMethods
    ),
    completionHandler: completionHandler
  )
}

사용예시

AF.request("<https://green.com/get>")
  .responseString { data in
  // data 후속 처리 
}
  1. responseJSON
  • 넘어온 데이터를 JSONResponseSerializer 를 이용해서 Any 타입으로 변경해 전달
public func responseJSON(
  queue: DispatchQueue = .main,
  dataPreprocessor: DataPreprocessor = JSONResponseSerializer.defaultDataPreprocessor,
  emptyResponseCodes: Set<Int> = JSONResponseSerializer.defaultEmptyResponseCodes,
  emptyRequestMethods: Set<HTTPMethod> = JSONResponseSerializer.defaultEmptyRequestMethods,
  options: JSONSerialization.ReadingOptions = .allowFragments,
  completionHandler: @escaping (AFDataResponse<Any>) -> Void
) -> Self {
  response(
    queue: queue,
    responseSerializer: JSONResponseSerializer(
      dataPreprocessor: dataPreprocessor,
      emptyResponseCodes: emptyResponseCodes,
      emptyRequestMethods: emptyRequestMethods,
      options: options
    ),
    completionHandler: completionHandler
  )
}

사용예시

AF.request("<https://green.com/get>")
  .responseJSON { data in
  // data 후속 처리 
}
  1. responseDecodable
  • 넘어온 데이터를 DecodableResponseSerializer를 이용해 Decodable 타입으로 변경해 전달
public func responseDecodable<T: Decodable>(
  of type: T.Type = T.self,
  queue: DispatchQueue = .main,
  dataPreprocessor: DataPreprocessor = DecodableResponseSerializer<T>.defaultDataPreprocessor,
  decoder: DataDecoder = JSONDecoder(),
  emptyResponseCodes: Set<Int> = DecodableResponseSerializer<T>.defaultEmptyResponseCodes,
  emptyRequestMethods: Set<HTTPMethod> = DecodableResponseSerializer<T>.defaultEmptyRequestMethods,
  completionHandler: @escaping (AFDataResponse<T>) -> Void
) -> Self {
  response(
    queue: queue,
    responseSerializer: DecodableResponseSerializer(
      dataPreprocessor: dataPreprocessor,
      decoder: decoder,
      emptyResponseCodes: emptyResponseCodes,
      emptyRequestMethods: emptyRequestMethods
    ),
    completionHandler: completionHandler
  )
}

사용예시

AF.request("<https://green.com/get>")
  .responseDecodable { data in
  // data 후속 처리 
}
  1. responseStream

넘어온 데이터를 DataStreamSerializer를 이용해 스트림으로 변경해 전달

publicfuncresponseStream<Serializer: DataStreamSerializer>(
  using serializer: Serializer,
  on queue: DispatchQueue = .main,
  stream:@escaping Handler<Serializer.SerializedObject, AFError>
) ->Self {
let parser = { [unownedself] (data: Data)inself.serializationQueue.async {
      // Start work on serialization queue.
let result = Result {try serializer.serialize(data) }
        .mapError { $0.asAFError(or: .responseSerializationFailed(reason: .customSerializationFailed(error: $0))) }
      // End work on serialization queue.
self.underlyingQueue.async {
self.eventMonitor?.request(self, didParseStream: result)

if result.isFailure,self.automaticallyCancelOnStreamError {
self.cancel()
        }

        queue.async {
self.capturingError {
try stream(.init(event: .stream(result), token: .init(self)))
          }

self.updateAndCompleteIfPossible()
        }
      }
    }
  }

  $streamMutableState.write { $0.streams.append(parser) }
  appendStreamCompletion(on: queue, stream: stream)

returnself
}

AF 사용하기

// Automatic String to URL conversion, Swift concurrency support, and automatic retry.
let response = await AF.request("<https://httpbin.org/get>", interceptor: .retryPolicy)
                       // Automatic HTTP Basic Auth.
                       .authenticate(username: "user", password: "pass")
                       // Caching customization.
                       .cacheResponse(using: .cache)
                       // Redirect customization.
                       .redirect(using: .follow)
                       // Validate response code and Content-Type.
                       .validate()
                       // Produce a cURL command for the request.
                       .cURLDescription { description in
                         print(description)
                       }
                       // Automatic Decodable support with background parsing.
                       .serializingDecodable(DecodableType.self)
                       // Await the full response with metrics and a parsed body.
                       .response
// Detailed response description for easy debugging.
debugPrint(response)
alamofireSession.request(url, method: .patch, parameters: body, encoder: JSONParameterEncoder.default)
  .validate(statusCode: 200..<300)
  .publishData()
  .tryMap({ dataResponse -> ResponseDto in
    switch dataResponse.result {
    // 응답에 대해 정상일 경우
    case let .success(data):
      do {
        return try JSONDecoder().decode(ResponseDto.self, from: data)
      } catch {
        // Response 디코딩 실패에 대한 에러 처리
        throw ErrorFactory.decodeResponseDtoFailed(
          url: url,
          body: body,
          data: data,
          underlying: error
        )
      }
    // 응답에 대해 실패했을때
    case let .failure(error):
      // 요청 실패
      throw ErrorFactory.requestFailed(
        url: url,
        body: body,
        underlying: error
      )
    }
  })
  .tryMap({ dto -> ResponseDto in
    if dto.response.success == false {
      throw ErrorFactory.actionFailed(
        url: url,
        body: body,
        responseDto: dto
      )
    } else {
      return dto
    }
  })