-
Chapter 1: Why Modern Swift Concurrency?Raywenderlich/Modern Concurrency in Swift 2022. 12. 15. 18:08
Version
Swift 5.8, iOS 16, Xcode 14
Apple이 마지막으로 비동기(asynchronous) 프레임워크(framework)에 대한 큰 작업(big deal)을 한 것은 2009년 Mac OS X Snow Leopard와 함께 GCD(Grand Central Dispatch)가 출시(came out)되었을 때였다.
GCD는 2014년에 출시(launch)된 Swift가 처음부터 동시성(concurrency)과 비동기성(asynchrony)을 지원하도록 도움(support for)이 되었지만, 그 지원(support)은 Swift(native)가 아니라 Objective-C의 요구 사항(needs)과 기능(abilities)을 중심으로 설계(designed)되었다. Swift는 해당 언어를 위해 특별히 설계(designed specifically)된 자체 메커니즘(mechanism)을 갖출 때까지(until), 동시성(concurrency)을 "차용(borrowed)"했을 뿐이었다.
이는 비동기(asynchronous), 동시(concurrent) 코드 작성을 위한 새로운(new) Swift 모델(native model)을 도입한 Swift 5.5에서 모든 것이 바뀌었다.
새로운 동시성 모델(concurrency model)은 다음을 포함한, Swift로 안전(safe)하고 성능 좋은(performant) 프로그램(programs)을 작성하는 데 필요한 모든 것을 제공한다:
- 구조화된 방식(structured way)으로 비동기(asynchronous) 작업(operations)을 실행(running)하기 위한 새로운(new) Swift(native) 구문(syntax).
- 비동기(asynchronous) 및 동시(concurrent) 코드(code)를 설계(design)를 위한 표준(standard) API 번들(bundle).
- 모든 상위 수준(high-level) 변경 사항(changes)이 운영 체제(operating system)에 직접 통합(integrate directly)되도록 하는
libdispatch
프레임워크(framework)의 하위 수준(low-level) 변경(changes). - 안전한(safe) 동시(concurrent) 코드(code) 생성(creating)을 위한 새로운 수준(level)의 컴파일러(compiler) 지원(support for).
Swift 5.5는 이러한 기능(features)을 지원(support)하기 위해 새로운 언어 구문(syntax)과 API를 도입(introduces)했다. 앱(apps)에서 최신(recent) Swift 버전(version)을 사용하는 것 외에도(besides using), 특정(certain) 플랫폼(platform) 버전(versions)을 대상(target)으로 지정해야 한다:
- Xcode 13.2 이상을 사용하는 경우, iOS 13 및 macOS 10.15(네이티브 앱의 경우) 이상을 대상(target)으로 하여 새로운 동시성(concurrency) 런타임(runtime)을 앱(app)과 함께 번들(bundle)로 제공한다.
- Xcode 13.0 또는 Xcode 13.1을 사용하는 경우, 최소한(minimum) iOS 15 또는 macOS 12를 대상(target)으로 해야 한다. 최신 기능(features)을 사용하려면 Xcode 14 이상으로 업데이트(updating)하는 것이 좋다.
이 책의 첫 번째 장(chapter)에서는 Swift의 새로운 동시성(concurrency) 지원(support)을 살펴보고(review), 이전(older) API와 비교(compared)하여 어떤 차이가 있는지 확인한다. 이후(later) 이 장(chapter)의 실습 부분(practical part)에서 실제 프로젝트(real-life project)로
async
/await
구문(syntax)을 사용해 보고, 몇 가지 멋진(cool) 비동기(asynchronous) 오류 처리(error-handling)를 추가(adding)하여 작업하게 된다.Understanding Asynchronous and Concurrent Code
대부분(most)의 코드는 코드 편집기(code editor)에서 작성(written)한 것과 동일한 방식(same way)으로 실행(runs)된다. 위에서 아래로(from top to bottom), 함수(function) 시작(beginning) 부분에서 시작(starting at)하여 한 줄씩(line-by-line) 끝까지 진행된다.
따라서 특정(given) 코드(code) 행(line)이 언제 실행(executes)되는지 쉽게 파악할 수 있다. 단순히(simply) 이전 행(line)을 따라가기(follows)만 하면 된다. 함수(function) 호출(calls)도 마찬가지이다: 코드(code)가 동기식(synchronously)으로 실행(runs)되면 순차적으로(sequentially) 실행(execution)된다.
동기(synchronous) 컨텍스트(context)에서 코드(code)는 단일(single) CPU 코어(core)의 하나의 실행 스레드(execution thread)에서 실행(runs)된다. 동기(synchronous) 함수(functions)를 단일 차선(single-lane) 도로(road)에서 각각(each) 앞차의 뒤를 따라 주행(driving)하는 자동차로 생각해(imagine) 볼 수 있다. 응급상황(on duty)인 구급차(ambulance)처럼 한 차량(vehicle)의 우선 순위(higher priority)가 더 높더라도, 나머지(rest of) 차량(traffic)을 "넘어서(jump over)" 더 빨리 주행(drive)할 수는 없다.
반면에(on the other hand) iOS 앱(apps)과 Cocoa 기반(based) macOS 앱(apps)은 본질적으로(inherently) 비동기(asynchronous)이다.
비동기(asynchronous) 실행(execution)으로 프로그램(program)의 여러 부분(pieces)이 하나의 스레드(thread)에서 임의의 순서(in any order)로 실행(run)될 수 있으며, 때로는(sometimes) 사용자 입력(user input), 네트워크 연결(network connections) 등과 같은 다양한 이벤트(events)에 따라 여러 스레드(on multiple threads)에서 동시에(at the same time) 실행(run)될 수도 있다.
비동기(asynchronous) 컨텍스트(context)에서, 특히(especially) 여러(several) 비동기(asynchronous) 함수(functions)가 동일한 스레드(thread)를 사용해야 하는 경우, 함수(functions)가 실행되는 정확한(exact) 순서(order)를 알기(tell) 어렵다. 정지 신호(stoplights)가 있고 양보(yield)해야 하는 곳이 있는 도로(traffic)에서 운전(driving)하는 것처럼, 함수(functions)는 때때로(sometimes) 계속할 차례(turn to continue)가 될 때까지(until) 기다리거나(wait), 진행(proceed)하기 위한 녹색 신호(green light)가 들어올 때까지(until) 멈춰야(stop) 할 수도 있다.
비동기(asynchronous) 호출(call)의 한 가지 예(example)는 네트워크(network) 요청(request)을 하고, 웹 서버(web server)가 응답(responds)할 때 실행(run)할 완료 클로저(completion closure)를 제공(providing)하는 것이다. 완료 콜백(completion callback)을 실행(run)하기 위해 기다리는(waiting) 동안(while), 앱(app)은 다른 작업(chores)을 수행하는 데 그 시간을 사용(uses)한다.
프로그램(program)의 일부를 의도적으로(intentionally) 병렬(in parallel) 실행(run)하려면 동시(concurrent) API를 사용한다. 일부(some) API는 고정된 수(a fixed number)의 작업(tasks)을 동시에(at the same time) 실행(executing)하는 것을 지원(support)한다. 그외 다른 API는 동시(concurrent) 그룹(group)을 시작(start)하고 임의의 수(an arbitrary number)의 동시(concurrent) 작업(tasks)을 허용(allow)한다.
이로 인해 수 많은(myriad) 동시성 관련 문제(concurrency-related problems)가 발생(causes)할 수도 있다. 예를 들어(for example), 프로그램(program)의 여러 부분(different parts)이 서로(each)의 실행(execution)을 차단(block)하거나, 두 개 이상의 함수(functions)가 동시에(simultaneously) 동일한 변수(variable)에 접근(access)하여 결과적으로 앱(app)이 충돌(crashing the app)하는 경쟁 상태(data-races)가 발생할(encounter) 수 있다.
그러나(however) 주의해서(with care) 사용하면 동시성(concurrency)은 여러(multiple) CPU 코어(cores)에서 서로 다른 함수(functions)를 동시에(simultaneously) 실행(executing)하여, 프로그램(program)이 더 빠르게(faster) 실행(run)하는 데 도움(help)이 될 수 있다. 이는 조심스러운(careful) 운전자(drivers)가 다중 차선(multi-lane) 고속도로(freeway)에서 훨씬 빠르게(much faster) 이동(move)할 수 있는 것과 같다.
다중 차선(multiple lanes)은 더 빠른(faster) 차량(cars)이 느린(slower) 차량(vehicles)을 우회(go around)하도록 해준다(allow). 더 중요한 것은(even more importantly) 구급차(ambulances)나 소방차(firetrucks)와 같이 우선 순위(high-priority)가 높은 차량(vehicles)을 위한 긴급 차선(emergency lane)을 비워(free) 둘 수 있다는 것이다.
마찬가지로(similarly), 코드(code)를 실행(executing)할 때 우선 순위가 높은(high-priority) 작업(tasks)은 우선 순위가 낮은(lower-priority) 작업(tasks)보다 먼저 대기열(queue)을 "건너 뛸(jump)" 수 있으므로 메인 스레드(main thread)를 차단하지 않고(avoid blocking) UI에 대한 중요한(critical) 업데이트(updates)를 위해 사용 가능한 상태(free)를 유지(keep)할 수 있다.
이에 대한 실제(real) 사용 사례(use-case)는 웹 서버(web server)에서 여러 이미지(group of images)를 동시에(at the same time) 다운로드(download)하여 썸네일(thumbnail) 크기(size)로 축소(scale)한 후 캐시(cache)에 저장(store)해야 하는 사진 검색 앱(photo browsing app)이 있다.
비동기(asynchrony)와 동시성(concurrency) 모두 훌륭하게 작동하여 "Swift에 새로운 동시성(concurrency) 모델(model)이 필요한 이유가 무엇인가?"라고 자문(ask yourself)할 수도 있다. 이는 타당한(fair) 질문(question)이며 다음(next) 섹션(section)에서 Swift 5.5 이전(pre)의 동시성(concurrency) 설정(options)을 검토(review)하고 새로운
async
/await
모델(model)의 차이점에 대해 알아본다.Reviewing the Existing Concurrency Options
Swift 5.5 이전(pre)에는 GCD를 사용하여 스레드(threads)에 대한 추상화(abstraction)인 디스패치 큐(dispatch queues)로 비동기(asynchronous) 코드(code)를 실행(run)했다. 또한
Operation
,Thread
또는 C 기반(based)pthread
라이브러리(library)를 직접(directly) 사용하여 '원형에 더 가까운(closer to the metal)' 이전(older) API를 사용했다.Note: 새로운 Swift 동시성(concurrency) API가 GCD를 대체(replaced)했기 때문에, 이 책에서는 GCD를 사용(use)하지 않는다. 궁금(curious)하다면, Apple의 GCD 문서(documentation)를 읽어본다: Dispatch documentation
이 API는 모두 동일한 기반(foundation)을 사용한다: 특정 프로그래밍 언어(programming language)에 의존(rely on)하지 않는 표준(standardized) 실행(execution) 모델(model)인 POSIX 스레드(threads)다. 각(each) 실행(execution) 흐름(flow)은 스레드(thread)이며, 앞서(above) 설명한(presented) 다차선(multi-lane) 자동차 예제(example)와 유사하게(similarly to) 여러(multiple) 스레드(threads)가 동시에(at the same time) 중첩(overlap)되어 실행(run)될 수 있다.
Operation
과Thread
같은 스레드(thread) 래퍼(wrappers)는 실행(execution)을 수동 관리(manually manage)해야 한다. 즉(In other words), 사용자에게 스레드(threads)를 생성(creating) 및 삭제(destroying)하고 동시(concurrent) 작업(execution)의 실행 순서(order) 결정(deciding)하며 스레드(threads) 간 공유(shared) 데이터를 동기화(synchronizing)해야 할 책임(responsible for)이 있다. 이는 오류가 발생하기 쉽고(error-prone) 지루한(tedious) 작업이다.GCD의 대기열 기반 모델(queue-based model)은 잘 작동했다(worked well). 그러나(however), 다음과 같은 문제(issues)가 발생(cause)하기 쉬웠다(prone):
- 스레드 폭증(Thread explosion): 너무 많은 동시(concurrent) 스레드(threads)를 생성하면, 활성(active) 스레드(threads)를 지속적으로(constantly) 전환(switching)해야 한다. 이는 궁극적으로(ultimately) 앱(app)의 속도가 빨라지기는커녕 오히려 느려진다(slows down).
- 우선 순위 반전(Priority inversion): 우선 순위가 낮은(low-priority) 작업(tasks)이 임의(arbitrary)로 동일한 대기열(queue)에서 대기 중인 높은 우선 순위(high-priority) 작업(tasks)의 실행(execution)을 차단(block)한다.
- 실행 계층 구조 부족(Lack of execution hierarchy): 비동기(asynchronous) 코드(code) 블록(blocks)에는 실행 계층 구조(execution hierarchy)가 없었는데, 이는 각(each) 작업(task)이 독립적으로(independently) 관리되었음(managed)을 의미한다. 이로 인해 실행 중(running)인 작업(tasks)을 취소(cancel)하거나 접근(access)하기가 어려웠다. 또한 호출자(caller)에게 작업(tasks)의 결과(result)를 반환하는 것을 복잡(complicated)하게 만들었다.
이러한 단점(shortcomings)을 해결(address)하기 위해, Swift 5.5는 완전히 새로운(brand-new) 동시성(concurrency) 모델(model)을 도입(introduced)했다. 다음으로(next), Swift의 최신(modern) 동시성(concurrency)에 대해 살펴본다.
Introducing the Modern Swift Concurrency Model
새로운(new) 동시성(concurrency) 모델(model)은 언어 구문(language syntax), Swift 런타임(runtime) 및 Xcode와 긴밀하게(tightly) 통합되어(integrated with) 있다. 새 모델은 개발자(developer)를 위해 스레드 개념(notion of threads)을 추상화(abstracts)한다. 주요한 새 기능(key new features)은 다음과 같다:
- A black-box cooperative thread pool.
async
/await
syntax.- Structured concurrency.
- Context-aware code compilation..
이 개요(high-level overview)를 바탕으로, 이러한 각(each) 기능(features)을 자세히(deeper) 살펴본다(look at).
1. A Cooperative Thread Pool
새로운(new) 모델(model)은 사용 가능한(available) CPU 코어(cores) 수를 초과(exceed)하지 않도록 스레드(thread) 풀(pool)을 투명하게(transparently) 관리(manages)한다. 이렇게 하면 런타임(runtime)에 스레드(threads)를 생성(create) 및 제거(destroy)하거나 비용이 많이 드는(expensive) 스레드 전환(thread switching)을 지속적으로(constantly) 수행(perform)할 필요가 없다. 대신(instead), 코드(code)를 일시 중단(suspend)했다가 나중에 풀(pool)의 사용 가능한(available) 스레드(threads)에서 매우 빠르게(quickly) 재개(resume)할 수 있다.
2. async/await Syntax
Swift의 새로운(new)
async
/await
구문(syntax)을 사용하면 컴파일러(compiler)와 런타임(runtime)은 코드 조각(a piece of code)이 앞으로(in the future) 한 번 이상 실행(execution)을 중단(suspend)했다가 재개(resume)할 수 있음을 알 수 있다. 런타임(runtime)에서 이를 원활하게(seamlessly) 처리(handles)하므로 스레드(threads)와 코어(cores)에 대해 걱정(worry)할 필요가 없다.새로운 언어 구문(syntax)은 콜백(callbacks)으로 이스케이핑 클로저(escaping closures)를 사용할 필요가 없기 때문에
self
나 다른 변수(variables)를 약하게(weakly) 또는 강하게(strongly) 캡처(capture)하는 구문을 제거(removes)할 수 있다는 것도 큰 장점(wonderful bonus)이다.3. Structured Concurrency
각(each) 비동기(asynchronous) 작업(task)은 이제 상위(parent) 작업(task)과 지정된(given) 실행(execution) 우선 순위(priority)가 지정된 계층 구조(hierarchy)의 일부(part)가 된다. 이 계층 구조(hierarchy)를 사용하면 상위 작업(parent)이 취소될 때 런타임(runtime)에서 모든 하위(child) 작업(tasks)을 취소(cancel)할 수 있다. 또한(furthermore), 런타임(runtime)은 상위 작업(parent)이 완료(completes)되기 전, 모든 하위 작업(children)이 완료(complete)될 때까지 기다릴(wait for) 수 있다. 모든 부분에서 능숙하게 운영할 수 있다(It’s a tight ship all around).
이는 높은 우선 순위(high-priority) 작업(tasks)이 해당 계층 구조(hierarchy)의 낮은 우선 순위(low-priority) 작업(tasks)보다 먼저 실행되는 큰(huge) 이점(advantage)과 보다 명확한(obvious) 결과(outcome)를 제공(provides)한다.
4. Context-aware Code Compilation
컴파일러(compiler)는 주어진(given) 코드 조각(piece of code)이 비동기적으로(asynchronously) 실행(run)될 수 있는지 여부를 계속(keeps) 추적(track)한다. 만약 그렇다면 공유 상태 변경(mutating shared state)과 같이 잠재적으로(potentially) 안전하지 않은(unsafe) 코드(code)를 작성(write)하지 못하도록 한다.
이러한 높은 수준(high level)의 컴파일러(compiler) 인식성(awareness)은 컴파일(compile) 시(time)에 상태(state)에 대한 동기(synchronous) 접근과 비동기(asynchronous) 접근(access)을 구별(differentiate)하고, 안전하지 않은(unsafe) 코드(code) 작성(write)을 어렵게 만들어 부주의로 인한(inadvertently) 데이터 손상(corrupting)을 방지하는 액터(actors)와 같은 정교한(elaborate) 새 기능(features)을 구현할 수 있게(enables) 한다.
이러한 모든 이점(advantages)을 염두에 두고(in mind), 새로운 동시성(concurrency) 기능(features)으로 코드를 작성(writing)해 그것이 어떤 느낌인지 직접 확인하게 될 것이다.
Running the Book Server
이 장(chapter)의 나머지(rest) 부분에서는 실시간(live) 가격 모니터링(price monitoring) 기능을 갖춘 완전한(fully-fledged) 주식(stock) 거래(trading) 앱(app)인 LittleJohn을 만들 것이다.
API에 대한 간단한(brief) 설명(explanation)과 함께 빠른 속도로(at a quick pace) 해당 코드(code)를 작성할 것이다. 지금 당장(right now)은 이 과정(process)을 즐기고(enjoy), 구조(mechanics)에 대해 걱정(worry)하지 않아도 된다. 다음 장(the coming chapters)에서 핵심(nitty-gritty) 세부 사항(details)에 대해 자세히(at length) 살펴볼 것이다.
가장 먼저 해야 할 일(first things first): 이 책(book)에서 다루는 대부분(most of)의 프로젝트(projects)는 JSON 데이터(data)를 가져오고(fetch), 이미지(images)를 다운로드(download)하는 등 웹(web) API에 접근(access)해야 한다. 이 책에는 book server 라는 자체 서버(server) 앱(app)이 함께 제공되며, 해당 장(chapters)을 진행하는 동안 항상 백그라운드(background)에서 실행(run)되어야 한다.
Mac의 Terminal 앱(app)을 열고 해당 저장소(repository)의 00-book-server 폴더(folder)로 이동한다. 다음을 입력(entering)하여 앱(app)을 시작(start)한다:
swift run
서버(server)를 처음 실행(run)하면 몇 가지(few) 종속 요소(dependencies)를 다운로드(download)하고 빌드(build)한다. 이 작업은 1~2분 정도 소요될 수 있다. 다음(following)과 같은 메시지(message)가 표시되면 서버(server)가 가동되어 실행 중(up and running)임을 알 수 있다:
[ NOTICE ] Server starting on http://127.0.0.1:8080
서버(server)에 접근(access)할 수 있는지 다시 확인(double-check)하려면, 즐겨 사용하는(favorite) 웹(web) 브라우저(browser)를 실행(launch)하고 http://localhost:8080/hello 주소를 연다(open).
이렇게 하면 컴퓨터(computer)에서 실행 중(running)인 book server에 접속(contacts)하여, 현재 날짜(current date)로 응답(respond)한다:
나중에(later) 해당 프로젝트(project) 작업을 마치고(finished), 서버(server)를 중지(stop)하려면 Terminal 창(window)으로 전환(switch)하고 Control-C 를 눌러 서버(server) 프로세스(process)를 종료한다.
Note: 서버(server) 자체는 Vapor 프레임워크(framework)를 사용하는 Swift 패키지(package)이지만, 이 책(book)에서는 해당 코드(code)를 다루지(cover) 않는다. 궁금한 점이 있다면(curious) Xcode에서 열어(open) 읽어(read)봐도 좋다. 또한 Server-Side Swift with Vapor에서 Vapor 사용에 대한 모든 것을 배울 수 있다.
Getting Started with LittleJohn
이 책(book)의 모든 프로젝트(projects)와 마찬가지로, LittleJohn의 SwiftUI 뷰(views), 네비게이션(navigation), 데이터 모델(data model)은 이미 구성되어(wired up) 준비돼(ready for) 있다. 선택한(selected) "주식 가격(stock prices)"을 실시간(live)으로 보여주는(displays) 간단한(simple) 티커(ticker) 앱(app)이다:
Note: 서버(server)는 임의의 숫자(random numbers)를 앱(app)에 보낸다(sends). 이 가상(fictitious) 가격(prices)의 상승(upward) 또는 하락(downward) 추세(trends)에 대해 어떠한 의미도 두지 않아야 한다.
앞에서 언급했듯이(as mentioned earlier), 이 장(chapter)의 흐름(flow)에 따라 앱(app) 작업을 즐겨본다. 여기서 수행하는 모든 작업은 이후(later) 장(chapters)에서 다시 살펴보고(revisit), 모든 API에 대해 더 자세히(detail) 알아볼 것이다.
가장 먼저 해야 할 일은 메인(main) 앱(app) 화면(screen)에 비동기(asynchronous) 코드(code)를 추가(add)하는 것이다.
Introducing async/await
첫 번째 작업(task)으로 웹(web) 서버(server)에서 사용 가능한(available) 주식(stocks) 목록을 JSON 형식(format)으로 가져오는(fetches) 함수(function)를 앱(app) 모델(model)에 추가(add)한다. 이는 iOS 프로그래밍(programming)에서 매우 일반적인(common) 작업(task)이므로 첫 번째 단계(step)로 적절(fitting)하다.
이 장(chapter)의 자료(materials)에서 projects/starter 아래(under)에 있는 LittleJohn의 starter 버전(version)을 연다(open). 그런 다음(then), LittleJohnModel.swift 를 열고(open)
LittleJohnModel
내부(inside)에 새로운 메서드(method)를 추가(add)한다:func availableSymbols() async throws -> [String] { guard let url = URL(string: "http://localhost:8080/littlejohn/symbols") else { throw "The URL could not be created." } }
위(above) 코드(code)에서 사용된 최신(modern) 동시성(concurrency)의 주요 특징(key features) 중 몇 가지를 살펴본다.
메서드(method) 정의(definition)에
async
키워드(keyword)를 사용하면 컴파일러(compiler)는 해당 코드(code)가 비동기(asynchronous) 컨텍스트(context)에서 실행(runs)된다는 것을 알 수 있다. 즉(in other words), 해당 코드(code)가 중단(suspend)되었다가 재개(resume)될 수도 있다는 의미이다. 또한(also), 메서드(method)가 완료(complete)되는 데 걸리는 시간에 관계없이, 궁극적으로(ultimately) 동기(synchronous) 메서드(method)가 수행하는 것과 마찬가지로(much like) 값(value, 또는 오류(throws))을 반환(returns)한다.Note: 이 책(book)의 starter 프로젝트(projects)에는
String
에 대한 확장(extension)이 포함(contain)되어 있어, 사용자 지정(custom) 오류(error) 유형(types)을 생성(creating)하는 대신 단순히(brevity) 문자열(strings)을 던질(throw) 수 있다.다음으로(next), 위에서 작성한
availableSymbols()
메서드(method)의 맨 아래(bottom)에 다음 코드(code)를 추가(add)하여URLSession
을 호출(call)하고 book server에서 데이터를 가져온다(fetch):let (data, response) = try await URLSession.shared.data(from: url)
async
메서드(method)URLSession.data(from:delegate:)
를 호출(calling)하면,availableSymbols()
가 일시 중단(suspends)되고 서버(server)에서 데이터를 다시 가져올 때 재개(resumes)된다:await
를 사용하면 런타임(runtime)에 중단 지점(suspension point)이 주어지며(gives), 먼저 실행(run)할 다른 작업(tasks)이 있는지 고려(consider)한 다음 코드를 계속(continue) 실행(running)한다.비동기(asynchronous) 호출(call)을 하면서 스레드(threads)나 전달(passing)해야 할 클로저(closures)에 대해 걱정(worry)할 필요가 전혀 없어 깔끔(neat)하다.
다음으로(next), 서버(server) 응답(response)을 확인(verify)하고, 가져온(fetched) 데이터를 반환(return)해야 한다. 아래 코드(code)를 추가(append)하여 메서드(method)을 완료(complete)한다:
guard (response as? HTTPURLResponse)?.statusCode == 200 else { throw "The server responded with an error." } return try JSONDecoder().decode([String].self, from: data)
먼저(first), 응답(response) 코드(code)가
200
인지 확인(check)한다. 서버(server) 언어(language)에서 이는 성공적인(successful) OK 응답(response)을 나타낸다. 그런 다음(then), 응답(response) 데이터를String
목록(list)으로 디코딩(decode) 한다(try). 성공(succeeds)하면 해당 결과(result)를 반환(return)한다.2장(chapter), "Getting Started With async/await"에서
async
/await
에 대해 자세히(detail) 알아볼 것이다.Note: 웹(web) 서버(servers)는 수 많은(myriad) 상태 코드(status codes)로 응답(respond)할 수 으며, 이 책(book)에서는 그것들을 모두 다루지(cover) 않을 것이다. 더 알고 싶다면, 이 목록(list)을 확인(check out)해 본다: HTTP status codes.
Using async/await in SwiftUI
Command-B를 눌러(press) 프로젝트(project)를 컴파일(compile)하고 지금까지 모든 코드(code)를 올바르게(correctly) 추가(added)했는지 확인(verify)하되, 아직 실행(run)은 하지 않는다. 다음으로(next), 티커(symbol) 목록(list) 화면(screen)에 대한 SwiftUI 코드(code)인 SymbolListView.swift를 연다(open).
여기서 중요한(essential) 부분(part)은 화면(onscreen)의 목록(list)에
symbols
를 표시하는ForEach
이다. 방금 생성한(created)LittleJohnModel.availableSymbols()
를 호출(call)하고, 그 결과(result)를SymbolListView.symbols
에 할당(assign)하여 모든 작업이 함께(together) 수행되도록 해야 한다.SymbolListView.body
내부(inside)에서.padding(.horizontal)
뷰 수정자(view modifier)를 찾는다. 그 바로(immediately) 아래(below)에 다음 코드(code)를 추가(add)한다:.onAppear { try await model.availableSymbols() }
Xcode를 확인(attention)하면, 코드(code) 자동 완성(autocompletion)에서
availableSymbols()
메서드(method)가 회색(grayed out)으로 표시되어 있음을 알 수 있다:또한 컴파일러(compiler)가 다음과 같은 적절한(rightfully) 오류를 보여준다:
Invalid conversion from throwing function of type '() async throws -> Void' to non-throwing function type '() -> Void'
Xcode는
onAppear(...)
가 코드(code)를 동기식(synchronously)으로 실행(runs)된다고 알려준다(tells). 그러나(however) 현재 코드는 비동시성(non-concurrent) 컨텍스트(context)에서 비동기(asynchronous) 함수(function)를 호출(call)하려 하고 있다.다행히도(luckily)
onAppear(...)
대신.task(priority:_:)
뷰 수정자(view modifier)를 사용해 비동기(asynchronous) 함수(functions)를 바로(right away) 호출(call)할 수 있다.onAppear(...)
를 제거(remove)하고 다음으로 바꾼다(replace):.task { guard symbols.isEmpty else { return } }
task(priority:_:)
를 사용하면 비동기(asynchronous) 함수(functions)를 호출(call)할 수 있으며.onAppear(_:)
와 유사하게(similarly to) 뷰(view)가 화면에(onscreen) 나타날(appears) 때 호출(called)된다. 그렇기 때문에 티커(symbols)가 없는지 확인하는 것부터 시작(start by)해야 한다.이제(now), 새로운 비동기(async) 함수(function)를 호출(call)하려면
task { ... }
수정자(modifier) 내부(inside)에 다음을 추가(append)한다:do { symbols = try await model.availableSymbols() } catch { }
이전과 마찬가지로(as before),
try
와await
를 모두 사용(use)하여 메서드(method)가 오류(error)를 발생(throw)시키거나 비동기적으로(asynchronously) 값(value)을 반환(return)할 수 있음을 나타낸다(signify). 결과(result)를symbols
에 할당(assign)하기만 하면 된다. 그게 여기서 해야 할 전부이다(that’s all you need to do here).catch
부분이 여전히 비어 있음(empty)을 알 수(notice) 있다.availableSymbols
가 유효한(valid) 응답(response)을 제공(provide)할 수 없는 잘못된(erroneous) 경우를 확실히(definitely) 처리(handle)해야 한다.starter 프로젝트(project)의 UI는
lastErrorMessage
를 업데이트(update)하는 경우 알림(alert box)을 표시(display)하도록 이미 구현(wired to)되어 있으므로, 여기에서는 해당 기능(functionality)을 사용합니다. 빈(empty)catch
블록(block) 내부(inside)에 다음 행(line)을 추가(add)한다:lastErrorMessage = error.localizedDescription
Swift는 오류(error)를 발생시키는 스레드(thread)에 관계없이(regardless of) 이를 포착(catches)한다. 코드가 완전히(entirely) 동기식(synchronous)인 것처럼 오류(error) 처리(handling) 코드(code)를 작성(write)하기만 하면 된다. 놀랍다(amazing)
서버(server)가 여전히(still) Terminal에서 실행 중(running)인지 빠르게(quickly) 확인(check)한 다음, 앱(app)을 빌드(build)하고 실행(run)한다.
앱(app)이 실행(launches)되는 즉시(as soon as), 인디케이터(activity indicator)가 잠시(briefly) 나타난뒤 주식(stock) 티커(symbols) 목록(list)이 표시된다:
굉장하다(awesome). 다음(next) 작업(task)은 비동기(asynchronous) 오류(error) 처리(handling)가 예상대로(as expected) 작동하는지 확인(test)하는 것이다. Terminal로 전환(switch to)하고 Control-C를 눌러(press) book server를 중지(stop)한다.
프로젝트(project)를 다시 한 번 실행(run)한다. 이제(now),
catch
블록(block)이 오류(error)를 처리(handle)하고 이를lastErrorMessage
에 할당(assign)한다. 그런 다음(then), SwiftUI 코드(code)가 이를 알림(alert box)으로 나타낸다(pop up):최신(modern) Swift 코드(code)를 작성(writing)하는 것이 그리 어렵지(difficult)는 않았을 것이다.
네트워킹(networking)에 필요한 코드가 적다는 사실에 흥분(excited)한다면 충분히 이해한다. 솔직히(to be honest) 나도 흥분했기(excited) 때문이다. 나는 모든 문장(sentence)을 느낌표(exclamation mark)로 끝내지(ending) 않도록 정말로(really) 자제(restrain)해야 했다!
Using Asynchronous Sequences
비록(even though) 이 장(chapter)은 도입부(introduction)에 불과하지만, 비동기(asynchronous) 시퀀스(sequences)와 같은 좀 더 고급(advanced) 주제(topics)를 다루어 볼 것이다.
비동기(asynchronous) 시퀀스(sequences)는 표준(standard) 라이브러리(library)의 "기본(vanilla)" Swift 시퀀스(sequences)와 유사(similar to)하다. 비동기(asynchronous) 시퀀스(sequences)의 후크(hook)는 시간 경과(over time)에 따라 점점 더 많은 요소(elements)를 사용할 수 있게 되고, 해당 요소(elements)를 비동기적으로(asynchronously) 반복(iterate over)할 수 있다.
TickerView.swift를 연다(open). 이것은
SymbolListView
와 구조(structure)가 유사한(similar) SwiftUI 뷰(view)이다. 시간 경과(over time)에 따른 주가(stock price) 변화(changes)를 보여주는(displays)ForEach
를 중심(around)으로 진행된다(revolves).이전(previous) 섹션(section)에서는 비동기(async) 네트워크(network) 요청(request)을 "실행(fired)"하고 결과(result)를 기다린(waited for) 다음 반환(returned)했다.
TickerView
에서는 요청(request)이 완료(complete)될 때까지 기다렸다가(wait for) 데이터를 표시(display)할 수 없기 때문에 동일한 방식(approach)을 사용할 수 없다. 데이터는 계속해서(keep) 무한정(indefinitely) 들어오고(coming), 변화된 가격(price)을 가져와야(bring) 한다.여기에서(here) 서버(server)는 시간이 지남(over time)에 따라 점점 더 많은 텍스트(text)를 추가(adding)하여 오래 지속되는 단일 응답(a single long-living response)을 보낸다(send). 각(each) 텍스트(text) 행(line)은 자체적으로 디코딩(decode)할 수 있는 완전한(complete) JSON 배열(array)이다:
[{"AAPL": 102.86}, {"BABA": 23.43}] // .. waits a bit ... [{"AAPL": 103.45}, {"BABA": 23.08}] // .. waits some more ... [{"AAPL": 103.67}, {"BABA": 22.10}] // .. waits even some more ... [{"AAPL": 104.01}, {"BABA": 22.17}] // ... continuous indefinitely ...
실시간(live) 티커(ticker) 화면(screen)에서 응답(response)의 각(each) 행(line)을 반복(iterate over)하여, 화면(onscreen)의 가격(prices)을 실시간으로(in real time) 업데이트(update)한다.
TickerView
에서.padding(.horizontal)
을 찾는다(find). 해당 행(line) 바로 아래(below)에task
수정자(modifier)를 추가(add)하고, 실시간(live) 가격(price) 업데이트(updates)를 시작(starts)하는 모델(model)의 메서드(method)를 호출(call)한다:.task { do { try await model.startTicker(selectedSymbols) } catch { lastErrorMessage = error.localizedDescription } }
코드(code)는 메서드(method)가 결과(result)를 반환(return)하지 않는다는 점만 제외(except)하면,
SymbolListView
에서 수행한 것과 유사하다(looks similar to). 여기서는 단일 반환 값(a single return value)이 아닌 지속적인(continuous) 업데이트(updates)를 처리(handling)하게 된다.LittleJohnModel.swift를 열고(open) 실시간(live) 업데이트(updates)를 추가할
startTicker(_:)
자리 표시자(placeholder) 메서드(method)를 찾는다(find).tickerSymbols
라는 published 속성(property)은 이미 티커(ticker) 화면(screen)의 UI에 연결(wired up)되어 있으므로, 이 속성(property)을 업데이트(updating)하면 변경 사항(changes)이 뷰(view)에 전파(propagate)된다.다음으로
startTicker(_:)
의 끝(end)에 아래 코드(code)를 추가(add)한다:let (stream, response) = try await liveURLSession.bytes(from: url)
URLSession.bytes(from:delegate:)
는 이전(previous) 섹션(section)에서 사용한 API와 유사(similar to)하다. 그러나(however) 데이터 대신(instead of) 시간 경과(over time)에 따라 반복(iterate)되는 비동기(asynchronous) 시퀀스(sequence)를 반환(returns)한다. 이는 해당 코드의stream
에 할당(assigned to)된다.또한(additionally), 공유(shared) URL 세션(session)을 사용하는 대신(instead of)
liveURLSession
이라는 미리 구성된(pre-configured) 사용자 정의(custom) 세션(session)을 사용하여, 만료(expire)되거나 시간 초과(time out)되지 않는 요청(requests)을 생성한다. 이렇게 하면 매우 긴(super-long) 서버(server) 응답(response)을 무기한(indefinitely)으로 계속(keep) 수신(receiving)할 수 있다.이전과 마찬가지로(just as before), 가장 먼저 해야 할 일(the first thing to do)은 성공적인(successful) 응답(response) 코드(code)를 확인(check)하는 것이다. 동일한 함수(function)의 끝(end)에 다음 코드(code)를 추가(add)한다:
guard (response as? HTTPURLResponse)?.statusCode == 200 else { throw "The server responded with an error." }
이제 재미(fun)있는 부분이 나온다. 새로운 반복문(loop)을 추가(append)한다:
for try await line in stream.lines { }
stream
은 서버(server)가 응답(response)으로 보내는(sends) 바이트 시퀀스(a sequence of bytes)이다.lines
은 해당 시퀀스(sequence)를 추상화(abstraction)하여 응답(response)의 텍스트(text) 행(lines)을 하나씩(one by one) 제공(gives)한다.행(line)을 반복(iterate over)하고 각(each) 행을 JSON으로 디코딩(decode)한다.
이렇게 하려면
for
반복문(loop) 내부(inside)에 다음을 추가(insert)한다:let sortedSymbols = try JSONDecoder() .decode([Stock].self, from: Data(line.utf8)) .sorted(by: { $0.name < $1.name }) tickerSymbols = sortedSymbols print("Updated: \(Date())")
디코더(decoder)가 성공적으로(successfully)
line
을 티커 목록(a list of symbols)으로 디코딩(decodes)하면, 이를 정렬(sort)하고tickerSymbols
에 할당(assign)하여 화면(onscreen)에 렌더링(render)한다. 디코딩(decoding)이 실패(fails)하면,JSONDecoder
는 단순히(simply) 오류(error)를 발생(throws)시킨다.마지막 오류 처리(error handling) 테스트(test)에서 book server를 여전히(still) 끈(turned off) 상태라면 다시 실행(again)한다. 그런 다음(then), 앱(app)을 빌드(build)하고 실행(run)한다. 첫 번째 화면(screen)에서 몇 가지 주식(stocks) 종목을 선택(select)한다:
그런 다음(then) 실시간 시세(Live ticker)를 탭(tap)하여, 다음 화면(screen)에서 실시간(live) 가격(price) 업데이트(updates)를 확인한다:
가격(price) 업데이트(updates)가 표시될 가능성이 높지만(though), 코드 편집기(code editor)에서
Publishing changes from background threads is not allowed...
라는 오류(glitches)와 보라색(purple) 경고(warning)를 확인(notice)할 수 있다.Updating Your UI From the Main Thread
이전에는(earlier),
@State
속성(property)을 업데이트(updating)하여 변경(updates)을 내보냈으며(published), SwiftUI는 메인 스레드(main thread)에서 변경(updates)을 라우팅(route)했다. 지금은(now) UI 업데이트(update)라고 지정(specifying)하지 않고(without) 비동기(asynchronous) 작업을 실행하는 동일한 컨텍스트(context) 내에서tickerSymbols
을 업데이트(update)하므로 코드(code)가 풀(pool)의 임의(arbitrary) 스레드(thread)에서 실행(running on)된다.이는 SwiftUI가 UI를 업데이트(updates)할 때 코드가(code) 정결(kosher)할 것으로 자연스레(naturally) 기대(expects)하기 때문에 약간의 고민(grief)을 유발(causes)한다.
다행히도(luckily), 필요할 때마다 메인 스레드(main thread)로 전환(switch to)할 수 있다.
MainActor
는 메인 스레드(main thread)에서 코드(code)를 실행(runs)하는 유형(type)이다.MainActor.run(_:)
을 호출(calling)하면 어떤 코드(code)든 쉽게(easily) 실행(run)할 수 있다.그러나(however), UI를 구동(drive)하는 모델(model) 클래스(classes)의 경우 모든 모델(model) 코드(code)를 메인 액터(main actor)에서 실행(run)되도록 할당(assign)하는 것이 좋다. 파일(file)을 위로 스크롤(scroll up)하여 다음과 같이
LittleJohnModel
에@MainActor
주석(annotate)을 추가한다:@MainActor class LittleJohnModel: ObservableObject {
앱(app)을 실행(run)하고 실시간 시세(live prices) 화면(screen)으로 이동한다. 이번에는(this time around) 가격(prices)이 지속적으로(continuously) 오르내리는 것(go up and down)을 볼 수 있다:
비동기(asynchronous) 시퀀스와(sequences)의 첫 만남(first encounter)이 즐거웠기를 바란다. 3장(chapter), "AsyncSequence & Intermediate Task"에서 더 많은 것을 배우게 될 것이다.
Canceling Tasks in Structured Concurrency
앞서 언급했듯이(as mentioned earlier), Swift 동시(concurrent) 프로그래밍(programming)의 큰 진전(leaps) 중 하나는 최신(modern) 동시(concurrent) 코드(code)가 구조화된(structured) 방식으로 실행(executes)된다는 것이다. 작업(task)은 엄격한(strict) 계층 구조(hierarchy)로 실행(run)되므로, 런타임(runtime)은 상위(task) 작업과 새 작업(tasks)이 상속(inherit)해야 하는 기능(features)을 알고 있다.
예를 들어(for example),
TickerView
에서task(_:)
수정자(modifier)를 확인해 본다. 코드(code)는startTicker(_:)
를 비동기적으로(asynchronously) 호출(calls)한다. 그리고startTicker(_:)
는 비동기식으로(asynchronously)URLSession.bytes(from:delegate:)
를 기다리고(awaits) 있으며, 이는 반복(iterate over)하는 비동기(async) 시퀀스(sequence)를 반환(returns)한다:각(each) 일시 중단(suspension) 지점(point)에서, 즉
await
키워드(keyword)가 나타날 때마다, 스레드(thread)는 잠재적(potentially)으로 변경(change)될 수 있다.task(_:)
내부(inside)에서 전체(entire) 프로세스(process)를 시작(start)하기 때문에 비동기(async) 작업(task)은 실행 스레드(execution thread) 또는 중단 상태(suspension state)에 관계없이(regardless of) 다른 모든 작업(tasks)의 상위(parent)가 된다.SwiftUI의
task(_:)
뷰 수정자(view modifier)는 뷰(view)가 사라질(goes away) 때 비동기(asynchronous) 코드(code)를 취소(canceling)한다.이 책(book)의 뒷부분에서 더 자세히 배우게(learn) 될 구조화된 동시성(structured concurrency) 덕분에 사용자가 화면(screen) 밖으로 이동(navigates)할 때 모든 비동기(asynchronous) 작업(tasks)도 취소된다(canceled).
이것이 실제로 어떻게 작동하는지 확인(verify)하려면 업데이트(updates) 화면(screen)으로 이동(navigate)하여 Xcode 콘솔(console)을 살펴보고(look at)
LittleJohnModel.startTicker(_:)
의 디버그(debug) 출력(prints)이 표시되는지 확인한다:Updated: 2021-08-12 18:24:12 +0000 Updated: 2021-08-12 18:24:13 +0000 Updated: 2021-08-12 18:24:14 +0000 Updated: 2021-08-12 18:24:15 +0000 Updated: 2021-08-12 18:24:16 +0000 Updated: 2021-08-12 18:24:17 +0000 Updated: 2021-08-12 18:24:18 +0000
이제 Back을 누른다(tap).
TickerView
가 사라지고(disappears),task(_:)
뷰 수정자(view modifier)의 작업(task)이 취소(canceled)된다.LittleJohnModel.startTicker(_:)
호출(call)을 포함(including)한 모든 하위(child) 작업(tasks)이 취소(cancels)된다. 결과적으로(as a result), 콘솔(console)의 디버그(debug) 로그(logs)도 중지(stop)되어 모든 실행(execution)이 종료(end)되었음을 확인(verifying)할 수 있다.그러나(however), 콘솔(console)에 다음과 같은 추가(additional) 메시지(message)가 표시된다:
[Presentation] Attempt to present <SwiftUI.PlatformAlertController: 0x7f8051888000> on ... whose view is not in the window hierarchy.
SwiftUI는 티커(ticker) 뷰(view)를 닫은(dismiss) 후, 경고(alert)를 표시(present)하려는 코드(code)와 관련된 문제(issue)를 기록(logging)하고 있다. 이는 런타임(runtime)이
model.startTicker(selectedSymbols)
에 대한 호출(call)을 취소(cancels)할 때 일부 내부(inner) 작업(tasks)에서 취소 오류(cancellation error)를 발생(throw)시키기 때문에 발생(happens)한다.이제 LittleJohn 작업(working)을 마쳤다(finished). 축하한다(congratulations). 이 책(book)의 첫 번째 프로젝트(project)를 완료(completed)했다.
스스로의 힘으로 챌린지(challenge)에 도전하고 싶다면 계속 진행한다(stick around). 그렇지 않다면(otherwise), 페이지를 넘겨(turn)
async
/await
및Task
에 대해 더 자세히(detail) 알아볼 수도 있다.Challenges
Challenge: Adding Extra Error Handling
앱(app)이 여전히(still) 우아하게(graciously) 처리(handle)하지 못하는 극단적인 경우(edge case)가 하나 있다. 사용자(user)가 가격(price) 업데이트(updates)를 확인(observing)하는 동안, 서버(server)를 이용할 수 없게(unavailable) 되는 경우이다.
가격(prices) 화면(screen)으로 이동(navigating)한 다음, 터미널(terminal) 창(window)에서 Control-C를 눌러(pressing) 서버(server)를 중지(stopping)하면 이 상황(situation)을 재현(reproduce)할 수 있다.
오류(error) 자체(per se)가 없기 때문에 앱(app)에 오류(error) 메시지(messages)가 표시(pop up)되지 않는다. 실제로(in fact) 응답(response) 시퀀스(sequence)는 서버(server)가 이를 닫을(closes) 때 완료(completes)된다. 이 경우(in this case) 코드(code)는 오류(error) 없이 계속(continues) 실행(execute)되지만 더 이상 업데이트(updates)가 생성(produces)되지 않는다.
이 챌린지(challenge)에서는 비동기(async) 시퀀스(sequence)가 종료(ends)될 때,
LittleJohnModel.tickerSymbols
를 재설정(reset)하는 코드(code)를 추가(add)한 다음 업데이트(updates) 화면(screen)을 빠져나간다(navigate out).LittleJohnModel.startTicker(_:)
에서for
반복문(loop) 뒤에, 비동기(async) 시퀀스(sequence)가 예기치 않게(unexpectedly) 종료(ends)될 경우tickerSymbols
를 빈(empty) 배열(array)로 설정(set)하는 코드(code)를 추가(append)한다.다음으로(next),
TickerView
에서 관찰된(observed) 티커(ticker) 기호(symbols)의 수를 관찰(observes)하고, 선택(selection)이 재설정(resets)되면 뷰(view)를 닫는(dismisses) 새로운 뷰 수정자(view modifier)를 추가(add)한다:.onChange(of: model.tickerSymbols.count) { newValue in if newValue == 0 { presentationMode.wrappedValue.dismiss() } }
starter에는 이미(already) 사용할 준비가 된
presentationMode
환경(environment)이 포함(includes)되어 있다.모든 것이 순조롭게 진행된다면(if everything goes well), 앱(app)에서 실시간(live) 업데이트(updates)를 보는(watching) 중에 서버를(server) 중지(stop)하면 LittleJohn이 자동으로(automatically) 업데이트(updates) 화면(screen)을 닫고 티커(symbols) 목록(list)으로 돌아간다.
챌린지(challenge)가 막히거나(stuck in) 예상대로(expect) 작동(work)하지 않는다면, 이 장(chapter)의 자료(materials)에서 해답(solution)을 확인(check)해 본다.
Key Points
- Swift 5.5에서 스레드 폭증(thread explosion), 우선 순위 역전(priority inversion), 언어 및 런타임과의 느슨한 통합(loose integration with the language and the runtime)과 같은 기존(existing)의 다양한 동시성(concurrency) 문제(issues)를 해결(solves)하는 새로운 동시성 모델(new concurrency model)을 도입(introduced)했다.
async
키워드(keyword)는 함수(function)를 비동기(asynchronous)로 정의(defines)한다.await
를 사용하면 비동기(asynchronous) 함수(function)의 결과(result)를 비차단(non-blocking) 방식(fashion)으로 기다릴(wait) 수 있다.- 비동기(asynchronous) 코드(code)를 실행(run)하려면
onAppear(_:)
대신(alternative)task(priority:_:)
뷰 수정자(view modifier)를 사용한다. for try await
반복문(loop) 구문(syntax)을 사용하면 시간 경과(over time)에 따라 자연스럽게(naturally) 비동기(asynchronous) 시퀀스(sequence)를 반복(loop over)할 수 있다.
'Raywenderlich > Modern Concurrency in Swift' 카테고리의 다른 글
Chapter 4: Custom Asynchronous Sequences With AsyncStream (0) 2023.01.30 Chapter 3: AsyncSequence & Intermediate Task (0) 2023.01.02 Chapter 2: Getting Started With async/await (0) 2022.12.21