[iOS] swift concurrency의 continuation알아보기

Youth·2023년 10월 2일
0

TIL

목록 보기
12/21

안녕하세요!!
이번엔 swift concurrency의 continuation이라는 주제로 찾아온 킴스캐슬입니다

이번이 제 블로그의 99번째 포스팅이더라고요

물론 갯수에 연연하지는 않지만 iOS개발을 하면서 최소한 99번의 고민을 했고 답을 찾아서 기록으로 남긴거잖아요? 그런의미에서 뿌듯하네요 ㅎㅎ뿌듯 그 잡채 100번째 주제는 뭘로할까 고민을 해보겠습니다!

다시 본론으로 넘어가서 요즘 WWDC스터디를 하면서 swift concurrency에 대해서 알아가는 중인데 알면알수록 재미있는 친구인거같아요

그런 의미에서 제가 예전에는 이해하지 못했지만 왜인지 지금에서는 이해를 하게된 swift의 continuation이라는 주제를 가져오게 되었습니다

그럼 시작해보겠습니다

swift concurrency

swift concurrency가 뭔지에 대해서 간단하게 설명을 드리면 우리가 보통 앱을 만들때 비동기를 고려해야하는데요

그래서 네트워크 통신을 할때 우리가 비동기적으로 코드를 작성하죠? 가장 대표적으로는 completion handler를 활용하는 방식이 있습니다. 예를 들면 이런코드가 일반적이죠

func findWeatherInfo<T: Codable>(place: String, returnType: T.Type, completion: @escaping (NetworkResult<T>) -> Void) {
    let dataRequest = AF.request(makeUrl(place: place), method: .get)
    dataRequest.responseData { response in
        switch response.result {
        case .success:
            guard let statusCode = response.response?.statusCode else { return }
            guard let value = response.value else { return }
            let networkResult = self.judgeStatus(by: statusCode, value, changeData: T.self)
            completion(networkResult)
        case .failure:
            completion(.networkErr)
        }
    }
}

이런 방식을 GCD를 이용한 방식이라고 보통 이야기를 합니다

GCD(Grand Centeral Dispatch)는 현재 iOS 개발에서 주로 사용되고 있는 동시성 프로그래밍 API입니다.

swift concurrency도 결국은 동시성 프로그래밍 API인건 동일합니다. swift concurrency는 WWDC 2021에서 새로 소개된 동시성 프로그래밍 API로 Swift Concurrency는 동시성 프로그래밍을 가독성이 좋은 깔끔한 코드로 작성하고자 도입된 개념이라고 할 수 있습니다

결국 GCD나 swift concurrency나 동시성프로그래밍을 하기위한 API라는점은 동일합니다 다만 사용방법이나 형태가 다를뿐인겁니다

그럼 그냥 쓰던 completion handler를 쓰면 안되나요?

당연히 GCD방식을 사용해도 코드는 잘 작동합니다
하지만 모든 기술의 등장에는 배경이 존재하고 그 배경이 기술선택의 근거가 됩니다

swift concurrency의 장점은 여러가지가 있지만 이 포스팅은 swift concurrency의 장점을 소개하는 글이 아니기때문에 자세히 이야기하진 않을 예정입니다

하지만 swift의 concurrency에 대한 wwdc스터디가 끝나면 정리해서 올려보겠습니다...ㅎㅎ


Coninuation

swift concurrency에서는 continuation이라는 중요한 방식? 개념?이 등장하는데요
async한 메서드를 호출할 때 swift concurrency에서는 thread의 제어권을 포기하는 suspended라는 현상이 발생합니다.

그러다 보니 다시 제어권을 돌려 받았을때 어디서부터 실행할지를 아는것이 필요한데 async(비동기)함수의 경우 heap에 suspension point에서 실행하는데 필요한 함수 컨텍스트들을 저장해서 제어권을 돌려받았을때 어디서실행할지를 알수있게 됩니다. 이것을 continuation이라고 부릅니다

heap에다가 저장하는 이유는 어떠한 변수가 await의 suspension point이전에 정의되었는데 await이후에 사용되는 경우는 해당 변수가 사라지면안되고 저장이되어야합니다. stack에 저장하게 된다면 다른 await메서드에 의해 replace되는 경우에 변수자체가 사라지기때문에 이러한 변수(suspension point이전에 정의되고 suspension point 이후에 사용되는 변수)를 추적하기 위해서 heap의 async frame에 저장되게 됩니다

continuation을 통해 일시정지된 함수의 상태를 추적해 어디서부터 재개할지 알 수 있습니다, 이후에 사용될 변수중에 이전에 정의된 변수의 상태가 어떠했는지를 heap의 async frame을 통해 알 수 있게 됩니다

기존 GCD에서는 thread가 block되면 task를 다른 thread로 보내는 full thread context switching이라는 꽤나 비용이큰 현상이 발생하는데 continuation을 활용하면 Full Thread Context Switching대신 function call정도의 비용으로 비동기 메서드를 호출하고 실행할 수 있게됩니다

GCD와 swift concurrency를 공부할때 기억해야할점은 swift Concurrency와 GCD는 상호 배타적이지 않다는겁니다. 또한 swift concurrency를 사용하면서 GCD도 여전히 필요한 경우도 있을 수도 있습니다

예를 들어, swift concurrency 내에서 GCD를 사용하여 특정 task를 실행 하거나, swift concurrency로 작성된 코드를 기존 GCD 기반의 프로젝트에 통합하는 등의 상황이 있을 수도 있는거죠

우리가 자주쓰는 라이브러리들의 경우엔 swift concurrency로 비동기 메서드가 작성되어있지 않을 가능성이 높습니다 라이브러리는 넓은 범위의 버전에서 호환이 가능해야하는데 swift concurrency는 비교적 최근에 업데이트되어서 사용가능한 API이기 때문이죠...

이제부턴 기존 GCD방식의 비동기 메서드를 continuation을 이용해서 swift concurrency로 동작할수있게 만드는 방법에 대해서 알아보도록 하겠습니다

withCheckedContinuation

제목은 withCheckedContinuation지만 프로젝트 특성상 error를 throw해줘야하기 때문에 설명은 withCheckedThrowingContinuation라고 하겠습니다 throwing이 없으면 throw가 아닌 함수고 throwing이 있으면 throw함수입니다

예를 들어보겠습니다
지금 제가 진행하고 있는 프로젝트는 swift concurreny를 이용해서 비동기 메서드를 호출하고 실행하는데요 image load와 cache를 위해 kingfisher라는 라이브러리를 도입하게 되었습니다

그 중에서도 retrieveImage라는 메서드를 사용하려고 봤더니

completion handler로 비동기 메서드를 수행하는 GCD방식인걸 알게되었습니다
사실 외부라이브러리가 아니라 내부 모듈이었다거나 아니면 그냥 팀원이 작성한 코드였다면 메서드 호출 자체를 swift concurrency방식으로 바꿔버리면 되었을수있겠지만 외부라이브러리의 소스코드라 바꿀수도 없고 난감한 상황에 빠지게 됩니다

지금과 같은 이런상황에서 사용할 수 있는 것이 withCheckedContinuation입니다

결국은 retrieveImage라는 메서드를 통해서 completion에 RetrieveImageResult라는 값을 넣어주는데 라이브러리에서 찾아보니 아래와 같은 구조를 가지고 있었습니다

결국은 이 비동기 메서드를 통해 만들어진 RetrieveImageResult라는 구조체에서 image라는 값을 return시켜주면 UIImage를 얻을수 있게되는겁니다

차근차근 코드를 작성해보겠습니다
우선 우리가 swift concurrency를 활용해서 UIImage라는 data를 얻고싶은거니까

func fetchImage(with urlString: String) async throws -> UIImage?

이런 메서드를 만드는걸로 시작해보겠습니다

여기서 결국 return될 UIImage가 GCD로 작성된 메서드의 completion handler의 input으로 들어가야할 값인거죠?

fetchImage(with urlString: String)라는 메서드의 return으로 completion handler의 비동기 호출값을 return시켜주면 됩니다

withCheckedThrowingContinuation를 통해 CheckContinuation의 T값이 return되게 됩니다

그리고 값을 return시키기 위해서는 resume(returning:)을 사용하고 error를 throw하기 위해서는 resume(throwing:)을 사용하라고 합니다

전체적인 코드를 보면 아래처럼 사용할 수 있습니다

애초에 withCheckedThrowingContinuation자체가 클로저를 받아서 T라는 generic을 return해주는 async throw메서드이기때문에 try await으로 호출하고 그값을 return 해주는 부분이 빨간색 네모 부분입니다

그리고 클로저내부에서 CheckedContinuation객체의 resume메서드를 사용해 실제 value(T)와 error를 return 해주는 부분이 파란색 네모입니다

🔥withCheckedThrowingContinuation를 사용할때 주의해야할 점
1. continue에서 두 번 이상 resume(returning:)을 호출하면 안됩니다
2. resume을 호출하지 않으면 Task가 무기한 일시 중단된 상태로 유지됩니다.

조금 헷갈리실 수 있는데 정리를 해보면 withCheckedThrowingContinuation라는 메서드를 사용해서 클로저 내에서 GCD 비동기 메서드를 호출하고 withCheckedThrowingContinuation의 parameter로 들어가는 closure의 input인 CheckedContinuation의 resume을 활용해 값(혹은 error)을 return해주게 되는 코드입니다

이렇게 사용하게되면 kingfisher의 GCD방식의 메서드를 swift concurrency 방식으로 호출 할수있게됩니다


만약에 리팩터링을 진행하려하는데 목표가 swift concurrency를 도입하려한다면 아마 기존의 GCD방식의 completion hadler를 활용한 비동기호출 코드를 어떻게 할지를 고민하는 단계를 거치리라 생각됩니다

두 가지 정도의 선택지가 존재하겠네요

  1. 기존의 GCD코드를 전부 swift concurrency 방식으로 교체한다
  2. withCheckedContinuation을 활용해 기존 GCD방식의 코드를 swift concurrency방식으로 wrapping한다

어떤 선택을 하더라도 명확한 이유가 있다면 맞는선택이겠지만
WWDC에서 swift concurrency영상을 보면서 느낀점은 swift concurrency가 thread관리 측면에서 장점이 존재하고 개발을 할때 실수를 줄일수 있고 코드의 가독성을 높여준다는 장점또한 가지고 있기때문에 1번방식을 우선순위로 생각하지 않을까 싶네요!

사용법과 장점 정도를 포스팅하려했던거 같은데 막상 포스팅을 하다보니 길어진거같네요 ㅠㅠ
그래도 이번 포스팅을 통해서 swift concurrency와 아주 조금이라도 친해지셨으면 좋겠습니다!

그럼 다음 100번째 포스팅으로 찾아뵙겠습니다
그럼 20000!

profile
AppleDeveloperAcademy@POSTECH 1기 수료, SOPT 32기 iOS파트 수료

0개의 댓글