Alamofire에 대해

JinSeok Hong·2021년 6월 30일
0

Alamofire


Alamofire란?

Alamofire is an HTTP networking library written in Swift.

Alamofire는 Swift기반의 HTTP 네트워킹 라이브러리로 iOS, macOS 개발에서 정말 유용하게 쓰인다.

주요 특징은 아래와 같이 나타난다.

  • 연결 가능한 Request/Response 메서드
  • Combine 지원
  • URL / JSON 파라미터 인코딩
  • File / Data / Stream / MultipartFormData 업로드
  • Request 혹은 Resume Data를 이용한 파일 다운로드
  • URLCredential을 통한 인증
  • HTTP Response 검증
  • Progress를 통한 Progress Closures 업로드 및 다운로드
  • cURL Command 출력
  • Requests를 동적으로 조정 및 재시도
  • TLS 인증서, 공개 키 Pinning
  • 네트워크 연결성
  • 광범위한 단위, 통합 테스트 보장

왜 Alamofire를 쓰는가?

사실 굳이 Alamofire를 쓰지 않더라도 iOS에서 기본적으로 제공하는 URLSession를 통해 네트워킹이 가능하다. 그럼에도 Alamofire를 많은 사람들이 이용하는 이유는 코드의 간소화, 가독성 측면에서 도움을 주고 여러 기능을 직접 구축하지 않아도 쉽게 사용할 수 있다는 점일 것이다.
물론 Alamofire 역시 URLSession에 기반한 라이브러리이기 때문에 크게 이질적이지 않아 사용에도 문제가 없다.

참고 : https://ichi.pro/ko/alamofire-vs-urlsession-swiftui-neteuwoking-bigyo-218576183751608


지금까지 Alamofire를 공부하고 이용했던 것들을 정리해보면서 이에 대한 개념과 활용성에 대해서 조금 더 고민해보자.


func getEvents(completion: @escaping (NetworkResult<Any>) -> Void){
    
    let url : String = APIConstants.promiseCheckEvents
    
    requestGetData(url: url, httpmethod: .get,header: header, decodeType: ResponseCalendarEvents.self) {(completionData) in
                completion(completionData)
    }
}
        
func getPromiseList(_ selectedDate: String, completion: @escaping (NetworkResult<Any>) -> Void){
    
    let url : String = APIConstants.promiseList + selectedDate
            
    requestGetData(url: url, httpmethod: .get, header: header, decodeType: ResponsePromiseList.self) {(completionData) in
                completion(completionData)
    }
}

func judgeStatus<T : Codable>(by statusCode : Int, _ data : Data, _ decodeType : T.Type) -> NetworkResult<Any>{
    let decoder = JSONDecoder()
        
    guard let decodedData = try? decoder.decode(decodeType, from: data) else{return .serverErr}
    
    switch statusCode {
    case 200: return .success(decodedData)
    case 400: return .requestErr(NetworkInfo.NO_DATA)
    case 500: return .serverErr
    default: return .networkFail
    }
}
    
func requestGetData<T : Codable> (url : String, httpmethod : HTTPMethod, header : HTTPHeaders, decodeType : T.Type, completion: @escaping (NetworkResult<Any>) -> Void){

    let dataRequest = AF.request(url,
                                method: httpmethod,
                                encoding: JSONEncoding.default,
                                headers: header).validate(statusCode: 200...500)
        
    dataRequest.responseData { response in
        switch response.result{
        case .success :
            guard let statusCode =  response.response?.statusCode else { return }
            guard let value =  response.value else{return}
            
            let networkResult = self.judgeStatus(by : statusCode, value, decodeType)
            
            completion(networkResult)
            
            case .failure:
                completion(.serverErr)
        }
    }
}

일반적으로 singleton 형태로 객체를 만들고 위의 네트워크 통신을 맡도록 하는 구조이다.
우선 반복적인 코드를 줄이기 위해 statusCode를 판단하는 부분과 dataRequest 부분을 따로 함수를 구현했다.
이로 인해 추가적인 request가 많아질수록 효과적으로 코드를 줄일 수 있다.
이렇게 정리했을 때의 장점은 각 기능별로 분리가 되어있어 코드를 보는 측면에서 좋다고 느꼈다.

여기까지가 내가 지금까지 활용해왔던 Alamofire 부분이다.


여기서 추가적으로 RequestInterceptor, EventMonitor, URLRequestConvertible를 더 알아보자.

Interceptor

우선 Interceptor는 서버에 요청을 보낼 때, 중간에 가로채서 어떤 작업을 한 뒤 서버로 보내는 역할을 한다.
서버로 Request를 보내기 전이니 공통 파라미터, 헤더, 토큰을 추가로 넣어줄 때 사용하면 편하게 사용할거라 생각한다.
또한 이를 위해선 func adapt() , func retry() 를 구현해줘야 한다. Interceptor에 따른 작동 순서는 다음과 같다.

  1. request 호출
  2. adapt() 서버로 가기전 어떠한 작업
  3. server에 작업 처리
  4. retry() : 에러를 받았을 때 재시도 작업

아래 코드 예시를 첨부한다.

class BaseInterceptor: RequestInterceptor {
    
    func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
        
        var request = urlRequest
   
        request.addValue("application/json; charset=UTF-8", forHTTPHeaderField: "Content-Type")
        
        //공통 파라메터 추가
        var dictionary = [String:String]()
        dictionary.updateValue(~~~, forKey: "~~~")
        
        do{
            request = try URLEncodedFormParameterEncoder().encode(dictionary, into: request)
        }catch{
            print(error)
        }

        completion(.success(request))
    }
    
    func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) {
        
        guard let statusCode = request.response?.statusCode else{
            completion(.doNotRetry)
            return
        }
           
        //statusCode에 따라 재시도 조건 삽입
        //completion(.doNotRetry)
        //NotificationCenter.default.post(name: NSNotification.Name(rawValue: NOTIFICATION.API.AUTH_FAIL), object: nil, userInfo: data)
    }
}

EventMonitor

다음으로는 EventMonitor이다. EventMonitor는 내부 이벤트를 보다 명확하게 볼 수 있게 해주는 로깅에 강점을 갖는다.
예시는 아래와 같으며 로그를 찍어보며 단계를 자세히 보는게 도움이 될거같다. 이밖에도 더 많은 이벤트에 따른 함수를 가지고 있다.

final class MyLogger : EventMonitor{

    func requestDidFinish(_ request: Request) {//request 끝났는지}
    func requestDidCancel(_ request: Request) {//request 취소됐는지}
    func requestIsRetrying(_ request: Request) {//request 재시도됐는지}
    func requestDidResume(_ request: Request) {
        print("reqeustDidResume") //request 잘 갔는지
    }
    func request(_ request: DataRequest, didParseResponse response: DataResponse<Data?, AFError>) {
        print("didParseResponse") //response를 잘 파싱했는지
    }
}

URLRquestConvertible

다음은 URLRequestConvertible이다. enum 타입을 통해 router 경로를 지정해주고 네트워크 스택을 구축해나가는 곳이다.
물론 사진, 유저를 검색한다고 했을 때 이 둘을 search라는 스택 하나, 그 다음 각각 photo, user 로 네트워크 스택을 쌓게 해줄 수도 있지만 HTTPMethod .get, .post에 따른 구분도 가능하게 해주며 스택이 깊을수록 경우 중복도 적어지고 enum으로 parameter, path한데 묶어서 볼 수 있으니 가독성 측면에서도 좋을거같다.
이렇게 구축한 경로를 func asURLRequest() throws -> URLRequest를 통해 request를 통해 반환해준다.

enum SearchRouter: URLRequestConvertible {
    case SearchPhotos(terms: String)
    case SearchUsers(terms: String)
    
    var baseURL: URL {
        return URL(string: API.BASE_URL + "search")!
    }
    
    var method: HTTPMethod {
        switch self {
        case .SearchPhotos: return .get
        case .SearchUsers: return .get
        }
    }
    
    var path: String {
        switch self {
        case .SearchPhotos: return "/photos"
        case .SearchUsers: return "/users"
        }
    }
    var parameters :[String:String]{
        
        switch self {
        case let .SearchPhotos(terms): return ["query": terms]
        case let .SearchUsers(terms): return ["query": terms]
        }
    }
    
    func asURLRequest() throws -> URLRequest {
        let url = baseURL.appendingPathComponent(path)
        var request = URLRequest(url: url)
        
        request.method = method
        
        request = try URLEncodedFormParameterEncoder().encode(parameters, into: request)
        
        switch self {
        case let .SearchPhoto(parameters):
            request = try URLEncodedFormParameterEncoder().encode(parameters, into: request)
        case let .SearchUser(parameters):
            request = try JSONParameterEncoder().encode(parameters, into: request)
        }
        
        return request
    }
}

적용하기

위에서 새롭게 배운 인터셉터, 로거, 라우터를 이제 등록을 시켜줘야한다. 제일 처음 만든 싱글톤 객체(ex. AlamofireManager)에 세션을 설정해주고 등록해주면 된다.
로거와 인터셉터는 필요에 따라 dictionary 형태로 여러 개 등록이 가능하고 라우터는 비슷한 네트워크를 구축하는 것들끼리 라우터를 만들어서 request할 때 호출해주면 된다.


//인터셉터
let interceptors = Interceptor(interceptors:[BaseInterceptor()])

//로거 설정
let monitors = [MyLogger()]

//세션 설정
var session : Session

private init(){
    session = Session(interceptor: interceptors, eventMonitors: monitors)
}

func getPhotos(searchTerm userInput : String, completion : @escaping(Result<[Photo], MyError>) -> Void){
    
    self.session
        .request(SearchRouter.SearchPhotos(terms: userInput))
        .validate(statusCode: 200..<401)
        .responseJSON { (response) in
        
        //response 처리
    }
}

효율적인 서버 통신을 위해 화이팅!

1개의 댓글

comment-user-thumbnail
2021년 7월 6일

정리가 잘 되어있네요. 개발을 공부하는 분들에게 많은 도움이 될 것 같아요 😊

답글 달기