Swift Concurrency는 iOS 13.0부터 도입된 기능으로, 비동기 프로그래밍을 보다 직관적으로 작성할 수 있게 해줍니다. 기존의 비동기 프로그래밍 방식은 복잡한 콜백 구조와 클로저를 사용해야 했지만, Swift Concurrency는 async와 await 키워드를 통해 이러한 복잡성을 줄여줍니다. 이를 통해 코드의 가독성이 높아지고, 유지보수가 용이해집니다.
Task는 Swift에서 비동기 작업을 수행하는 기본 단위입니다. Task를 사용하면 비동기 작업을 정의하고, 이를 실행하며, 결과를 처리할 수 있습니다. Task는 비동기 작업을 쉽게 관리할 수 있도록 도와주며, 코드의 가독성을 높이는 데 기여합니다.
async 함수를 생성하고 실행할 때 일반적인 함수 내에서는 실행이 안되는데 그 이유는 async 함수는 concurrent Context에서 실행되어야 하기 때문입니다. 이런 경우 async 함수를 Task로 감싸서 실행할 수 있습니다.
Task{
await asyncFunction()
}
Task는 DispatchQueue처럼 블록 안에 코드를 임의의 스레드로 처리하는 공통점이 있지만 내부적으로 스레드를 block하지 않는다는 차이점이 있습니다.
async는 함수가 비동기적으로 실행될 수 있음을 나타내며, await는 비동기 작업이 완료될 때까지 기다리겠다는 의미입니다. 예를 들어, 네트워크 요청을 보내고 그 결과를 기다리는 경우, await를 사용하여 결과가 준비될 때까지 기다릴 수 있습니다. 이렇게 하면 코드가 더 직관적이고 읽기 쉬워집니다.
async와 await는 다음과 같이 사용합니다
func asyncFunction1() async { // 선언할 때 괄호 뒤에 async 키워드 달기
// 내용
}
func asyncFunction2() async -> Int { // 반환값이 있는 함수의 경우
// 내용
}
await asyncFunction1() // async 함수 호출 시 함수명 앞에 await 키워드 달기
await asyncFunction2()

sync에서 스레드 관리는 위의 이미지와 같습니다. A(fetchThumnail)함수에서 B(thumbnailURLRequest)함수를 호출하면 A함수가 B함수에게 스레드 권한을 넘기고 B함수가 완료되면 다시 A에게 권한을 돌려줍니다.

위 이미지는 async에서 스레드 관리 이미지입니다. A함수가 B함수를 호출해서 스레드 권한이 A에서 B로 갔습니다. 이때 B함수는 async 함수이기 때문에 스레드 제어권을 포기하는 suspend가 가능합니다. 이때를 Suspension Point라 하고 스레드 제어권은 system으로 넘어가며, system은 스레드를 통해 다른 작업을 수행합니다. 이후 비동기 함수가 완료되면 B함수에게 스레드 권한이 돌아가고 B함수가 완료되면 A함수에게 넘어갑니다.
async let은 비동기 작업을 선언할 때 사용되며, 여러 비동기 작업을 동시에 시작할 수 있게 해줍니다.
// 비동기 함수 정의
func fetchData1() async -> Int {
// 3초 대기 후 데이터 반환
try? await Task.sleep(nanoseconds: 3_000_000_000)
return 5
}
func fetchData2() async -> Int {
// 1초 대기 후 데이터 반환
try? await Task.sleep(nanoseconds: 1_000_000_000)
return 10
}
// 메인 비동기 함수
func main() async {
async let data1 = fetchData1() // 첫 번째 비동기 호출 시작
async let data2 = fetchData2() // 두 번째 비동기 호출 시작
// 두 개의 비동기 호출이 완료될 때까지 기다림
let result1 = await data1
let result2 = await data2
let sum = result1 + result2
print("결과의 합: \(sum)") // 결과 출력
}
// 총 소요 시간: 약 3초 (가장 긴 작업에 맞춰)
TaskGroup은 여러 비동기 작업을 그룹화하여 동시에 실행할 수 있도록 도와줍니다. 이를 통해 여러 작업을 병렬로 처리하고, 각 작업의 결과를 쉽게 관리할 수 있습니다. 이러한 구조적 동시성은 코드의 가독성을 높이고, 오류를 줄이는 데 큰 도움이 됩니다.
// 비동기 함수 정의
func fetchData1() async -> Int {
try? await Task.sleep(nanoseconds: 1_000_000_000) // 1초 대기
return 5
}
func fetchData2() async -> Int {
try? await Task.sleep(nanoseconds: 2_000_000_000) // 2초 대기
return 10
}
func fetchData3() async -> Int {
try? await Task.sleep(nanoseconds: 1_500_000_000) // 1.5초 대기
return 15
}
// 메인 비동기 함수
func main() async {
await withTaskGroup(of: Int.self) { group in
group.addTask {
await fetchData1()
}
group.addTask {
await fetchData2()
}
group.addTask {
await fetchData3()
}
// 모든 작업의 결과 수집
var results: [Int] = []
for await result in group {
results.append(result)
}
// 결과 출력
let sum = results.reduce(0, +)
print("결과의 합: \(sum)") // 결과 출력
}
}
// Task를 통해 main 함수 호출
Task {
await main()
}
// 총 소요 시간: 약 2초 (가장 긴 작업에 맞춰)
구조적 동시성은 부모 작업이 자식 작업의 생명주기를 관리할 수 있도록 하여, 작업 간의 관계를 명확히 합니다. 이를 통해 작업이 완료되지 않은 상태에서 부모 작업이 종료되는 것을 방지할 수 있습니다. 이러한 방식은 코드의 안정성을 높이고, 예기치 않은 오류를 줄이는 데 기여합니다.