-
[Swift] Async, Await, Task란?Swift 2024. 8. 14. 01:27
안녕하세요. 1000JI입니다 :)
사실 저에겐 RxSwift를 그동안 계속 다룬 만큼 제일 익숙하기에 다른 비동기 처리에 대한 수요가 따로 없었습니다.
하지만 현재 회사에서 개발하고 있는 프로젝트에서는 다루고 있는 상황입니다..!!!
그래서!! 드디어 말로만 듣던... 제가 많이 활용해보지 못했던 Async, Await를 자세히 살펴보려고 합니다!
Async, Await가 뭘까요?!
저는 지금까지 RxSwift나 Escaping handler를 통해 비동기 처리를 했었는데요~!
Swift 5.5(Swift Concurrency)를 통해 추가된 Concurrency Model로 더욱 편하게 비동기 처리를 해줄 수 있는 문법이라고 보시면 되겠습니다.
그럼 어떻게 더욱 편하게 비동기 처리를 해줄 수 있는 것 일까요?
우선 코드로 먼저 어떻게 간소화가 되었는지, 쉽게 되었는지 살펴보려고 합니다.
해당 코드는 블로그 글을 살펴보다가 비교하기 좋은 코드가 있어서 가져오게 되었습니다 :)
func processImageData2c(completionBlock: (Result<Image, Error>) -> Void) { loadWebResource("dataprofile.txt") { dataResourceResult in switch dataResourceResult { case .success(let dataResource): loadWebResource("imagedata.dat") { imageResourceResult in switch imageResourceResult { case .success(let imageResource): decodeImage(dataResource, imageResource) { imageTmpResult in switch imageTmpResult { case .success(let imageTmp): dewarpAndCleanupImage(imageTmp) { imageResult in completionBlock(imageResult) } case .failure(let error): completionBlock(.failure(error)) } } case .failure(let error): completionBlock(.failure(error)) } } case .failure(let error): completionBlock(.failure(error)) } } } processImageData2c { result in switch result { case .success(let image): display(image) case .failure(let error): display("No image today", error) } }
자!... 코드를 보시면 뭔가 뎁스가 엄청 깊고 코드가 한눈에 읽히지 않죠?
저도 물론 콜백 클로저를 많이 사용해봤지만 이렇게 다중으로 써본 적은 따로 없었던 것 같습니다.
구현하다보면 언젠가 구멍 하나가 발생 할 것 같은 코드로 보입니다........ :)
그러면 만약 Async/Await로 반영한다면 어떻게 코드가 구성이 될까요?
func loadWebResource(_ path: String) async throws -> Resource func decodeImage(_ r1: Resource, _ r2: Resource) async throws -> Image func dewarpAndCleanupImage(_ i : Image) async throws -> Image func processImageData() async throws -> Image { let dataResource = try await loadWebResource("dataprofile.txt") let imageResource = try await loadWebResource("imagedata.dat") let imageTmp = try await decodeImage(dataResource, imageResource) let imageResult = try await dewarpAndCleanupImage(imageTmp) return imageResult }
따란~! 와우 뭔가 되게 간단해졌죠?
각 키워드 async, await가 들어가면서 코드가 단촐(?) 해졌습니다. 그리고 더 읽기도 편해졌네요 :)
위의 코드는 극단적으로 이랬던 코드가 이렇게 될 수 있어!!! 라는 걸 보여줬다면...?
이번에는 좀 더 단순한 코드를 가져와서 살펴보고자 합니다.
func addNumWithAsync() async -> Int { var result: Int = 0 for i in 1...10 { result += i } return result } func callAsyncFunction() async { print(await addNumWithAsync()) } Task { await callAsyncFunction() //55 }
자자자자...
addNumWithAsync() 옆에 있는 async는 무엇일까요?
나는 addNumWithAsync() 메소드를 비동기로 실행하겠다~ 라는 의미 입니다.
만약 에러 핸들링이 필요해서 메소드 내부에서 에러를 던져야하는 상황이라면
func addNumWithAsync() async throws -> Int
이런식으로 throws를 붙이면 됩니다. 쉽죠? :)
아무튼 저 부분은 후순위로 두고!!
그러면 async로 선언한 메소드를 호출하기 위해선 어떻게 해야할까요?
바로 callAsyncFunction() 메소드 왼쪽에 붙어져 있는 await를 함께 호출해야 합니다.
await는 반드시 비동기로 수행되는 함수가 와야한다는 것을 의미합니다.
만약 에러 핸들링 처리로 throws가 붙어져 있는 async 메소드라면
func callAsyncFunction() async { do { try await addNumWithAsync() } catch { print(error) } }
이런식으로 do~catch & try 또는 try?를 붙여서 사용하면 되겠습니다.
여기까지보면 아... 이렇게저렇게 사용하면 되겠구나~?라고 이해가 되실거라 생각합니다!
그러면 좀 더 요것들이 어떻게 돌아가는지 상세하게 살펴보겠습니다.
Suspension Point
갑자기 왠 Suspension Point? 직역하자면 연기(?), 보류(?) 포인트 입니다.
Apple에서는 어떻게 설명하고 있냐면
더보기A suspension point is a point in the execution of an asynchronous function where it has to give up its thread
서스펜션 포인트는 비동기 기능을 실행할 때 스레드를 포기해야 하는 포인트입니다.
비동기 기능을 실행 할 때 스레드를 포기해야하는 포인트...?
이해가 안되는 부분이 많은데 Thread 제어권이 어떻게 이동하는지 배경 지식이 필요합니다.
동기 함수의 Thread 제어권
callAsyncFunction() 함수에서 addNum() 함수를 호출한다면 Thread 제어권은 addNum() 메소드로 넘겨주게 되고,
이후 함수가 종료 되었을 때 다시 callAsyncFunction()이 제어권을 받게 됩니다.
보다시피 동기로 Thread 제어권을 넘겨줬기 때문에 addNum() 메소드가 종료되기 전까지는 다른 작업을 수행 할 수 없습니다.
비동기 함수의 Thread 제어권
예제 코드에 대한 설명이라고 생각하면 됩니다.
addNumWithAsync() 메소드를 호출하게 되면 그 지점이 Suspension Point가 되고, Thread 제어권은 System에게 넘기게 됩니다.
System은 제어권을 다른 Task에 할당하여 수행하다가 비동기 함수가 완료 되었을 때 제어권을 돌려줌으로써
비동기 함수 호출 이후의 작업을 이어서 수행 할 수 있습니다.
두 그림을 비교해보면 addNum() 메소드는 동기이기 때문에 제어권을 끝날 때 까지 쥐고 있다가 끝나면 돌려주지만,
addNumWithAsync() 메소드는 제어권이 System에 있기 때문에 비동기적으로 끝날 때 제어권을 돌려주고 있습니다.
그렇다면 마지막으로 아래 코드 처럼 구현된다면 출력은 어떻게 될까요?
func taskFunction() { Task { await printTestCode() } } func printTestCode() async { await test1() print("DEBUG: called test1") await test2() print("DEBUG: called test2") await test3() print("DEBUG: called test3") } func test1() async { for index in 1...3 { print("DEBUG: 😎 \(index)") } } func test2() async { for index in 1...3 { print("DEBUG: 🤣 \(index)") } } func test3() async { for index in 1...3 { print("DEBUG: 😇 \(index)") } }
바로바로 아래 처럼 표출됩니다!
DEBUG: 😎 1 DEBUG: 😎 2 DEBUG: 😎 3 DEBUG: called test1 DEBUG: 🤣 1 DEBUG: 🤣 2 DEBUG: 🤣 3 DEBUG: called test2 DEBUG: 😇 1 DEBUG: 😇 2 DEBUG: 😇 3 DEBUG: called test3
그런데 이 출력문을 보고 의문점이 생길 수 있습니다.
"어.. 분명 async, await로 비동기 처리하면 Thread 주도권이 System에 넘어가고.. 비동기 함수 호출 이후에 다른 Task 작업을 한다고 했는데...? 왜 순차적으로 동작하지?" 라고요..!
왜냐면 제가 의문점이 들었던 내용이거든요.. :)
바로바로.. test1(), test2(), test3() 앞에 각각 await가 붙어있었기 때문에
순차적으로 기다렸다가 실행해서 동기적으로 동작하는 것입니다.
그러면 어떻게 동시에 시작하게 코드를 구현 할 수 있을까요?
async let print1 = test1() print("DEBUG: called test1") async let print2 = test2() print("DEBUG: called test2") async let print3 = test3() print("DEBUG: called test3") await print1 await print2 await print3
이런식으로 async let을 활용해서 동시에 비동기 처리를 할 수 있습니다!
await가 아니기 때문에 test1() 메소드를 실행하자마자 즉시 실행되며, 결과가 print1에 저장되지만 사용되지 않습니다.
따라서 바로 다음 줄의 코드를 실행하게 됩니다.
그 이후 await를 사용해 async let으로 시작된 작업의 결과를 요청할 때 까지 해당 작업은 백그라운드에서 계속 진행됩니다.
await가 호출되면 그 시점에서 해당 작업이 완료될 때 까지 대기하게 됩니다.
하지만............. 위처럼 구현해도 결과 값은
DEBUG: called test1 DEBUG: called test2 DEBUG: called test3 DEBUG: 😎 1 DEBUG: 😎 2 DEBUG: 😎 3 DEBUG: 🤣 1 DEBUG: 🤣 2 DEBUG: 🤣 3 DEBUG: 😇 1 DEBUG: 😇 2 DEBUG: 😇 3
입니다....... 아까와는 다르게 called 문구는 동시에 출력이 된 것 같은데, 비동기 처리 함수는 순차적으로 동작한 것 같죠..?
내용을 찾아보니 await가 붙은 코드가 항상 다른 테스크로 넘어가거나 비동기적으로 실행된다고 착각 할 수 있습니다.
실제로 실행 흐름 일시 중지(suspend)할 순 있지만, 비동기 함수가 항상 된다는 의미는 아니고 예제로 든 코드를 살펴보면
간단한 반복분 및 print문을 포함하고 있습니다. 따라서 굳이 비동기적으로 실행하지 않고
내부 코드가 완료 될 때 까지 기다리면서 마치 동기적으로 실행된다고 보시면 됩니다.
그러면 포기 할 수 없지 않겠습니까? 간단하게라도 비동기 처리 할 수 있는 예제 코드로 테스트 해보겠습니다.
func taskFunction() { Task { await printTestCode() } } func printTestCode() async { // 비동기적으로 요청을 동시에 실행 async let result1 = test1() async let result2 = test2() async let result3 = test3() // 결과를 기다리고 출력 await result1 print("DEBUG: called test1") await result2 print("DEBUG: called test2") await result3 print("DEBUG: called test3") } func test1() async -> String? { if let data = await fetchData(from: "https://jsonplaceholder.typicode.com/posts/1") { print("DEBUG: test1 response: \(data)") return data } return nil } func test2() async -> String? { if let data = await fetchData(from: "https://jsonplaceholder.typicode.com/posts/2") { print("DEBUG: test2 response: \(data)") return data } return nil } func test3() async -> String? { if let data = await fetchData(from: "https://jsonplaceholder.typicode.com/posts/3") { print("DEBUG: test3 response: \(data)") return data } return nil } func fetchData(from url: String) async -> String? { guard let url = URL(string: url) else { return nil } do { let (data, _) = try await URLSession.shared.data(from: url) return String(data: data, encoding: .utf8) } catch { print("DEBUG: Failed to fetch data from \(url)") return nil } }
따란..! :) 정말 간단한 비동기 처리 코드를 구현하여 가져와보았습니다.
그럼 실행 결과문을 살펴볼까요?
DEBUG: test2 response: { DEBUG: test1 response: { DEBUG: called test1 DEBUG: called test2 DEBUG: test3 response: { DEBUG: called test3
오오...!!! 뭔가 동시에 실행되서 비동기적으로 동작한 것으로 보여집니다!
async로 선언한 메소드 내부의 비동기 처리 할 지 여부도 판단하다니... 똑똑한 것 같습니다... :)
아무튼 요런 예제를 통해서 아하! 꼭 비동기함수라고 선언한다해서 비동기 처리가 되는 것은 아니구나? 라는 것과
await, async를 어떻게 활용하냐에 따라서도 동작이 달라지는구나!! 라는 것을 알게 되었습니다.
추가로 async로 되어있는 메소드를 실행하는 방법 중 또 다른 방법인 Task에 대해서 간단하게 알아보겠습니다.
Task란?
더보기A unit of asynchronous work.
비동기 작업의 단위입니다.
Task 자체가 비동기 작업의 단위기 때문에 async로 선언된 메소드를 호출 할 수 있는 것 입니다.
비동기로 수행 할 작업을 클로저를 통해 전달하여 asynchronous context를 생성하며,
전달된 작업들은 Task 인스턴스 생성과 동시에 수행하게 됩니다.
간단하죠? :)
async 함수는 다른 async 함수 내부 혹은 Task 구조체 같은 asyncronous context에서 호출이 가능하다 했으니까요 :)
이상으로 그동안 알아보고 싶었던 Await, Async, Task에 대해서 알아보았습니다.
생각보다 크~게 어려운 것은 아닌 것 같지만 실무에 적용하다보면 더 활용 방법이 있겠죠?
많이 공부해서 다시 한 번 같은 내용으로 돌아오겠습니다 :)
틀린 내용이나 궁금한 점 있으시면 댓글 부탁드리겠습니다.
감사합니다 :)
참고 사이트
https://ios-development.tistory.com/958
[iOS - swift] Async, Await 사용 방법
Async, Await 이란? 기존에 비동기 처리 방식은 DispatchQueue나 completionHandler를 사용하여 처리했지만, 더욱 편하게 비동기 처리할 수 있는 문법 // DispatchQueue 사용한 비동기 처리 DispatchQueue.global.async { }
ios-development.tistory.com
https://velog.io/@jeunghun2/Swift-async-await
[Swift] async & await
Swift Concurrency async / awiat
velog.io
https://seokyoungg.tistory.com/38
[iOS] 동시성 프로그래밍(9) - async, await
async와 await는 Swift5.5에서 추가된 Concurrency Model로 더 safe, easy, fast 하게 다루기 위해 나온 기능이다. call-back기반의 비동기 함수의 문제점 기존의 비동기 작업이 끝나는 시점을 completion handler를 통
seokyoungg.tistory.com
'Swift' 카테고리의 다른 글
[Swift] @MainActor란? (4) 2024.08.18 [Swift] Actor란? (0) 2024.08.16