Swift Concurrency를 사용해 비동기 코드를 작성하려면 Task를 이용해야합니다.
Task {
await 나는야비동기함수()
}
일단 여기에 넣으라고 하니까 넣어서 쓰던 반성의 시간들,,,(맨날 봐야지 봐야지 하고 프로젝트에 쫓겨 미루던 나,,,🥲) 그래도 기본 동작 정도는 알고 있어야하겠죠? 간단하게나마 Task의 동작 방식을 살펴보겠습니다.
Task의 정의
애플 공식문서에 기술된 Task에 대해 살펴보겠습니다.
그렇습니다. Task는 비동기 작업의 단위라고 합니다.
추측하자면 우리가 지금까지 관성적으로 사용하던 다음과 같은 코드는 모두 하나의 비동기 작업으로 볼 수 있다는 뜻이기도 합니다.
Task {
// 3개 함수 모두 하나의 비동기 단위임!
await 나는야비동기함수1()
await 나는야비동기함수2()
await 나는야비동기함수3()
}
Swift의 비동기 함수(async)는 모두 비동기 컨텍스트에서 호출되어여 합니다. 우리가 평상시에 작성하는 코드(동기 컨텍스트)에서 비동기 작업을 호출하려면 비동기 컨텍스트를 생성해야 하고, 그 방법이 Task를 생성하는 것으로 이해할 수 있습니다.
Task의 주요 특징
1. 하나의 비동기 컨텍스트
func networking() {
// 하나의 비동기 작업 단위에서 실행되기 때문에, 순차적으로 실행.
Task {
await networkClient.fetchContent()
await networkClient.fetchImage()
}
}
Task는 하나의 비동기 컨텍스트를 의미하기 때문에, 각각의 독립적인 자원을 가지고 있습니다. 독립적인 자원 안에서 실행되기 때문에, 클로저 내부의 비동기 코드는 모두 순차적으로 실행됩니다.
2. 다른 Task와의 독립성
func networking() {
// 비동기 작업 1
Task {
await networkClient.fetchImage()
}
// 비동기 작업 2
Task {
await networkClient.fetchContent()
}
}
위 코드의 첫 번째 fetchImage()가 먼저 끝날지, 두 번째 fetchContent()가 먼저 끝날지는 알 수 없습니다. 위 코드는 비동기 컨텍스트가 2개 생성된 상황으로, 각각의 Task는 독립적(병렬적)으로 실행됩니다.
물론 위와 같은 코드는 순서를 보장할 수 없기에, 순서를 보장하며 병렬적인 코드를 작성하고 싶을 때 'Sturctured Concurrency(구조적 동시성)'을 사용할 수 있습니다. 간단히 부모 Task의 이펙트가 자식 Task에 전파되는 방식으로, 자세한 내용은 이후 다뤄보려 합니다.
3. 작업 취소 지원
GCD는 기본적으로 작업 취소를 지원하지 않기 때문에, 플래그 변수를 활용해 이를 구현하는 번거로움이 있었습니다. 하지만 Task는 기본적으로 취소 기능을 지원하기 때문에 쉽게 구현이 가능합니다.
아래 코드는 3초뒤에 작업을 취소하는 간단한 예시입니다.
func performTask() async {
// 1. Task 생성과 동시에 작업 시작
let task = Task {
for i in 1...10 {
// 2. 작업 취소 확인
if Task.isCancelled {
print("작업이 취소되었습니다.")
return
}
// 3. 1초 대기
print("작업 실행 중: \(i)")
try? await Task.sleep(for: .seconds(1))
}
print("작업 완료")
}
// 4. 3초 뒤에 작업을 취소
try? await Task.sleep(for: .seconds(3))
task.cancel()
print("취소 요청 보냄")
}
// 작업 실행 중: 1
// 작업 실행 중: 2
// 작업 실행 중: 3
// 취소 요청 보냄
// 작업이 취소되었습니다.
4. 자동 스레드 관리
Task는 Swift 런타임이 스레드 관리를 대신 수행합니다. 기본적으로 시스템이 최적화한 글로벌 큐에서 코드가 실행되며, 이는 곧 특정 스레드에서 실행되는 것을 보장하지 않는다는 뜻이기도 합니다.
func test() async {
let task = Task {
print("시작 스레드: \(Thread.current)")
try await Task.sleep(for: .seconds(1))
print("종료 스레드: \(Thread.current)")
}
try? await task.value
}
// 시작 스레드: <NSThread: 0x600002244740>{number = 2, name = (null)}
// 종료 스레드: <NSThread: 0x600002240700>{number = 3, name = (null)}
위 코드에서 비동기 코드 이후 다른 스레드로 전환한 것을 확인할 수 있습니다. await 키워드를 만나면서 Task는 현재 스레드의 소유권을 포기했다가(suspend), 작업이 재개(resume)될 때 시스템이 적절한 스레드를 할당해준 것으로 해석할 수 있습니다.
즉, Task 안의 모든 코드가 같은 스레드에서 실행되지 않을 수 있음을 항상 인지하고 있어야겠습니다.
또한 GCD 방식과 다르게, 코어 개수에 따라 최적화된 작업 스케줄링을 수행하여 성능을 높이고 리소스를 효율적으로 사용한다고 합니다.(이 부분은 조금 더 딥한 부분이라 추후 살펴보면 좋을 것 같습니다)
정리
정말 간단히 Task에 대해 알아봤습니다. 구조적 동시성, 작업 스케줄링 등 알아야 할 내용이 더 많지만 요번 포스팅은 Task의 정의를 다시 한번 복기하며 마무리하겠습니다.
Task는 Swift Concurrency의 핵심 요소로, 비동기 작업의 단위입니다.
'iOS' 카테고리의 다른 글
캐플 리팩토링 두 번째 이야기 - 프로젝트 세팅하기 (0) | 2025.01.30 |
---|---|
구조체로 싱글톤 만들기? (0) | 2025.01.30 |
캐플 리팩토링 첫 번째 이야기 - 방향성 설정하기 (0) | 2025.01.22 |
MVC와 Cocoa MVC, 뭐가 다를까? (0) | 2025.01.16 |
인생 첫 번째 iOS 개발 면접 후기 (1) | 2025.01.14 |