Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Swift의 async/await를 사용한 비동기 프로그래밍에 대해 설명해주세요. #40

Open
hamfan524 opened this issue May 13, 2024 · 1 comment
Assignees

Comments

@hamfan524
Copy link
Member

hamfan524 commented May 13, 2024

async/await 문법의 동작 원리와 사용 방법은 무엇인가요?
Task와 TaskGroup을 사용하여 비동기 작업을 관리하는 방법을 설명해주세요.

@hamfan524 hamfan524 self-assigned this May 13, 2024
@hamfan524
Copy link
Member Author

hamfan524 commented May 16, 2024

Swift의 async/await를 사용한 비동기 프로그래밍에 대해 설명해주세요.

async/await이 만들어진 이유부터 알아보도록 하겠습니다.

일단 동기(synchronous) 작업은 해당작업이 끝날 때까지 다른 작업들은 기다리는 작업입니다.

반면 비동기(asynchronous) 작업은 해당작업이 끝나든 말든 나머지 작업을 바로 실행합니다.
일반적으로 이미지 다운로드나 네트워킹 등 작업 시간이 오래 걸리는 것들은 비동기로 처리하는 경우가 많습니다.

이런 비동기로 보낸 작업들이 끝난 시점을 알려주는 대표적인 방법이 completionHandler입니다.

import Foundation

func fetchData(completion: @escaping (Data?, Error?) -> Void) {
    let url = URL(string: "예시 URL주소")!

    URLSession.shared.dataTask(with: url) { data, response, error in
        if let error = error {
            completion(nil, error)
        } else { 
            guard let httpResponse = response as? HTTPURLResponse,
              (200...299).contains(httpResponse.statusCode) else {
                return                          // 여기서 completion 호출하지 않고 바로 return
            }
            completion(data, nil)
        }
    }.resume()
}

비동기 작업에서 completionHandler의 문제점이 있는데 completionHandler는 작업 종료시 항상 completion이 호출 되어야 하는데, 컴파일러가 "여기서 completion을 호출하세요" 라고 알려주지 않기 때문에 개발자들이 직접 상황에 맞게 호출을 해주어야한다는 점입니다.
그런 단점때문에 위 코드에서처럼 에러상황에 completion없이 바로 리턴을 하는 코드가 작성될 가능성이 생기고, 잠재적인 버그가 발생할 수 있게 됩니다.

위와 같은 문제를 해결하기 위해 Swift 5.0에서 ResultType이 나오게 되었으나, 이런 문제 외에도 비동기 작업이 중첩되면 콜백 지옥이 되는 현상까지 자주 볼 수 있었습니다.

위 내용들을 요약하여 completionHandler의 단점을 살펴보자면

  • completion 호출 망각 가능성
  • 오류 처리의 복잡성
  • 비동기 호출간 제어흐름의 복잡가능성
  • 마지막으로 가독성이 떨어짐

Async/Await 도입

위에서 말한 이유들 때문에 Swift5.5부터 Async/Await이 도입되게 되었습니다.

가장 큰 특징은 비동기 코드를 동기 코드처럼 작성할 수 있다는 점입니다.

  1. Async

async 키워드는 비동기 함수를 선언할 때 사용됩니다.
이렇게 선언된 함수는 백그라운드 스레드에서 실행되며, 호출자에게 결과를 반환하기 위해서 메인 스레드를 차단하지 않습니다.

아까 전 completionHandler 코드를 async로 선언한 코드로 수정해보겠습니다.

import Foundation

func fetchData() async throws -> Data {
    let url = URL(string: "예시 URL주소")!

    let (data, response) = try await URLSession.shared.data(from: url)

    guard let httpResponse = response as? HTTPURLResponse,
          (200...299).contains(httpResponse.statusCode) else {
        throw NSError(domain: "HTTPError", code: 0, userInfo: nil)
    }

    return data
}
  1. Await

await 키워드는 비동기 함수의 반환 값을 기다리는데 사용됩니다.

위의 비동기 함수를 호출하는 예시 코드입니다.

Task {
    do {
        let data = try await fetchData()
        // 데이터 처리
    } catch {
        // 에러 처리
    }
}

await 키워드를 제외하면 마치 동기 코드처럼 보이는게 핵심입니다.

이 도입으로 인해 Swift는 작업이 종료될 때, completion Handler 없이도 호출한 곳에 알려주는 것을 보장하고, 개발자는 안전하고, 짧게 코드를 작성할 수 있게 되었습니다.

요약하여 Async/await의 장점을 설명하자면

  1. 코드의 가독성 향상: 비동기 코드를 동기적으로 작성할 수 있어 가독성이 향상됩니다.
  2. 에러 처리 용이성: try-catch 구문을 사용하여 에러 처리가 간편해집니다.
  3. 콜백 지옥 회피: 복잡한 콜백 체인을 피할 수 있습니다.

async/await 문법의 동작 원리와 사용 방법은 무엇인가요?

위에서 대략적으로 설명하였지만, 한 번 더 설명해보겠습니다.

동작원리

  1. 비동기 함수 선언:
    async 키워드로 함수를 선언합니다. 이 함수는 비동기적으로 실행됩니다.
  2. await 사용:
    비동기 함수 내에서 다른 비동기 함수를 호출할 때는 await 키워드를 사용합니다.
    이 키워드는 해당 비동기 함수가 완료될 때까지 기다리고, 결과를 반환합니다.
  3. 비동기 작업 완료까지 대기:
    await를 사용한 줄부터 비동기 작업이 완료될 때까지 함수의 실행이 일시 중단됩니다.
    이때 메인 스레드가 차단되지 않고, 다른 작업을 수행할 수 있습니다.
  4. 비동기 작업 처리:
    비동기 작업이 완료되면 해당 결과를 반환하고, 다음 코드가 실행됩니다.

사용방법

  1. 비동기 함수 선언
    async 키워드를 사용하여 비동기 함수를 선언합니다.
    이때 함수의 반환 타입은 async 키워드 앞에 붙어야 합니다.
func fetchData() async throws -> Data {
    // 비동기 작업 수행
}
  1. await 사용
    비동기 함수 내에서 다른 비동기 함수를 호출할 때는 await 키워드를 사용합니다.
Task {
    do {
        let data = try await fetchData()
        // 데이터 처리
    } catch {
        // 에러 처리
    }
}

Task와 TaskGroup을 사용하여 비동기 작업을 관리하는 방법을 설명해주세요.

Task

Task는 비동기 작업을 나타내는데 사용됩니다.

비동기 코드를 작성할 때 Task를 생성하면 비동기 API를 자유롭게 호출하고 백그라운드에서 작업을 수행할 수 있는 새로운 비동기 컨텍스트에 접근할 수 있습니다.

Task를 사용하면 비동기 코드를 캡슐화할 수 있을 뿐만 아니라 이러한 코드의 실행, 관리 그리고 잠재적으로는 취소되는 방식까지도 제어할 수 있습니다.

일반적인 Task의 사용

let basicTask = Task {
    return "일반적인 Task 사용"
}

print(await basicTask.value)

// 출력: 일반적인 Task 사용

위와 같은 일반적인 사용 외에도 내부적으로 에러를 반환하도록 처리할 수도 있고, 위에서 본 예시들처럼 비동기함수를 실행할 수 있습니다.

TaskGroup

TaskGroup은 여러 개의 비동기 작업을 그룹화하여 동시(병렬적으로)에 실행하고 완료된 작업들의 결과를 처리할 수 있습니다.

다른 설명들보다 예시코드를 보는게 더 좋을거 같아 예시코드를 보겠습니다.

예) 사진 갤러리에서 여러 이미지를 다운로드하는 코드

await withTaskGroup(of: UIImage.self) { taskGroup in
    let photoURLs = await listPhotoURLs(inGallery: "가져올 사진")
    for photoURL in photoURLs {
        taskGroup.addTask { await downloadPhoto(url: photoURL) }
    }
}
  1. 먼저 갤러리의 사진 URL 목록을 다운로드합니다.(이 작업은 Task Group 상태에 영향을 미치지 않습니다.)
  2. 각 사진 URL을 반복하여 동시에 다운로드를 시작합니다.
  3. withTaskGroup에서 모든 사진이 다운로드가 완료되면 메서드가 반환됩니다.

여기서 다른 내용들을 더 심화로 다루기엔 내용들이 너무 많으니,
더 자세한 내용들은 WWDC23 - Beyond the basics of structured concurrency 이 글을 보고 학습하면 좋을 것 같습니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant