WWDC

WWDC 2021 - Use async/await with URLSession 정리

Goniii 2025. 9. 5. 20:42

https://developer.apple.com/videos/play/wwdc2021/10095/

 

Use async/await with URLSession - WWDC21 - Videos - Apple Developer

Discover how you can adopt Swift concurrency in URLSession using async/await and AsyncSequence, and how you can apply Swift concurrency...

developer.apple.com

 

 

 

async/await가 URLSession과 어떻게 작동하는지 살펴보자 (0:00)

  • Swift Concurrency는 코드를 직관적이고 선형적으로 만들고, Swift의 네이티브 에러 처리와 잘어울림
  • 네트워킹은 본질적으로 비동기이기 때문에 URLSession에서 async/await이 매우 적합함
  • iOS 15, macOS Monterey부터 URLSession은 새로운 async API를 제공함

반려견을 좋아하는 사람들을 위한 사진 공유 앱을 보자

  • 즐겨찾기 기능
  • 사진을 가져오는 기능

 

Completion Handler를 사용한 코드

  • URLSession을 사용하는 completionHandler 기반의 메서드
  • 해당 코드는 세 가지 실수가 있음

 

이 코드의 제어 흐름(Control Flow)을 보자

  1. URLSession.shared.dataTask를 통해 작업을 만듦
  2. 해당 작업을 재개함
  3. 작업이 완료되면 completion Handler로 이미지 생성 후 흐름을 끝냄

→ 앞뒤로 점프하며 제어 흐름이 왔다갔다 함

 

 

이 코드의 스레딩을 살펴보자

 

이 짧은 코드에도 놀라울 정도로 복잡하다

메서드에는 총 세 가지의 실행 컨텍스트가 있음

  1. 가장 바깥 쪽 계층: fetchPhoto(url: completionHandler:)
    • 해당 메서드를 호출한 호출자의 스레드 또는 큐에서 실행됨
  2. URLSessionTask completionHandler
    • 세션의 대리자 큐에서 실행 (session’s delegate queue)
  3. 최종 completion Handler
    • 메인 스레드에서 실행 (DispatchQueue.main.async)

→ 컴파일러는 에러를 잡아주지 않기 때문에 Data Race 같은 스레딩 문제를 피하기 위해선 개발자가 주의해야 함

 

💡 Session’s Delegate Queue란?

1. URLSession의 실행 구조
- 요청/응답, 인증 처리, 리다이렉트, 데이터 수신 이벤트 등이 발생하면 → delegate에게 알림을 보냄
- 이때 delegate 메서드가 실-행되는 큐(DispatchQueue/OperationQueue)를 delegateQueue라고 부름

2. URLSession은 내부적으로 delegate 패턴을 기반으로 작동함

3. 세션 delegate 큐의 기본값
- URLSession(configuration:delegate:delegateQueue) 초기화 시 지정하지 않으면 직렬 OperationQueue를 사용함
- URLSessionTask 콜백은 main queue가 아닌, delegateQueue에서 실행됨

4. completionHandler와 delegate 큐의 관계
- 네트워크 이벤트는 내부적으로 먼저 delegateQueue에서 처리됨
- 이후 우리가 등록한 completionHandler가 그 delegateQueue에서 실행됨
- dataTask(with:completionHandler:) 같은 메서드 사용 시

→ 따라서, completionHandler는 기본적으로 메인 스레드를 보장하지 않음

 

이제 해당 코드에 있는 세 가지 문제를 알아보자

1. completion handler의 호출이 메인 스레드에 일관되지 않음

→ 올바른 데이터일 때만, 메인 스레드에서 실행을 보장함

 

 

2. Missing early return (return의 부재)

→ completion handler 호출 후 return 하지 않아 이후의 코드가 그대로 실행되어 completion handler를 두 번 호출할 가능성이 존재함

 

 

3. UIImage 생성 실패 가능성

→ data로 UIImage 생성에 실패했을 경우, nil을 리턴하기 때문에 completion handler에 (nil, nil)을 리턴할 가능성 있음

 

async/await 사용 버전

  • 제어 흐름(Control Flow)이 위에서 아래로 선형적
  • 이 메서드는 모두 동일한 Concurrency Context에서 실행되므로 스레딩 문제에 걱정할 필요가 없음
  • URLSession에서 새로운 비동기 메서드를 사용하여 차단 없이 네트워킹 가능
  • throw 키워드를 사용하여 오류 처리 가능하며, UIImage를 nil로 반환할 수 없어 컴파일러가 알려줄 것임

 

새로운 URLSession 메서드를 살펴보자

  • URLSession.data 메서드는 기존과 마찬가지로 URL, URLRequest를 모두 허용함

 

 

데이터를 업로드하거나 파일을 업로드하는 메서드도 제공

→ 기존과 거의 동일하지만, 기본 HTTP 메서드를 제공하지 않으므로 요청 전 설정 필요함

 

 

다운로드 메서드

  • 다운로드 메서드는 응답 본문을 메모리(Data)가 아닌 파일로 저장함
  • 이 메서드는 파일을 자동으로 삭제하지 않으므로 직접 삭제해야 함!!
  • 해당 예제에는 추가 처리를 위해 파일 위치를 이동하는 예제

 

💡 URLSession.download - 개념 잡기

  • 네트워크 응답 본문을 메모리(Data)가 아닌 파일로 저장해서 반환함
  • 큰 파일(영상, 압축, 큰 이미지 등)에 유리
  • 반환 값
let (fileURL, response) = try await URLSession.shared.download(from: someURL)
// fileURL: 임시(temporary) 위치의 로컬 파일 URL
// response: URLResponse (대부분 HTTPURLResponse)
  • fileURL은 임시 디렉터리에 놓은 실제 파일로, 필요에 따라 원하는 경로로 직접 이동(move)하거나 복사(copy)해야 함
  • 파일을 자동 삭제하지 않으므로 직접 삭제해야 함 </aside>

작업 취소

  • URLSession의 async 메서드를 취소하는 방법은 Task.Handle을 사용하는 것
  • 위 예제에는 두 개의 리소스를 하나씩 로드하는 동시성 작업을 만듦
  • Task.Handle을 이용하여 cancel() 메서드 호출하여 작업 취소 가능

→ task.cancel()을 호출하면 Swift Concurrency 취소가 전파되어 내부의 네트워크 요청도 취소됨

 

 

💡 Task.Handle

  • Swift Concurrency에서 Task { … }를 만들면, 이 작업을 제어할 수 있는 객체가 필요함 → 그 역할을 하는 게 Task.Handle
  • Task: 실제 실행하는 비동기 작업
  • Task.Handle: 그 작업을 외부에서 취소(cancel)하거나, 결과(await)하기 위한 핸들
  • Task.Handle의 타입은 제네릭 타입
Task<Success, Failure: Error>
// Success: 작업이 반환하는 값의 타입
// Failure: 작업이 던질 수 있는 에러 타입

 

“Concurrency Task”와 “URLSessionTask”는 다른 개념!!

Concurrency Task

  • Swift 런타임이 관리하는 동시성 단위
  • Task.Handle을 통해 취소, 결과 대기 가능

URLSessionTask

  • URLSession이 관리하는 네트워크 작업 단위(dataTask, uploadTask, downloadTask 등)

 


Response Body 점전적으로 받기

  • 보통 URLSession.data는 전체 응답을 다 받은 뒤에 결과를 리턴함
  • 실시간 API는 끝나지 않고 이어짐 → 전체를 가디리면 앱이 멈춤

→ 이때, URLSession.bytes 메서드 사용

  • URLSession.AsyncBytes 타입을 반환
  • response body를 Byte 단위로 AsyncSequence 처럼 사용

  • Response Header가 수신되면 return하고 Response body를 Byte의 AsyncSequence로 전달

 

예제를 보자

  • 즐겨찾기 개수를 실시간으로 업데이트하는 API
  • EndPoint 확인

 

 

AsyncSequence API를 사용하여 실시간으로 즐겨찾기 개수 업데이트

  • 반환된 바이트 타입은 URLSession.AsyncBytes

 

 

JSON 데이터 읽기

  • bytes.lines: 응답 스트림을 줄 단위 비동시 시퀀스로 변환.
  • for try await: 데이터가 오면 한 줄씩 사용
  • JSON 디코딩 후 UI 반영
  • UI 업데이트 메인 액터에서 이루어져야 하므로 await 구문을 사영하여 비동기 함수인 updateFavoriteCount 호출

 

💡 점진적 업데이트 (Real Time 방식)

  1. 보통의 요청 - 응답 방식
    • 서버가 전체 응답을 다 보낼 때까지 기다렸다가 한 번에 결과를 줌
    • 서버가 “전송 끝”이라는 신호를 보내기 전까진 클라이언트는 데이터를 쓸 수 없음
    • 따라서, 실시간 반영이 불가능하고, 다시 요청해야 최신 데이터를 가져올 수 있음
  2. 스트리밍 방식 (실시간 업데이트)
    • 실시간을 구현하려면 서버가 응답을 끝내지 않고 연결을 열어둔 채,
    • 값이 변할 때마다 새로운 JSON 라인을 계속 밀어주는 구조로 사용
    {"photoId": "123", "favorites": 42}\\n   ← 처음 상태
    {"photoId": "124", "favorites": 17}\\n   ← 다른 사진 업데이트
    {"photoId": "123", "favorites": 43}\\n   ← 좋아요 수 증가!
    
    • 서버 입장에서는 “HTTP 연결 하나 열어둔 채” 실시간 이벤트 푸시
    • 클라이언트는 bytes.lines 줄 단위 데이터를 받자마자 처리 가능

 

 

AsyncSequence

  • AsyncSequence는 많은 변환을 지원하며,
  • FileHandle과 같은 다른 시스템 프레임워크 API와 사용 가능함

https://developer.apple.com/videos/play/wwdc2021/10058/

 

Meet AsyncSequence - WWDC21 - Videos - Apple Developer

Iterating over a sequence of values over time is now as easy as writing a “for” loop. Find out how the new AsyncSequence protocol enables...

developer.apple.com

 


 

Task-Specific Delegate

  • URLSession은 인증, 네트워크 메트릭 등과 같은 이벤트에 대한 콜백을 제공하는 Delegate 중심으로 설계됨
  • 즉, 기존에는 URLSessionDelegate 등을 구현해서 콜백 메서드로 이벤트를 받는 방식이었음

 

async/await API에는 task가 드러나지 않음

→ async 메서드는 URLSessionTask 객체를 직접 반환하지 않음

즉, 예전처럼 task.delegate를 붙여서 이벤트 처리하기가 불가능해짐

그렇다면 특정 task에만 적용되는 인증 처리 같은 건 어떻게 할까?

 

Task-specific delegate

  • 이제 각 async 메서드 호출 시 delegate 파라미터를 넘길 수 있음
  • 특정 작업에만 delegate 지정 가능
  • 이 delegate 객체는 그 task가 완료되거나 실패할 때까지 강하게 참조되어 유지됨
  • 세션 자체가 아닌 개별 task 이벤트 처리에 적합
  • 단, background URLSession은 지원하지 않음
  • 예시) 인증 필요 시 로그인 창 호출

 

 

 

동일한 기능을 활용할 수 있또록 Objective-C의 NSURLSessionTask의 Delegate 속성을 도입함

 

 

 

💡 어떤 상황에서 유용한가?

예시: Dogs 앱

  • 사진을 가져오는 건 인증 필요 없음 → 일반 async 호출
  • “사진 즐겨찾기” 인증 필요 → Unauthorized 발생 가능
  • 이때, 인증이 필요한 task에만 AuthenticationDelegate를 넣어주면 됨

즉, 모든 task에 공통으로 적용할 필요 없이, 특정 task에만 딜리게이트 로직 적용 가능

 

 

  • AuthenticationDelegate는 URLSessionTaskDelegate 프로토콜을 준수하며,
  • 이니셜라이저에 signInController 인스턴스를 허용함
  • signInController 클래스에는 사용자에게 자격증명을 묻는 메시지를 표시하는 몇 가지 함수가 이미 포함됨

 

URLSession didReceive 메서드 구현

class AuthenticationDelegate: NSObject, URLSessionTaskDelegate {
    private let signInController: SignInController
    
    init(signInController: SignInController) {
        self.signInController = signInController
    }
    
    func urlSession(_ session: URLSession,
                    task: URLSessionTask,
                    didReceive challenge: URLAuthenticationChallenge) async
    -> (URLSession.AuthChallengeDisposition, URLCredential?) {
        if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPBasic {
            do {
                let (username, password) = try await signInController.promptForCredential()
                return (.useCredential,
                        URLCredential(user: username, password: password, persistence: .forSession))
            } catch {
                return (.cancelAuthenticationChallenge, nil)
            }
        } else {
            return (.performDefaultHandling, nil)
        }
    }
}
  • 사용자에게 자격 증명을 묻는 메시지를 표시하여 HTTP 인증에 응답하도록 선택할 수 있음
  • 에러 처리 됨

 

💡 task-specific delegate 정리

  • 기존 세션 델리게이트: 세션 전체에 공통 적용 (모든 task 이벤트 처리)
  • task-specific delegate: 개별 task에만 적용, async/await 방식에도 사용 가능
  • 제한: background 세션 </aside>

결론

728x90