Task란 무엇인가) Task의 동작 Case Study

SteadySlower·2023년 9월 8일
0

iOS Development

목록 보기
32/38

안녕하세요. 지난번 포스팅에서는 Task의 정의와 구현 방법에 대해서 알아봤는데요. 이번 포스팅에서는 Task를 다양하게 실행해보면서 좀 더 공부해보도록 하겠습니다.

예제 코드 소개

먼저 다운로드 비동기 함수를 하나 mocking 해보았습니다. 2초 후에 String을 리턴합니다. 에러가 발생하면 nil을 리턴합니다. (실무에서는 에러를 던지는 경우가 더 많습니다. 예시 코드에서는 편의를 위해서 이렇게 했습니다.)

참고로 Task.sleep의 경우 현재 Task를 취소하게 되면 error를 throw합니다. 때문에 do, catch문으로 감쌌습니다.

이 함수를 실행하고 취소해보면서 이번 포스팅을 진행해보겠습니다.

func downloadString() async -> String? {
    do {
        try await Task.sleep(nanoseconds: 2 * 1_000_000_000) //👉 2초간 대기 (다운로드 mocking)
        return "다운로드된 String"
    } catch {
        return nil
    }
}

Case 1. Task의 실행 시점

아래 코드에서 Task 객체는 만들어지기만 했을 뿐 명시작으로 실행 여부를 지시하지 않았는데요. 지난 포스팅에 설명한대로 Task 객체는 만들어지자 마자 바로 주어진 클로저를 실행합니다.

func downloadString() async -> String? {
    print("다운로드 시작")
    do {
        try await Task.sleep(nanoseconds: 2 * 1_000_000_000) //👉 2초간 대기 (다운로드 mocking)
        return "다운로드된 String"
    } catch {
        return nil
    }
}

Task {
    return await downloadString()
}

/*
🖨 출력 결과
다운로드 시작
*/

해당 Task의 참조를 변수에 할당하는 경우에도 완전히 동일하게 동작하는 것을 볼 수 있습니다.

func downloadString() async -> String? {
    print("다운로드 시작")
    do {
        try await Task.sleep(nanoseconds: 2 * 1_000_000_000) //👉 2초간 대기 (다운로드 mocking)
        return "다운로드된 String"
    } catch {
        return nil
    }
}

let downloadTask = Task {
    return await downloadString()
}

/*
🖨 출력 결과
다운로드 시작
*/

2. Task를 취소한 경우 value

아래 코드를 보겠습니다. 일단 Task 객체를 만들고 다운로드가 시작되면 “다운로드 시작”이 출력됩니다. 그리고 2초 후에 “다운로드 끝”이 출력되고 “다운로드된 String”이 리턴이 될 것으로 예상할 수 있습니다. 실행을 해보면 예상대로 출력결과가 나옵니다.

func downloadString() async -> String? {
    print("다운로드 시작")
    do {
        try await Task.sleep(nanoseconds: 2 * 1_000_000_000) //👉 2초간 대기 (다운로드 mocking)
        print("다운로드 끝")
        return "다운로드된 String"
    } catch {
        return nil
    }
}

let downloadTask = Task {
    return await downloadString()
}

print(await downloadTask.value)

/*
🖨 출력 결과
다운로드 시작
...2초 후...
다운로드 끝
Optional("다운로드된 String")
*/

그렇다면 취소한다면 어떻게 될까요? 당연히 nil이 리턴이 될 것입니다. 그리고 Task.sleep에서 에러를 던지므로 그 아랫줄인 “다운로드 끝”를 출력하는 코드는 실행되지 않을 것입니다.

func downloadString() async -> String? {
    print("다운로드 시작")
    do {
        try await Task.sleep(nanoseconds: 2 * 1_000_000_000) //👉 2초간 대기 (다운로드 mocking)
        print("다운로드 끝")
        return "다운로드된 String"
    } catch {
        return nil
    }
}

let downloadTask = Task {
    return await downloadString()
}

downloadTask.cancel()
print(await downloadTask.value)

/*
🖨 출력 결과
다운로드 시작
nil
*/

Case 3. Task의 value를 여러번 호출하는 경우

이런 경우도 있을 수 있을 것 같습니다. 아래와 같이 value를 여러번 호출하는 경우입니다. 이런 경우 그 때마다 다운로드를 다시 실시할까요?

예상하셨겠지만 정답은 “Task는 생성될 때 1번만 실행한다”입니다. 그리고 Task의 참조는 그 return 값을 저장하고 있다가 호출할 때마다 다시 실행할 필요 없이 그 값만 리턴합니다.

func downloadString() async -> String? {
    print("다운로드 시작")
    do {
        try await Task.sleep(nanoseconds: 2 * 1_000_000_000) //👉 2초간 대기 (다운로드 mocking)
        return "다운로드된 String"
    } catch {
        return nil
    }
}

let downloadTask = Task {
    return await downloadString()
}

print(await downloadTask.value)
print(await downloadTask.value)
print(await downloadTask.value)

/*
🖨 출력 결과
다운로드 시작
Optional("다운로드된 String")
Optional("다운로드된 String")
Optional("다운로드된 String")
*/

Case 4. 이미 실행을 마친 Task를 취소하는 경우

아래 코드는 2초가 걸리는 다운로드 비동기 작업을 3초 후에 취소합니다.

이미 다운로드를 마친 Task를 취소하는 경우에는 어떻게 될까요? 어쨌든 취소가 되었으니까 nil이 리턴될까요?

정답은 아닙니다. cancel()은 아직 실행을 마치지 않은 Task에만 영향을 끼칩니다. 이미 실행을 마쳤다면 아무런 영향이 없습니다.

func downloadString() async -> String? {
    do {
        try await Task.sleep(nanoseconds: 2 * 1_000_000_000) //👉 2초간 대기 (다운로드 mocking)
        return "다운로드된 String"
    } catch {
        return nil
    }
}

let downloadTask = Task {
    return await downloadString()
}

try! await Task.sleep(nanoseconds: 3 * 1_000_000_000)
downloadTask.cancel()

print(await downloadTask.value)

/*
🖨 출력 결과
Optional("다운로드된 String")
실행 종료
*/

Case 5. URLSession과의 비교

아마 거의 모든 분이 URLSession.shared.dataTask(with:)을 통해서 네트워크 통신을 해보셨을텐데요. 이 객체는 URLSessionDataTask를 리턴하는데 우리가 다루고 있는 Task 객체와 비슷한 점이 몇가지 있습니다.

비동기 작업을 위해서 사용하고 .cancel()을 통해서 취소할 수 있습니다.

다만 실행하기 위해서는 resume()를 실행해야 하고 전달하는 클로저가 completionHandler이기 때문에 데이터 통신이 끝나고 나서 실행된다는 것입니다. (await를 기다렸다가 실행된다고 비교할 수 있겠네요.)

let url = URL(string: "https://www.google.com")!
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
    print(data, response, error)
}

task.resume()
task.cancel()

1:1 대응을 할 수는 없지만 비동기를 이해하는데는 참고하면 큰 도움이 될 것 같습니다.

마치며…

비동기를 다루는 것은 어렵습니다. 하지만 필수적인 분야입니다. 다행히 Swift의 업데이트 방향을 보면 이 부분을 최대한 개발자 친화적으로 바꾸려고 노력하는 것 같습니다.

Task는 아주 쉽게 concurrency 환경을 제공합니다. Combine 처럼 참조를 유지할 필요도 없고 async/await와 함께 사용하면 간결하게 코드를 만들 수 있습니다.

다음 포스팅에서는 @MainActor에 대해 알아보겠습니다.

profile
백과사전 보다 항해일지(혹은 표류일지)를 지향합니다.

0개의 댓글