ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Alamofire를 구조화된 코드로 사용하기 (feat. URLRequsetConvertible)
    iOS 2024. 6. 19. 14:54

     

     

    Alamofire를 사용해 API를 호출하는 작업을 여러번 해보면서 Alamofire의 request 함수를 호출하는 작업이 몇 가지 변수들의 조합으로 패턴화 된다는 생각이 들었다.

     

    어떻게 하면 이 반복되는 패턴들을 효율적으로 처리할 수 있을 지 고민했던 흔적을 기록해보려 한다.

     

     

     

    이 글을 읽으면 좋은 분들

    1. Almofire 사용시 반복적인 URL 문자열 선언 작업을 줄이고 싶은 분
    2. URLRequestConvertible의 사용법과 작동원리를 풀어쓴 글을 보고싶은 분

     

     

     

     

     

    먼저 처음 alamofire를 사용하는 예시 코드를 살펴보는 것으로 본 포스트를 시작해보겠다.

     

     

    Alamofire request get request 호출 예시

     

      let url = "https://API.example.com/v1/search"
      
      AF.request(URL, method: .get, parameters: parameters, headers: headers)
            .responseDecodable(of: decodingType) { response in
                switch response.result {
                case .success(let value):
                    // value값을 처리하는 이후의 작업
                case .failure(let error):
                    // error 케이스에 대한 예외처리
                }
            }

     

     

    위의 예시 코드의 경우 총 7개의 변수가 발생한다

     

    1. URL 문자열
    2. http request method
    3. http request parameter
    4. http header
    5. response 구조체 타입
    6. response 수신 성공 시의 다음 작업을 정의한 콜백 함수
    7. response 수신 실패 시의 예외처리를 정의한 콜백 함수

     

     

    예시는 GET request에 대한 것이지만 만약 POST request의 경우에는 추가로 encoding 방식의 정의 또한 필요할 것이다.

     

    처음엔 이러한 변수들의 조합으로 나오는 모든 요청패턴들을 한 번의 반복작업 이후엔 함수의 호출만으로 간단히 사용하려 프로토콜을 작성해보았다.

     

     

    부끄럽지만 연습 프로젝트에서는 error 예외처리를 위한 콜백은 사용하지 않았다.

     

     

    비슷비슷하지만 조금씩 다른 각각의 요청 패턴들을 AlamofireRequest라는 직접 작성한 프로토콜에서 추상화하고 해당 프로토콜의 extension에 각각의 구현부를 반복해서 메서드 오버로딩한 끝에 아래와 같이 간단히 호출하여 사용할 수 있게 되었다.

     

     

     func callExampleAPIRequest() {
            getHTTPRequest(URL: exampleAPI.URL,          // enum에 정의한 String 호출
                           parameters: exampleParameters, // Alamofire.Parameters
                           headers: exampleHeaders,       // Alamofire.HTTPHeaders
                           decodingType: ExampleResult.self,// response 구조체 타입
                           callback: {(response: ExampleResult) -> () in
                               // response를 처리하는 로직  
                           }
            )
     }

     

     

    사용하는 코드에서는 마치 멋있게 추상화한 것처럼 보이지만 실제 구현코드는 비슷비슷하게 생긴 메서드들의 단순반복노가다으로 구성되어 있어 이 코드를 쓰면서도 구현부만 보면 계속 찝찝한 기분이 들었다.

     

    그렇게 연습 프로젝트를 완성한 후, 다른 더 좋은 방법을 구글링해보니 이처럼 조금씩 양식이 다른 API 요청 URL을 보다 효율적으로 생성하고 관리할 수 있는 방법으로 Alamofire에서 제공하는 URLRequestContertible 프로토콜을 사용할 수 있다는 걸 알게 되었다.

     

     

     

    Alamofire를 멋있게 사용하는 법을 알려주신 taekki님의 포스트를 첨부하며
    이하의 내용에 지대한 영향을 주셨음을 미리 밝힙니다.

     

     

     

    [Alamofire Mapper] URLRequestConvertible 사용해보기

    Alamofire 조금 더 이해하고 사용해보자 들어가기 전에 Alamofire 공식 문서를 보면 URLRequest를 사용자 정의해서 사용할 수 있는 URLRequestConvertible이라는 프로토콜이 있더라구요? 항상 기본적인 request

    taekki-dev.tistory.com

     

     

     

    Alamofire의 API 요청 메서드를 사용하는 방법은 크게 두 가지로 구분할 수 있다. 

     

    1. URLConvertible 프로토콜을 준수하는 객체를 인자로 전달하여 Session 클래스의 request 메서드를 호출
    2. URLReqeustConvertible 프로토콜을 준수하는 객체를 인자로 전달하여 Session 클래스의 request 메서드를 호출

     

     

    그리고 이 두 방법의 차이는 URLReqeust 인스턴스의 생성과정을 직접 구현하느냐 Alamofire에 이미 구현된 프로세스를 통하느냐로 구분된다. 

     

    어느 쪽이던 통신하고자 하는 API의 양식에 맞는 URLRequest 인스턴스를 생성해야한다는 점은 동일하다.

     

    하지만 전술한 Alamofire 호출 예제 코드처럼 굳이 URLRequest 인스턴스를 생성하는 코드를 작성하지 않고 URL 문자열을 인자로 넘겨주는 것만으로도 분명히 Alamofire 사용하는데는 문제가 없었는데 왜 그런 것일까.

     

    결론부터 말하자면, 이는 URLConvertible을 준수하는 객체(String, URLComponents)를 인자로 전달할 때에도 Alamofire가 내부적으로 URLRequest 인스턴스를 대신 생성해주기 때문이다.

     

    여기서 URLConvertible 프로토콜과 URLRequestConvertible 프로토콜의 관계를 살펴보면 위의 결론에 도달할 수 있다.

     

    먼저 URLRequestConvertible 프로토콜을 선언하는 코드부터 확인해 보자.

     

     

     URLRequestConvertible 프로토콜

     

    URLRequest 구조체의 extension으로 URLRequest자신을 반환하는 asURLRequest 메서드를 구현하고 있다.

     

     

    여기서 URLRequestConvertible의 추상메서드 asURLRequest는 URLRequest 구조체를 반환하는 역할을 하고 있다.

     

    이 구조체의 생성자는 URLConvertible을 준수하는 String과 HTTPMethod, HTTPHeaders를 인자로 받아 URLRequest 인스턴스를 생성하고 있다.

     

    여기서 중요한 것은 바로 필수적인 인자로 URLConvertibler을 준수하는 객체와 HTTPMethod 객체를 받는다는 것이다.

     

    이를 잘 기억해두면서 URLConvertible 프로토콜을 살펴보자.

     

    URLConvertible 프로토콜

     

    String과 URLComponents 구조체의 extension으로 url 인스턴스를 반환하는 asURL 함수를 구현하고 있다

     

    위 코드를 통해 URLConvertible 프로토콜은 URL 문자열이나 URLComponents 구조체를 URL 구조체의 인스턴스로 변환하기 위한 프로토콜임을 확인할 수 있다.

     

    또한 앞서 살펴본 asURLRequest 메서드는 URLConvertible을 준수하는 객체(String 또는 URLComponents)를 인자로 받는 것이 필수적이다.

     

    위 두 가지 사실에서 URLConvertible 프로토콜은 결국  URLRequestConvertible의 추상메서드 asURLReqeust가 URLRequest 인스턴스를 생성하기 위해 필요한 프로토콜임을 추론할 수 있다.

     

    그렇다면 실제로도 Alamofire가 그렇게 동작하는 지 소스코드를 통해 확인해 볼 필요가 있다.

     

    실제 요청시 프로그래머가 호출하게 되는 Alamofire의 request 메서드를 살펴보면 이러한 역할 분담을 기반으로 API 요청 프로세스가 구현되어 있음이 잘 드러난다.

     

    먼저 일반적으로 Alamofire를 사용할 때 자주 사용하게 되는 URL 문자열을 직접 request 메서드의 인자로 넘겨주는 경우를 살펴보자.

     

     

     

    URLConvertible 프로토콜을 활용하는 AF.request 메서드

     

    let url = "https://API.example.com/v1/search"
    
    // URL 문자열과 요청 메서드, 파라미터, 헤더를 인자로 받는 경우
    AF.request(url, method: .get, parameters: parameters, headers: headers)
            .responseDecodable(of: decodingType) { response in
                switch response.result {
                case .success(let response):
                    // response 수신 성공시 로직
                case .failure(let error):
                   // error 예외처리
                }
            }

     

     

    위는 서두에도 제시했었던 URL문자열과 HTTP Method, Alamofire.Parameters, Alamofire.Headers 객체를 인자로 받는 request 메서드이다. 

     

    이때 프로그래머가 호출하는 메서드는 Alamofire의 Session 클래스의 멤버로 구현된 request 메서드 중 URLConvertible을 준수하는 객체를 필수 인자로 받는 아래의 메서드이다.

     

     

    Alamofire Session 클래스의 URLConvertible을 인자로 받는 request

     

    내부에서 호출된 URLRequestConvertible을 인자로 받는 request메서드를 통해 DataRequest를 반환한다

     

    RequestConvertible 구조체

     

    URLRequest 인스턴스를 생성하기 위해 Alamofire에서 제공하는 구조체

     

     

    코드상으로 확인할 수 있는 위 request 메서드가 수행하는 내부의 로직은 아래와 같다

    1. URLConvertible을 준수하는 String이나 URLComponent를 인자로 받는 RequestConvertible 구조체의 생성자 호출
    2. URLRequestConvertible을 준수하는 RequestConvertible 인스턴스 생성 및 변수에 할당
    3. RequestConvertible 구조체의 인스턴스를 인자로 받는 request 메서드 호출
    4. DataRequest 클래스의 인스턴스 반환

     

     

    즉, Alamofire가 프로그래머를 대신해서 인자로 받은 URL 문자열을 바탕으로 내부적으로 URLRequestCovertible을 준수하는 구조체를 생성한 후에 API를 요청하는 작업을 수행하고 있는 것이다.

     

    그렇다면 URLRequest를 생성하는 로직을 프로그래머가 직접 구현한 경우에는 어떤 프로세스를 거치게 될까.

     

     

    URLRequestConvertible을 활용하는 request 메서드

     

    static func request<T>(_ object: T.Type,
                               router: APIRouter, // URLRequestConvertible 준수
                               success: @escaping onSuccess<T>,
                               failure: @escaping onFailure) where T:
        Decodable {
            AF.request(router)
                .responseDecodable(of: object) { response in
                    switch response.result {
                    case .success:
                        // response 수신 성공시 로직
                    case .failure(let err):
                        // 에러 예외처리
                    }
                }
        }

     

     

    위 경우에는 URLRequestConvertible를 준수하는 객체를 인자로 받아 Alamofire의 Request 클래스를 상속하며 실제 네트워크 통신을 수행하는 DataRequest 인스턴스를 생성한다.

     

    그 후 DataRequest는 Session의 perform함수에 인자로 전달됨으로써 커스텀 큐에서 비동기로 자신과 부모인 Request클래스가 가진 메서드들을 실행하고 통신의 상태 및 결과를 반영하여 반환된다.

     

    지금까지 살펴보면 결국 일반적으로 사용하는 URL 문자열을 인자로 받는 request메서드를 호출하는 방식 또한 내부적으로URLRequestConvertible을 준수하는 객체를 인자로 받는 request를 호출함으로써 실행됨을 코드상으로 확인할 수 있다.

     

    이는 전술했듯 URLRequest 인스턴스의 생성 과정을 사용자가 직접 구현하느냐 Alamofire가 대신해주느냐의 차이 외엔 완전히 동일한 프로세스를 따르고 있는 것이다.


    그런 점에서 URL 문자열을 인자로 받는 request 메서드를 호출하면서 메서드 오버라이딩을 반복했던 내 프로토콜은 Alamofire의 request 메서드에 대한 이해가 부족했기에 가능한 발상이었다.

     

    그렇다면 URLRequest 인스턴스의 생성과정을 직접 프로그래머가 구현하면 어떤 장점이 있을까.

     

    URLRequestConvertible을 준수하는 아래의 enum을 살펴보자.

     

     

    Naver Search API 요청을 위한 URLRequestConvertible 프로토콜 사용 예

     

    import Foundation
    import Alamofire
    
    
    enum APIRouter: URLRequestConvertible {
        
        case searchShoppings(_ query: String, sort: Sorting)
        // 아래에 정의한 프로퍼티들을 조합하여 URLRequest 인스턴스를 생성해 반환한다
        func asURLRequest() throws -> URLRequest {
            let url = NaverSearchAPI.baseURL.appendingPathComponent(path)
            var urlRequest = URLRequest(url: url)
            
            urlRequest.method = method
            urlRequest.headers = APIRouter.headers
            
            if let parameters = parameters {
                return try encoding.encode(urlRequest, with: parameters)
            }
            
            return urlRequest
        }
        
        var method: HTTPMethod {
            switch self {
            case .searchShoppings(let query, let sort):
                return .get
            }
        }
        
        static let headers: HTTPHeaders = [
            "X-Naver-Client-Id" : NaverSearchAPI.MyAuth.clientID,
            "X-Naver-Client-Secret" : NaverSearchAPI.MyAuth.clientSecret
        ]
        
        private var path: String {
            switch self {
            case .searchShoppings(let query, let sort):
                return "/shop.json"
            }
        }
        
        static var defaultParameters: Parameters = [
            "query" : "",
            "display" : 30,
        ]
        
        private var parameters: Parameters? {
            switch self {
            case .searchShoppings(let query, let sort):
                APIRouter.defaultParameters["query"] = query
                APIRouter.defaultParameters["sort"] = sort.rawValue
                return  APIRouter.defaultParameters
            }
        }
        
        enum Sorting: String, CaseIterable {
            case sim
            case date
            case dsc
            case asc
            
            var buttonTitle: String {
                switch self {
                case .sim:
                    return "정확도"
                case .date:
                    return "날짜순"
                case .dsc:
                    return "가격높은순"
                case .asc:
                    return "가격낮은순"
                }
            }
        }
        
        var encoding: ParameterEncoding {
            switch self {
            case .searchShoppings(let query, let sort):
                return URLEncoding.default
            }
        }
    }

     

     

    위 APIRouter enum의 경우 case에 searchShoppings 라는 named tuple을 사용해 associated value를 할당하고 있다.

     

    간단하게 로직을 설명하면 asURLRequest 함수가 실행되면  HTTPMethod, Header, Parameter, encoding 등의 연산프로퍼티 내부의 switch 문에서 호출시 선택한 case로 분기하여 반환값을 가져와 URLRequest 인스턴스를 목표하는 API 요청양식에 맞게 설정하고 있다.

     

    특징적인 부분은 기존에 프로그래머가 API 요청을 호출하는 부분에서 직접 구현해줘야 했던 API의 요청양식을  enum으로 구현하여 프로그래머의 타이핑이 아닌 enum 내부에 선언된 case를 검색하는 방식으로 ㄴ안전하게 사용할 수 있게 된다는 점이다.멋져

     

     

    필자의 경우 API response 값을 관리하는 model 클래스의 멤버로 메서드를 선언해 사용하였다.

     

     

    그런데 여기서 한 가지 의문이 생길 수 있다.

     

    코드상으로 asURL 메서드와 asURLReqeust 메서드가 호출되는 부분이 없는데 어떻게 해서 Alamofire는 URLRequest 인스턴스를 생성한 후에 API 요청을 보내는 것일까.

     

    이 역시 Alamofire가 request를 수행하는 프로세스를 살펴보면 알 수 있다.

     

    위 APIRouter enum의 내부에 구현된 asURLRequest 메서드는 앞서 request 함수에서 살펴보았던 Session 클래스의 멤버 perform 메서드의 내부에서 아래와 같이 호출된다.

     

     

    URLRequestConvertible을 인자로 받는 Session 클래스의 request 메서드

     

     

     

    Session 클래스의 perform 메서드 

     

    switch 문을 통해 인자로 전달받은 Request 클래스 인스턴스의 타입에 따라 분기하고 있다

     

     

    URLRequestConvertible을 준수하는 객체로부터 생성된 Request의 경우 DataRequest 인스턴스를 perform 메서드의 인자로 넘기기 때문에 아래와 같이 performSetupOperations 메서드가 실행된다.

     

     

    Session 클래스의 performSetupOperations 메서드

     

    Alamofire의 내부에서 프로그래머가 구현한 asURLRequest 메서드를 실행하여 URLRequest 인스턴스를 생성하는 부분

     

     

    그러므로 프로그래머는 아래의 두 가지만 작업해주면 목표하는 URLRequest 인스턴스의 생성을 Alamofire에게 지시할 수 있게 된다. 

     

    1. URLRequestConvertible을 준수하는 구현객체 내부에 필요한 API 요청에 알맞게 asURLRequest 메서드를 구현
    2. Session의 request 메서드를 호출할 때 자신이 작성한 구현객체를 url 파라미터의 인자로 전달

     

     

    또한 일반적인 사용방법처럼 URL 문자열을 인자로 받는 request메서드를 호출한 경우에는 Alamofire에서 RequestConvertible 구조체 내부에 구현해놓은 asURLRequest 메서드를 실행한다.

     

    이때 URLRequest의 생성자를 호출하면 URLConvertible 프로토콜을 구현하는 asURL 메서드의 실행까지 함께 이루어진다.

     

    그러므로 URLRequestConvertible 프로토콜은 내부적으로 URLConvertible 프로토콜까지 포괄한다고 볼 수 있을 것이다.

     

    마지막으로 URLConvertible 프로토콜을 활용한 Alamofire 사용 예제를 살펴보며 본 포스트를 마무리하려 한다.

     

     

     URLRequestConvertible 프로토콜을 준수하는 객체를 인자로 받는 Alamofire request 코드 예시

     

    import Foundation
    import Alamofire
    
    
    class APIClient {
        typealias onSuccess<T> = ((T) -> Void)
        typealias onFailure = ((_ error: Error) -> Void)
        
        static func request<T>(_ object: T.Type,
                               router: APIRouter,
                               success: @escaping onSuccess<T>,
                               failure: @escaping onFailure) where T:
        Decodable {
            AF.request(router)
                .responseDecodable(of: object) { response in
                    switch response.result {
                    case .success:
                        guard let decodedData = response.value else {return}
                        success(decodedData)
                    case .failure(let err):
                        failure(err)
                    }
                }
        }
    }

     

     

    API 요청 사용 및 결과값 처리 코드 예시

     

    func requestSearch(_ query: String, sort: APIRouter.Sorting, 
                       callback: @escaping (response: SearchResponse.self) -> (),
                       errorCallback: @escaping () -> ()) {
         APIClient.request(SearchResponse.self,
                           router: APIRouter.searchShoppings(query, sort: sort),
                           success: {(response: SearchResponse.self) -> () in
                               // response 수신 성공시 로직
                               callback(response)
                           },
                           failure: {(error: Error) -> () in
                              // 에러 예외처리
                              errorCallBack(error)
                           }
         )
     }

     

     

    위의 두 코드와 앞서 제시한 enum의 코드를 살펴보면 Alamofire로 API 서버와 네트워킹하는 로직이 아래와 같이 서로 독립적인 책임을 가진 세 개의 계층으로 구조화된 것을 알 수 있다.

     

    1. API 요청이 필요한 이벤트를 발생시키고 응답값을 처리하는 계층
    2. Alamofire의 request 메서드를 호출하여 네트워크 통신을 수행하는 계층 
    3. URLRequest 인스턴스를 생성하는 계층

     

     

    하지만 기존의 URL 문자열을 request 메서드의 인자로 사용하는 경우 URLRequest 인스턴스를 생성하는 계층의 구현을 Alamofire에 의존할 뿐아니라 API 요청이 필요한 이벤트를 발생시키고 응답값을 처리하는 계층에서 URLRequest에 설정될 값들까지 정의하게 되는 문제가 있었다.


    반면에 현재 작성된 코드는 각 계층의 코드가 각자의 역할만 수행하게 되어 보다 책임의 분리가 확실한 구조로 코드를 작성할 수 있게 된다.

     

    그러므로  자연스럽게 아래와 같이 코드의 유지보수성과 재사용성이 향상되는 이익이 발생한다.

     

     

    1. API 요청을 발생시키는 지점에서는 Alamofire를 import할 필요가 사라지고, 불필요한 인스턴스 반복생성을 차단
    2. API 요청 및 응답값 처리 코드의 가독성 향상 및 휴먼에러 방지
    3. enum에서 각 케이스를 선택하는 것만으로 API 요청을 정의하게 되어 코드 가독성 증대 및 휴먼에러 방지
    4. 사용하는 API들이 추가/삭제 되거나 변경되어도 별도의 로직변경없이 enum만 수정하면 되어 유지보수성 증대 
    5. 새로운 프로젝트에서도 enum만 새롭게 선언하면 되어 코드의 재사용성 증대

     

     

    따라서 처음 Alamofire을 사용하는 것이 아니라면 이렇게 Alamofire에서 제공하는 프로토콜을 활용한 구조화된 코드로 API 요청을 작성해 관리하는 편이 더 이점이 많다는 생각이 든다.

    댓글

All Posts are written by Tunastorm