[ swift ] 비동기 처리 방식인async/await 에 대하여

sonny·2024년 12월 24일
1

TIL

목록 보기
81/140

Swift의 async/await는 비동기 프로그래밍의 새로운 패러다임이다.

라고 한다는데, 확실히 비동기 작업은 현대 소프트웨어 개발에서 필수적이다.

네트워크 요청, 파일 입출력, 데이터베이스 접근 등 시간이 오래 걸리는 작업은 메인 스레드를 차단하지 않고 수행해야 하는데, Swift에서는 비동기 작업을 클로저(closure)나 콜백(callback)을 통해 처리했지만, 이건 코드 가독성을 떨어뜨리고 유지보수를 어렵게 만드는 단점이 있다고 한다.

이를 해결하기 위해 Swift 5.5에서는 async/await 키워드를 도입해서 비동기 코드를 더 읽기 쉽고 유지보수하기 쉽게 만들었다고 하니 한번 알아보자!


async/await란?

async/await는 비동기 작업을 처리하는 새로운 방식이다.

이 개념은 다른 언어들(JavaScript, Python 등)에서도 사용되기에 Swift에서도 비슷한 철학으로 구현되었다고 한다.

핵심 키워드

  1. async: 함수 또는 메서드가 비동기적으로 동작한다는 것을 나타내는데, 이 함수는 호출 시 작업이 완료될 때까지 기다림이 필요하다.

  2. await: 비동기 함수의 작업이 완료될 때까지 실행을 일시 중단하고 결과를 반환을 받는다. 이 키워드는 비동기 함수 안에서만 사용할 수 있다.

작동 원리

async 함수는 호출 즉시 실행되지 않는다.

대신에 호출자는 이 함수의 작업이 완료되길 기다릴 수 있는데, await 키워드를 통해 작업 결과를 받을 수 있고,

내부적으로 Swift는 이 작업을 비동기적으로 처리해서 적절한 시점에서 코드 실행을 재개하게 된다.


왜 async/await가 필요할가?

기존 Swift에서는 비동기 작업을 처리하기 위해 다음과 같은 방법을 사용했다고 한다.

클로저 기반 비동기 코드

클로저를 사용하여 비동기 작업을 처리할 수 있엇지만, 중첩된 클로저(콜백 지옥ㅠ)가 발생하여 코드가 복잡해질 수 있다.

func fetchData(completion: @escaping (String) -> Void) {
    DispatchQueue.global().async {
        // 비동기 작업 수행
        sleep(2) // 2초 대기
        completion("데이터 로드 완료")
    }
}

fetchData { result in
    print(result)
}

이 방식은 간단한 작업에는 적합할 수 있지만 작업이 중첩되거나 에러 처리 로직이 추가되면 가독성이 급격히 떨어지는 코드라고 한다.

콜백 지옥이 발생할 수 있는 상황이란

콜백 지옥은 여러 비동기 작업을 중첩해서 처리할 때 발생하는데, 여러 개의 비동기 함수에서 각각 완료 후 다른 비동기 작업을 호출하는 경우에 콜백이 중첩되어 복잡해진다고한다.

중첩된 콜백 지옥

func fetchData(completion: @escaping (String) -> Void) {
    DispatchQueue.global().async {
        sleep(2)
        completion("데이터 로드 완료")
    }
}

fetchData { result1 in
    print(result1)
    fetchData { result2 in
        print(result2)
        fetchData { result3 in
            print(result3)
            // 이런 식으로 계속 중첩되면 콜백 지옥이 된다.
        }
    }
}

위 코드에서는 fetchData 함수가 중첩되어 호출되고 있고 각각의 completion 클로저 안에서 또 다른 fetchData 호출이 일어나고 잇다.

이렇게 되면 코드가 점점 더 복잡해지고, 읽기 어려워져서 관리하기 힘들어지는데, 이것이 콜백 지옥의 전형적인 예시라고 볼 수 있다.
.
.

아무튼!

async/await의 두두등장

async/await를 사용하면 아까 보았던 위 코드와 같은 비동기 작업을 마치 동기 코드처럼 작성할 수 있게 된다.

func fetchData() async -> String {
   try await Task.sleep(for: .seconds(2)) // 2초 대기
   return "데이터 로드 완료"
}

Task {
    let result = await fetchData() // fetchData 함수 호출
    print(result) // 결과 출력
}

(마치 된 것 같아 동기적)

동기적으로 작성된 것처럼 보이지만, 실제로는 비동기적으로 동작한다.

이 코드가 비동기적으로 동작하는 이유를 이해하려면, 비동기 작업과 async/await의 동작 방식을 알아야 한다.

코드 분석을 해보면,

1. fetchData

  • fetchData 함수는 async 함수로 선언됐고 따라서 호출할 때 await를 사용해야 한다.

  • Task.sleep(for: .seconds(2))는 비동기적으로 2초 동안 기다린다는 의미인데, 이 코드를 사용하면 프로그램의 실행을 지연시킬 수 있지만, 메인 스레드는 차단되지 않고 다른 작업을 계속 진행할 수 있다.

  • 2초 대기 후 "데이터 로드 완료"를 프린트한다.

2. Task

  • Task 블록은 비동기 작업을 실행할 수 있는 새로운 컨텍스트를 생성하는데,

  • let result = await fetchData()

    • 이 호출은 fetchData의 작업이 완료될 때까지 기다리지만, 현재 Task 블록 내의 다른 작업(후속 작업)이 비동기적으로 실행될 수 있도록 한다는 것이다.

    • await는 해당 비동기 작업이 완료되기 전까지 대기하지만, 호출한 스레드는 차단되지 않는다.

3. 실행되는 흐름을 보면,

  1. Task 블록이 실행되면서 fetchData()를 호출하고,
  2. fetchData는 2초 동안 비동기적으로 대기(Task.sleep).
  3. 2초 후에 "데이터 로드 완료" 문자열이 반환되고, result에 저장.
  4. print(result)가 실행되어 "데이터 로드 완료"가 출력 완료.

위 코드가 동기적으로 보였던 이유

코드가 동기적으로 보이는 이유는 await를 사용했기 때문인데,

await는 함수 호출부에서 결과를 기다리는 것처럼 보이게 만든다.

하지만 이는 동기적인 차단(blocking)이 아니고, 다른 작업과 병렬적으로 실행될 수 있다.

Task {
    let result = await fetchData() // 여기서 대기하는 것처럼 보임.
    print(result) // fetchData의 결과를 기다린 후 실행.
}

보기에 직관적이지만 비동기로 동작하고, Task 내부는 백그라운드에서 실행된다.


여기서 중요한 점

  • Task.sleep은 스레드를 차단하지 않고 대기 상태로 유지한다는 것.
  • 비동기 호출로 인해 메인 스레드는 차단되지 않기에 UI 업데이트와 같은 작업이 계속해서 원활히 진행될 수 있다는 것.

async/await의 주요 특징

1. 가독성 향상

async/await는 비동기 작업을 순차적으로 작성할 수 있어 코드가 직관적이고 간결한데, 이걸 통해서 복잡한 비동기 로직도 쉽게 이해할 수 있게 된다.

2. 에러 처리의 단순화

do-catch 구문과 결합하여 비동기 작업의 에러를 처리할 수 있고 기존 클로저 기반 비동기 코드에서는 에러 처리를 위한 별도의 로직이 필요했지만, async/await를 사용하면 동기 코드에서와 동일한 방식으로 처리할 수 있다.

func fetchMovieData() async throws -> String {
    let url = URL(string: "https://api.example.com/movies")!
    let (data, _) = try await URLSession.shared.data(from: url)
    return String(data: data, encoding: .utf8) ?? "데이터 변환 실패"
}

Task {
    do {
        let movieData = try await fetchMovieData()
        print(movieData)
    } catch {
        print("에러 발생: \(error)")
    }
}

3. 메인 스레드와 백그라운드 스레드 간 전환이 간편

기존의 GCD(Grand Central Dispatch)나 OperationQueue를 직접 사용할 필요가 없어 async/await는 적절한 스레드에서 작업을 재개한다.


도입 시 고려 사항이라고 한다.

  1. 지원 버전: async/await는 Swift 5.5 이상, iOS 15 이상에서 사용할 수 있는데, 더 낮은 버전을 지원해야 한다면 대체 방법이 필요하다.

  2. 호환성: 기존 클로저 기반 비동기 코드와 함께 사용할 때 호환성을 고려해야 한다.

  3. 학습 곡선: async/await 개념은 간단하지만, 기존 GCD나 OperationQueue와의 차이점을 이해하는 데 시간이 걸릴 수 있다.
    (나도 아직 차이를 잘 이해하지 못했다)


음...

async/await으로 비동기 작업을 좀더 직관적이고 간결하게 처리할 수 있다는 점에서 도움이 된 것 같다.

기존의 콜백 기반 비동기 코드에 비해 코드를 보면 뭔가 흐름이 자연스럽게 이어가는 것 같아 가독성도 더 좋아진 것 같고,

그리고 Task.sleep처럼 비동기적으로 대기할 수 있는 메서드를 사용하니까 스레드를 차단하지 않고 다른 작업을 진행할 수 있다는 점도 신기했다.

참 공부할 것이 많은 swift ,,,,,,

profile
iOS 좋아. swift 좋아.

0개의 댓글

관련 채용 정보