동시성은 여러 작업을 병렬로 수행하는 소프트웨어 기능으로 정의할 수 있다. 많은 앱 개발 프로젝트는 어느 시점에서 동시 처리를 사용해야 하며, 동시성은 우수한 사용자 경험을 제공하는 데 필수적이다. 예를 들어, 동시성은 앱의 사용자 인터페이스가 이미지 다운로드 또는 데이터 처리와 같은 백그라운드 작업을 수행하는 동안 앱의 응답성을 유지하도록 해준다.
스위프트 프로그래밍 언어의 구조화된 동시성 기능을 살펴보고 이러한 기능을 사용하여 앱 프로젝트에 멀티테스킹 지원을 추가하는 방법을 공부해보자.
스레드 개요
스레드(thread)는 현대 CPU의 기능이며 모든 멀티테스킹 운영체제에서 동시성의 기반을 제공한다. 현대 CPU는 많은 수의 스레드를 실행할 수 있지만, 한 번에 병렬로 실행할 수 있는 실제 스레드 수는 CPU 코어 수에 의해 제한된다.(CPU 모델에 따라 일반적으로 4~16개 코어) CPU 코어보다 많은 스레드가 필요한 경우 운영체제는 스레드 스케줄링을 수행하여 이들 스레드의 실행을 사용 가능한 코어 간에 어떻게 공유할지를 결정한다.
스레드는 메인 프로세스 내에서 실행되는 미니 프로세스로 생각할 수 있으며, 그 목적은 앱 코드 내에서 병렬 실행의 형태를 가능하게 하는 것에 있다.
다행스럽게도 구조화된 동시성이 백그라운드에서 스레드를 사용하지만 모든 복잡성을 처리하므로 우리가 직접적으로 상호작용할 필요가 없다는 것이다.
앱 메인 스레드
앱이 처음 시작될 때 런타임 시스템은 보통 앱이 기본적으로 실행되는 단일 스레드를 생성한다. 이 스레드를 일반적으로 메인 스레드라고 한다. 메인 스레드의 주요 역할은 UI 레이아웃 렌더링, 이벤트 처리 및 사용자 인터페이스에서 뷰와 사용자 상호작용 측면에서 사용자 인터페이스를 처리하는 것이다.
메인 스레드를 사용해 시간 소모적인 작업을 수행하는 앱 내의 다른 코드는 시간 소모적인 작업이 완료될 때까지 전체 앱이 멈춘 것처럼 보이게 한다. 이것은 메인 스레드가 다른 작업을 방해받지 않고 계속할 수 있도록 수행할 작업을 별도의 스레드에서 시작해 피할 수 있다.
[!note] 이해하고 넘어가자. 아이폰 앱이 실행되면 메인 스레드라는 기본 실행 줄기가 생기는데, 이 역할은 주로 UI 관련이다. 그래서 메인 스레드는 절대 오래 걸리는 작업(서버에서 데이터 받기, 큰 파일 읽기)을 하면 안된다. 왜? 그동안 화면이 멈춘 것처럼 보이기 때문이다.
완료 핸들러
오래 걸리는 작업은 메인 스레드가 아닌 다른 스레드(백그라운드)에서 실행된다. 그런데 작업이 끝난 후에는 결과를 UI에 보여줘야 한다. 이때 완료 핸들러라는 코드를 준비해둔다.
- 완료 핸들러는 "작업이 끝난 순간 자동으로 불려서 결과를 넘겨주는 함수(클로저)"이다.
func downloadImage(url: String, completion: @escaping (UIImage?) -> Void) { DispatchQueue.global().async { // 백그라운드에서 실행 let data = try? Data(contentsOf: URL(string: url)!) let image = data.flatMap { UIImage(data: $0) } // 완료되면 completion 호출 DispatchQueue.main.async { completion(image) // 메인 스레드에서 UI 업데이트 } } } // 사용하기 downloadImage(url: "https://example.com/image.png") { image in myImageView.image = image // 완료 후 실행될 코드 } - downloadImage 함수는 이미지를 다운로드하는 동안 메인 스레드를 막지 않는다.
- 다운로드가 끝나면 completion이 호출되어 결과(UIImage)를 받아 UI에 표시할 수 있다.
- 쉽게 말해, 완료 핸들러는 "작업 끝나면 이 코드 실행해줘!"라는 약속 같은 것.
completion: @escaping (UIImage?) -> Void - 함수 선언부의 이 부분이 완료 핸들러다.
- UIImage?를 받아서 아무것도 반환하지 않는 함수(클로저)
- 즉, 이미지 다운로드가 끝났을 때 실행할 코드를 담는 공간이다.
{ image in myImageView.image = image } - 이 부분이 완료 핸들러의 실제 코드(본문)
- 다운로드가 끝나면 completion(image)가 실행되며 여기 있는 코드가 불린다.
완료 핸들러 방식의 문제점
- 비동기 작업이 여러 개 이어지면, 중첩 구조가 점점 깊어진다.(콜백 지옥)
- 코드의 흐름을 눈으로 따라가기 어렵고, 에러 처리도 복잡하다.
구조화된 동시성(async / await)
구조화된 동시성은 스위프트 버전 5.5와 함께 스위프트 언어에 도입되어 앱 개발자가 동시 실행을 보다 쉽고 논리적이고 작성하고, 이해하기 쉬운 방식으로 안전하게 구현할 수 있도록 한다. 다시 말해 구조화된 동시성 코드는 논리 흐름을 이해하기 위해 완료 핸들러 코드로 다시 이동할 필요 없이 위에서 아래로 읽을 수 있다.
또한, 구조화된 동시성은 비동기 함수에서 발생하는 오류를 더 쉽게 처리할 수 있도록 한다. 쉽게 말해, async/await를 이용해 비동기 흐름을 마치 동기 코드처럼 단순하게 작성할 수 있는 방법.
import SwiftUI struct ContentView: View { var body: some View { Button(action: { doSomething() }) { Text("Do Something") } } func doSomething() { } func takesTooLong() { } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } } } - 앞의 코드는 클릭할 때 doSomething() 함수를 호출하도록 구성된 Button 뷰를 생성한다.
- 이 템플릿 코드를 변경해가며 스위프트의 구조화된 동시성을 공부해보자.