Swift 5.5
에서 도입된 Swift Concurrency
는 비동기 프로그래밍을 더욱 쉽고 안전하게 만들어주는 시스템이다.
async
키워드는 비동기적으로 실행되는 함수에 붙이고, await
키워드는 비동기 함수의 결과를 기다릴 때 사용한다.
func fetchData(_ url: URL) async -> Data {
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
let photo1 = await fetchImage(from: url1)
let photo2 = await fetchImage(from: url2)
let photo3 = await fetchImage(from: url3)
let photos = [photo1, photo2, photo3]
await
를 통해 비동기 함수를 호출할 경우, 해당 코드가 완료될 때까지 기다린 뒤 다음 실행을 하기 때문에 위의 코드와 같은 상황이라면 3번을 기다리는 게 된다.
async let photo1 = fetchImage(from: url1)
async let photo2 = fetchImage(from: url2)
async let photo3 = fetchImage(from: url3)
let photos = await [photo1, photo2, photo3]
함수 이름 앞에 await
를 붙이는 대신 결과를 할당하는 상수 앞에 async
를 붙이고, 3개의 응답 결과 앞에 await
를 붙이면 함수가 병렬로 실행된다.
throws
는 오류를 발생하는 함수에 붙이고, throw
는 오류를 발생시킬 때 사용한다.
func fetchData(_ urlString: String) async throws -> Data {
guard let url = URL(string: urlString) else {
throw NetworkError.invalidURL // NetworkError는 커스텀으로 정의한 에러 타입이다.
}
let (data, response) = try await URLSession.shared.data(from: url)
guard let response = response as? HTTPURLResponse,
(200..<300).contains(response.statusCode) else {
throw NetworkError.badResponse
}
guard !data.isEmpty else {
throw NetworkError.noData
}
return data
}
이 때 throws
함수의 결과를 받을 땐 try
가 붙게 된다. async throws
함수의 경우에는 try await
, throws
함수가 아니면 await
만 붙여서 호출한다.
Task
는 비동기 작업의 기본 단위로, 작업을 수행할 클로저를 제공한다. async
함수를 비동기 코드가 아닌 일반 동기 코드에서 호출하려면 Task
를 통해 실행해야 한다. 스코프 내에서 함수를 호출하고, await
키워드를 이용하여 비동기 작업이 완료될 때까지 대기한다.
Task {
let data = try await fetchData("https://example.url")
print(data)
}
위의 코드를 기존 DispatchQueue
코드와 비교해보면 아래와 같다.
// 백그라운드에서 데이터 호출
DispatchQueue.global().async {
let data = fetchData()
// 결과 처리는 메인 스레드
DispatchQueue.main.async {
print(data)
}
}
이와 달리 Task
는 스레드 관리가 자동으로 처리되므로 메인 스레드와 백그라운드 스레드를 명시적으로 지정할 필요가 없다.
Task {
let task1 = Task(priority: .high) {
try await fetchData("https://example.url1")
}
let task2 = Task(priority: .medium) {
try await fetchData("https://example.url2")
}
let task3 = Task(priority: .low) {
try await fetchData("https://example.url3")
}
print(try await task1.value)
print(try await task2.value)
print(try await task3.value)
}
cancel()
: 작업 취소 시 사용let task = Task {
try await fetchData("https://example.url")
}
task.cancel()
yield()
: 작업의 중단 지점 명시func fetchManyData() async {
let datas = await fetchData()
for data in datas {
print(data)
await Task.yield()
}
}
yield
호출 시 현재 Task
의 실행이 중단되고 대기열의 끝으로 이동하며, 대기 중인 Task
의 스케줄링이 진행된다. 과도한 사용은 성능 저하로 이어지므로 필요할 때만 사용하는 게 좋다고 한다.
sleep()
: 동시성 동작을 확인하기 위해 주어진 시간만큼 작업을 중단할 때 사용try await Task.sleep(for: .seconds(2))
동시성을 안전하게 관리하기 위해 설계된 참조 타입이다. 여러 스레드가 동일한 데이터에 접근할 때 발생할 수 있는 경합 문제를 방지한다. (작업을 순차적으로 처리하여 안전성을 보장)
actor
키워드를 사용하여 정의하며, Actor
내부의 데이터는 Actor
내부의 메소드를 통해서만 접근할 수 있다는(캡슐화) 특징이 있다. 내부 메소드는 기본적으로 비동기 메소드로 작성되고 await
키워드를 사용하여 호출한다.
여러 화면에서 동시에 수정 가능한 데이터를 다루는 객체를 액터로 관리하면 좋다. ex. 사용자 정보 관리, 파일 다운로드 매니저, 앱 로깅 시스템, WebSocket 연결 관리 등
actor UserManager {
private var currentUser: User?
func updateNickname(_ nickname: String) async {
currentUser?.nickname = nickname
// 서버 업데이트
try? await APIService.updateUserInfo(nickname: nickname)
}
func getCurrentUser() async -> User? {
return currentUser
}
}
// 여러 view controller에서 동시에 접근해도 안전
class ProfileViewController: UIViewController {
private let userManager: UserManager = .init()
// ... //
func changeNickname() {
Task {
await userManager.updateNickname(nicknameTextField.text ?? "")
}
}
}