URLSession × async await で通信処理を書いてみる

前回、Combineでエラーハンドリングとかも含めた比較的丁寧な通信処理を書いたのですが、
`async await`について今更ながら勉強したし、比較も兼ねて、`async await`バージョンも書いてみました。

前回のCombine版はこちら！

https://qiita.com/kamomeKUN/items/fc6f1ed512f90d5df47c

Request/Responseの共通化に関する説明はこちらの記事にしか載せてないので、その部分を知りたい方はぜひこちらの記事をご覧いただけると幸いです。

## async awaitについて
* Swift5.5から使えるようになった新しい技術
* `async` `await`キーワードを使うことで、非同期処理についてネストが深くならないような書き方ができる
* `completion`を使う方法だと複雑な処理になったときに、`completion`忘れ、コードを追うのが大変になるが、`async await`はその心配がない
* コンパイラーによるコードチェックが手厚い
などのメリットがあります。

こちらの解説サイトが個人的にわかりやすく、大変参考になりました。

https://blog.personal-factory.com/2022/01/23/how-to-use-async-await-since-swift5_5/

## 完成コード
```swift
public protocol APIClientProtocol: AnyObject {
    func send<T: RequestProtocol>(_ request: T) async throws -> T.Response
}

public final class APIClient: APIClientProtocol {
    private init() {}
    public static let shared = APIClient()
    
    public func send<T: RequestProtocol>(_ request: T) async throws -> T.Response {
        guard let url = URL(string: request.baseURL)?.appendingPathComponent(request.path) else {
            throw APIError.failedToCreateURL
        }
        
        guard var components = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
            throw APIError.failedToCreateComponents(url)
        }
        
        components.queryItems = request.parameters?.compactMap {
            .init(name: $0.key, value: "\($0.value)")
        }
        
        guard var urlRequest = components.url.map({ URLRequest(url: $0) }) else {
            throw APIError.failedToCreateURL
        }
        
        urlRequest.httpMethod = request.method.rawValue
        
        urlRequest.allHTTPHeaderFields = [
            "Content-Type": "application/json"
        ]
        
        do {
            let (data, urlResponse) = try await URLSession.shared.data(for: urlRequest)
            // responseのチェック
            guard let urlResponse = urlResponse as? HTTPURLResponse else {
                throw APIError.noResponse
            }
            
            // HTTPステータスコードのチェック
            guard 200 ..< 300 ~= urlResponse.statusCode else {
                throw APIError.unacceptableStatusCode(
                    urlResponse.statusCode
                )
            }
            
            let decodedData = try JSONDecoder().decode(T.Response.self, from: data)
            // Responseデータのstatus項目が正常値であることをチェック
            guard decodedData.status == "ok" else {
                throw APIError.responseStatusError
            }
            return decodedData
        } catch {
            if let error = error as? DecodingError {
                // Decodeエラーのハンドリング
                throw APIError.parserError(error.localizedDescription)
            } else if let error = error as? APIError {
                // 上流のPublisherでエラーが発生していればここで返す
                throw error
            } else {
                throw APIError.unknown(error.localizedDescription)
            }
        }
    }
}
```
今回はこちらのAPIに対してリクエストを行う想定で書いています。

https://newsapi.org

## どっちも書いてみた感想
### エラーハンドリングの面では個人的にはCombineのが使いやすかった
`async await`については、エラーを`throw`させることで発生させるのですが、今回の自分のケースのように、`APIError`などの自分自身で定義したエラーを返す場合に困り事がありました。
`throw`で返した呼び出し元のメソッドでは、`Error`型として扱われるため、呼び出し元のメソッドでCombineの時は不要だったエラーハンドリングを書かないといけなくなりました。

さらにいうと、CleanArchitectureなどで、
* DataLayerのエラーを`APIError`
* DomainLayerのエラーを`APPError`
のような形で定義している場合、throwで返すたびに、カスタムエラー型が`Error`型にダウンキャストされてしまい、その度にエラーを使うところでキャストするというのは少し面倒になる気がします。

### async awaitでは、AnyCancellableで非同期処理を保持したり、[weak self]を書かなくていいことがメリット
`Combine`を使った場合のデメリットにもなるのですが、非同期処理を呼び出す場合は以下のようなことをしないといけません。
* `private var cancellables = Set<AnyCancellable>()`を定義する
* 非同期処理のResponseの処理などのところが`[weak self]`地獄にならなそう
* `.receive(on: DispatchQueue.main)`などの書き忘れでクラッシュなんていう事故にならなそう

これらはCombineに限った話ではなく、「クロージャによる非同期処理」「RxSwiftでの非同期処理」にも共通する問題かもしれませんが、非同期処理を実行するときは、これらに注意して開発したり、これらを忘れるだけで思わぬバグに遭遇したりしますね。

今回の`async await`版は、間違った書き方をするとコンパイルエラーになりますし。
そして、非同期をクロージャで書かなくていいので、今まで必要だった、処理とは直接関係のないコードを書かなくていいのは大きなメリットだなと思いました。

### メソッドチェーンが好きか嫌いかで好みが分かれそう
`Combine`で実装すると、`.flatMap` `.tryMap`などでメソッドチェーンして処理を書いていくことになります。
今回の`async await`のケースでは、`do catch`によって、ハンドリングしたり、decodeされた値をプロパティとして保持したりするので、書き方の好みとかも影響するのかなと思いました。

（慣れているからかもしれませんが）自分はメソッドチェーンでゴリゴリ書かれている方が見やすいなと思ってしまいます。

## おわりに
今回は、`async await`を使ったAPI通信処理について書かせていただきました。  
Combineと比較してみましたが、お互いに長所・短所がありそうですね。個人的にはどちらも理解して、状況によって使い分けていきたいなと思います。

今回は該当ケースではないので、本題では触れなかったのですが、直列や並列で大量のメソッドを呼び出すとなると、ネストが深くなるので、その場合は、`async await`が真価を発揮しそうだなと思います👍

ではまた👋