Alamofire 적용하고 가독성 높이기

zoe·2023년 9월 18일
0

Alamofire로 네트워크 레이어 개선하기

오늘은 Alamofire에 대해서 알아보려고 한다. 사실 야곰아카데미에서는 오픈소스, 라이브러리를 사용하기보단 원리를 이해하고 기초를 알길 권장하기 때문에 한번도 사용해본적이 없었다. 근데 Alamofire가 채용공고에 자주 등장하기도 하고 어떤라이브러리인지 궁금해서 이번기회에 Alamofire에 대해 알아보고, HighwayInfo 프로젝트에 적용해보고자 한다.

개요

Alamofire는 애플의 서버 통신 API인 URLSession을 기반으로 더 편리한 네트워킹을 도와주는 라이브러리이다.

  • 복잡한 네트워킹 작업을 은닉하고 주요 로직에 집중할 수 있도록 해주어서 많은 기업에서 사용하고 있는것같다.
  • 또, 코드가 더 간단해지고 여러 기능을 직접 구축하지 않아도 된다는 것도 장점이다.

구현 목표

  • Request Routing을 활용해 Moya를 사용하지 않고 구현해보기
  • 중복되는 코드 최소화
메서드 1메서드 2

이렇게 파라미터만 다른 중복되는 두 메서드를 리팩토링 해보고자 합니다.

Alamofire 코드에 적용 하기

우선 가장 먼저 만들어볼것은 서버 통신 규약을 정의하는 Router 프로토콜과 해당 프로토콜을 따르면서 실제 서버 통신을 담당하는 RouterManager를 만들어보겠습니다.

1. Router

Router는 네트워크 통신에 필수요소인 URL과 path, HTTP 통신 방법, 헤더, 그리고 데이터 요청 업무인 task 통 5가지를 가지도록 선언해주었습니다.

import Alamofire

protocol Router {    
    var baseURL: URL { get }
    var path: String { get }
    var method: HTTPMethod { get }
    var headers: [String: String]? { get }
    var task: Task { get }
}

enum Task {
    case requestPlain
    case requestJSONEncodable(Encodable)
    case requestParameters(parameters: [String: Any])
}

그 다음으로 해당 프로토콜을 채택해 실제 통신을 구현하는 RouterManager를 만들어보겠습니다.

2. Router Manager

RouterManager는 Session과 Interceptor를 가집니다.
request 메서드를 호출하면 통신이 이루어지고
메서드의 실구현은 extension으로 빼서 가독성을 높여보았습니다.

에러에 대해서도 커스텀한 에러 프로토콜을 만들고(RouterManagerError) 채택해 서비스에 맞는 에러 형태를 띠도록 구현해보았습니다.

struct RouterManager<T: Router> {
    private let alamofireSession: Session
    private let interceptor: Interceptor?
    
    init(alamofireSession: Session = .default, interceptor: Interceptor? = nil) {
        self.alamofireSession = alamofireSession
        self.interceptor = interceptor
    }
    
    func request(router: T) -> Single<Data> {
        return sendRequest(router: router)
    }
}

sendRequest 메서드를 보면

extension RouterManager {
    func sendRequest(router: T) -> Single<Data> {
        return Single.create(subscribe: { single in
            let dataRequest: DataRequest = makeDataRequest(router: router)
            
            dataRequest
                                         
             // response.result로 응답의 결과를 알 수 있음
                .responseData { response in
                               
                   // switch를 사용해서 결과에 따른 클로져를 작성
                    switch response.result {
                    case .success:
                        guard let statusCode = response.response?.statusCode else { return }
                        let isSuccessful = (200..<300).contains(statusCode)
                        
                        if isSuccessful {
                            guard let data = response.data else { return }
                            single(.success(data))
                        } else {
                            let error = RouterManagerError(code: .isNotSuccessful, response: response)
                            single(.failure(error))
                        }
                        
                    // 요청이 실패하는 경우 .failure과 error를 전달
                    case .failure(let underlyingError):
                        let error = RouterManagerError(
                            code: .failedRequest,
                            underlying: underlyingError,
                            response: response
                        )
                        single(.failure(error))
                    }
                }
            return Disposables.create()
        })
    }
}

3. API 통신을 하기 위한 Request 구성하기

import Alamofire

enum AccidentAPI {
    // api 목록
    case getAccidents
}

extension AccidentAPI: Router {
    // base url
    var baseURL: URL {
        return URL(string: "http://openapigits.gg.go.kr/api/rest/")!
    }
    
    // base url 뒤에 붙는 path
    var path: String {
        switch self {
        case .getAccidents:
            return "getIncidentInfo?"
        }
    }
    
    // API 요청방식
    var method: HTTPMethod {
        switch self {
        case .getAccidents:
            return .get
        }
    }
    
    // API 요청 헤더
    var headers: [String: String]? {
        return nil
    }
    
    // 인코딩 방식
    // 파라미터로 보낼야할 것이 있으면 .requestParameters
    // 바디에 담아서 보내야할 것이 있으면 .requestJSONEncodable
    var task: Task {
        switch self {
        case .getAccidents:
            return .requestParameters(
                parameters: ["serviceKey": Bundle.main.accidentApiKey]
            )
        }
    }
}

4. API 통신 Service 객체

실제 API통신을 수행하는 Service 객체를 만들어보자

import Foundation
import RxSwift

struct AccidentService {
    let fetchAccidents: () -> Observable<[AccidentDTO]>
    
    init(fetchAccidents: @escaping () -> Observable<[AccidentDTO]>) {
        self.fetchAccidents = fetchAccidents
    }
}

extension AccidentService {
    static let live = Self(
        fetchAccidents: {
            return RouterManager<AccidentAPI>
                .init()
                .request(router: .getAccidents)
                .map({ data in
                    let parser = AccidentParser(data: data)
                    let decoded = parser.parseXML()
                    return decoded
                })
                .asObservable()
        }
    )
}
  • 여기에서 하는 일은 API를 연결시키고 - 디코딩한 후 - map으로 원하는 데이터로 변환 하게된다.
  • 해당 서비스를 통해 연결시켜줄 API 종류를 프로퍼티로 가진다.

5. 실제 코드에서 호출

  • Service에서 API 통신 후 받은 데이터들 중 뷰에 필요한 데이터를 Repository에서 호출해주면 된다.
  • 그 후 UseCase에서 Repository에 원하는 데이터를 요청하는 방식으로 구현해보았다.
final class DefaultAccidentRepository: AccidentRepository {
    private let service: AccidentService
    
    init(service: AccidentService) {
        self.service = service
    }
    
    func fetchAllAccidents() -> Observable<[AccidentDTO]> {
        service.fetchAccidents()
    }
}

결론

Alamofire를 적용하면서 이렇게 중복되는 메서드를 리팩토링해보았습니다. RouterManager를 보면 기존에 두번 반복되던 코드가 사라진걸 확인해볼 수 있어요.

소감

  • 파라미터만 다른 두 메서드를 리팩토링하여 하나의 메서드로 만들 수 있었습니다.
  • 복잡한 네트워킹 작업이 은닉화되고 코드 가독성이 향상된것같습니다.
  • 네트워크 작업을 처리할 때 생산성이 향상되었습니다.

앱에서 수많은 API 요청이 발생하는데, Request Router를 통해 모든 요청을 생성할 수 있고, 이를 통해 하나의 파일에서 일관적으로 관리할 수 있었어요.
기존에 여기저기 흩어져 있는 통신관련 파일들을 하나로 볼 수 있어서 가독성이 높아졌고, 유지보수하기도 수월해진것같아요.

profile
개발하면서 마주친 문제들을 정리하는 공간입니다.

0개의 댓글