221123 TIL [async/await를 이용한 비동기 통신]

Doogie·2022년 11월 23일
0

[ 기존 비동기 통신(dataTask와 같은)의 문제점 ]

  • 비동기 통신 과정에서 한 번의 통신으로 여러개의 정보를 가져오는 경우 모든 과정에 대한 에러처리가 힘듬
    • 가능은 하나 방대한 양의 코드가 추가됨
    • 컴파일러가 에러에 대한 부분을 언급해주지 않기에 만약 개발자가 적절한 에러처리를 하지 않고 넘어갔고 그 부분에서 에러가 발생했다면 어플이 죽게되고 어디서 문제가 발생했는지 파악이 힘듬
    • 많이들 겪는 콜백 지옥이라던가... 그런 부분도 문제
  • 구문 자체를 읽거나 이해하는데 어려움이 있음

→ 이런 이유로 인해 RxSwift등을 통해 반환값으로 전달해 사용하는 방법으로 사용 할 수 있었으나 라이브러리를 사용하지 않는다면 여전히 불편함을 겪었었는데 이 기능을 애플에서 async/await 라는 기능을 제공하기 시작

(기존에는 iOS 15 이상에서만 제공에 현업에서 적용하기 힘든 부분이 있었으나 xcode 13.2 버전부터 iOS 13 부터 제공함)

[ 비교 ]

일단 '이걸 왜 써야하는데?' 라는 생각이 든다면 코드부터 비교해보자

아래 작성하는 코드는 구글 북스 api를 통해 검색된 책의 정보를 가져오는 프로젝트에서 검색된 책의 정보들을 비동기적으로 가져오는 코드이다

- 기존 코드(dataTask를 이용한)

    func request(api: APIable, completion: @escaping (Result<Data, APIError>) -> Void) {
        guard let url = makeURL(api: api) else {
            completion(.failure(APIError.urlError))
            return
        }
        
        var request = URLRequest(url: url)
        request.httpMethod = api.method.string
        
        let dataTask = session.customDataTask(request: request) { result in
            DispatchQueue.main.async {
                guard result.error == nil else {
                    completion(.failure(APIError.transportError))
                    return
                }
                
                guard let response = result.response as? HTTPURLResponse, (200...299).contains(response.statusCode) else {
                    completion(.failure(APIError.responseError))
                    return
                }

                guard let data = result.data else {
                    completion(.failure(APIError.dataError))
                    return
                }
                
                completion(.success(data))
            }
        }
        dataTask.resume()
    }

뭐 사실 이렇게 이용 할 때만 하더라도 나 또한 '굳이 async/await를 사용해야 하나?' 하는 생각을 갖고 있었는데 아래 async/await를 적용한 코드를 보자

- async/await 적용

    func newRequest(api: APIable) async throws -> Data {
        guard let url = makeURL(api: api) else {
            throw APIError.urlError
        }
        
        var request = URLRequest(url: url)
        request.httpMethod = api.method.string
        
        let result: (data: Data, response: URLResponse) = try await URLSession.shared.data(for: request)
        
        guard let response = result.response as? HTTPURLResponse, (200...299).contains(response.statusCode) else {
            throw APIError.responseError
        }
        
        return result.data
    }

에러처리가 어쩌고 어플이 죽고 자시고를 떠나서 그냥 코드 양만 봐도 확연하게 줄어든 모습을 확인할 수 있다.

그럼 호출부는 어떨까?

- 기존 코드

    private func getSearchInfo() {
        startLoading.accept(())
        let api = BookAPIModel(bookTitle: searchText, startIndex: startIndex, maxResult: maxResult, method: .get)
        networkManager.request(api: api) { [weak self] result in
            switch result {
            case .success(let data):
                do {
                    let searchResult = try self?.dataDecoder.parse(data: data, resultType: SearchResult.self)
                    
                    guard let totalItems = searchResult?.totalItems, totalItems != 0 else {
                        self?.showAlert.accept("검색 결과가 없습니다")
                        self?.totalItems.accept(0)
                        self?.stopLoading.accept(())
                        return
                    }
    
                    self?.totalItems.accept(totalItems)
                    
                    let oldItems = self?.items.value ?? []
                    let newItems = oldItems + (searchResult?.items ?? [])
                    self?.items.accept(newItems)
                } catch let error{
                    self?.showAlert.accept(error.errorMessage)
                }
            case .failure(let error):
                self?.showAlert.accept(error.errorMessage)
            }
            self?.stopLoading.accept(())
        }
        asyncTest()
    }

- 수정된 코드

func asyncTest() {
        startLoading.accept(())
        let api = BookAPIModel(bookTitle: searchText, startIndex: startIndex, maxResult: maxResult, method: .get)
        
        Task {
            do {
                let data = try await networkManager.newRequest(api: api)
                await MainActor.run {
                    guard let searchResult = try? dataDecoder.parse(data: data, resultType: SearchResult.self) else {
                        print("디코드 에러")
                        return
                    }
                    
                    guard let totalItems = searchResult.totalItems, totalItems != 0 else {
                        showAlert.accept("검색 결과가 없습니다")
                        totalItems.accept(0)
                        stopLoading.accept(())
                        return
                    }
                    
                    self.totalItems.accept(totalItems)
                    
                    let oldItems = items.value
                    let newItems = oldItems + (searchResult.items ?? [])
                    items.accept(newItems)
                }
                
            } catch let error {
                await MainActor.run {
                    showAlert.accept(error.errorMessage)
                }
            }
            await MainActor.run {
                stopLoading.accept(())
            }
        }
    }

뭐... 사실 호출부에서는 코드 양으로는 그렇게 크게 줄어들지는 않았고 길이로 따지면 오히려 늘어났을 수 있는데 그건 아래와 같은 차이점 때문이다


- 굳이 뽑아보는 단점

기존 dataTask를 이용하는 방식에 비해 단점이 있다면 completionHandler 구문 작성시 UI 업데이트가 일어날 예정인 곳에서 미리 main thread에서 작동 하도록 설정이 가능했는데 async/await는 그게 어렵다는 것이다

근데 이건 그리 큰 문제는 아니다. 오히려 통신값을 보내서 계산하는 과정까지도 메인 큐에서 실행 하는 일이 발생 할 수 있게되니 딱! UI업데이트 할 때만 메인큐에서 실행할 수 있게 하는 동작시간으로는 더 나아질 수 있는 처리이다(사실 사람이 느낄 수 없는 차이겠지만...)


위 이유 때문에 UI업데이트 때문에 메인스레드에서 작동되어야 하는 코드가 추가돼서 길이가 늘어난거지 사실상 길이 자체는 차이가 없고 기존에 사용하던 DispatchQueue.main.async 키워드가 아닌 await MainActor run 키워드를 쓰면서 생기는 장점이 더 크게 다가왔다 (이 내용은 이 포스팅 에서 확인 가능)

마무리

그런데 async/await를 사용하게 된다면 네트워크 없이 테스트 하는 방법에 대해서는 더 고민을 해봐야 할 것 같다...

아무튼 async/await에 대해 처음 알았을 때는 iOS 15 버전에서만 사용이 가능하다고 해서 지금 나랑은 먼 얘기일 줄 알았는데 13 버전으로 낮춰진 시점에서 안 쓸 이유가 없다고 생각이 드는 그런 좋은... 그런 것이다! 라고 생각한다

profile
끊임없이 문을 여는 개발자

0개의 댓글