[회고] 신입 iOS 개발자가 되기까지 feat. 카카오 자세히보기

🍎 Apple/Swift

[WWDC24] Swift 6로 앱을 마이그레이션하기

inu 2024. 7. 4. 15:07
반응형

https://developer.apple.com/kr/videos/play/wwdc2024/10169/

 

Swift 6으로 앱을 마이그레이션하기 - WWDC24 - 비디오 - Apple Developer

기존 샘플 앱의 업데이트를 따라 Swift 6 마이그레이션 과정을 직접 경험해 보세요. 증분 마이그레이션 방법을 모듈별로 설명하고, 컴파일러로 데이터 레이스 위험이 있는 코드를 식별하는 방법

developer.apple.com

 

주된 내용들만 메모해서 정리했습니다. 전체 내용이 궁금하신 분들은 영상을 감상하시는 것을 추천드려요!


Intro

다른 세션에서 설명한 앱의 아키텍처입니다. main queue, background queue, arbitary queue(헬스킷같은 곳으로부터 나오는 특정 completion handler는 이곳에서 처리된다고 함) 등의 처리를 통해 스레드 사용성을 분리했지만, 어떤 곳에서 어떤 이유로 코드가 실행되는지 명확하지 않았습니다.

 

하지만 Swift concurrency가 도입되고나서는

각 사용성에서 적절한 Actor에서 작업이 처리됨이 명확하고, 각 Actor들은 thread-safe한 value type으로 상호교류를 하게 되어 훨씬 명확한 구분이 가능해졌습니다.

 

하지만 여전히 data race를 피해야하는 문제가 발생하고 있습니다. 만약 refercne type을 mutable state로 전달한다면 프로그램이 충돌하거나 사용자 데이터가 손상될 수도 있습니다.

 

Strict Concurrency Checking

Swift6 Language mode를 사용하면 컴파일러단에서 이러한 문제를 체크하고 방지할 수 있습니다.

특정 객체가 메인액터더라도, 이를 extension으로 확장하여 따르도록 한 프로토콜에서는 메인액터 영역을 보장할 수 없습니다. 프로토콜단에서 메인액터를 사용하고 있지 않으니, 당연한 일입니다.

 

우리가 사용하는 특정 모듈들은 아직 동시성 관련 제약이 제대로 업데이트되어 있지 않을 수 있습니다. C library일수도 있고, 아직 Swift6로 업데이트되지 않았을 수 있습니다.

Build Setting에서 Strict Concurrency Checking을 Complete로 전환하면 이를 체크해서 확인할 수 있습니다.

 

전역변수가 있을 경우 이를 let으로 변경하거나, 특정 액터에 종속되도록 하는 것이 좋습니다. 그렇지 않으면 data race 위험이 있습니다.

방법이 없을땐 우선 nonisolated(unsafe)를 사용하는 것도 방법입니다. 이는 액터의 격리를 무시하게 만듭니다. 경고를 지우는 임시방편이지만 다른 방법(디스패치큐 등)으로 안정성을 보장했다면 이렇게 사용할수도 있습니다.

 

cf. lazy 변수가 초기화될때는 두 스레드가 동시에 접근하려고 하더라도, 원자성이 보장되며 막아준다고 합니다. 몰랐던 사실이네요.

 

delegate나 completion handler로부터 callback을 받았을 때의 동시성

특정 delegate는 특정 스레드에 대한 안정성을 보장해주지만

특정 delegate는 background queues에서 돌며 안정성이 보장되지 않습니다.

 

그럴땐

이렇게 MainActor의 Task를 열어 처리하는 방법도 있고, CaffeineThresholdDelegate 자체를 MainActor로 수정하는 방법도 있습니다. (물론 이는 CaffeineThresholdDelegate를 수정가능한 경우에 한하여 사용할수 있는 방법이겠죠?)

 

만약 특정 액터를 사용함을 확실히 알고 있다면 사용할 수 있는 방법이 있습니다. 예를 들어 문서나, 코드 등을 통해 CaffeineThresholdDelegate가 MainActor에서 실행된다는 것은 안다고 가정해봅시다.

Task를 대신하여 사용한 이 코드는 MainActor에 대한 새로운 Task를 만드는 것이 아닙니다. 이 코드가 이미 MainActor에서 실행되고 있음을 Swift에 알리는 것입니다. 물론 여전히 다른 Actor에서 호출될 가능성이 존재합니다. 그렇게 될 경우 프로그램 실행이 중지됩니다. (...) 하지만 이게 data race로 사용자 데이터를 손상시키는 것보다 낫다고 이야기하네요. 상황에 따라 잘 분간해서 사용해야겠어요.

 

assumeIsolated 대신 @preconcurrency를 사용할수도 있습니다. 역할은 동일합니다. 진입하려는 액터와 같은 액터를 사용한다고 가정하고, 아니면 앱을 폭발시키는 것입니다. 물론 CaffeineThresholdDelegate가 명시적으로 MainActor를 사용한다면 이건 지워도됩니다.

 

Swift6

이제 동시성 관련 에러를 모두 삭제했으니 Swift6를 사용할 준비가 되었습니다.

설정에서 바꾸면 됩니다.

 

이렇게 동시성 이슈를 해결하다보면 많이 당황스러울텐데 걍 이겨내라고 하네요. 수천개에서 수만개까지 에러가 뜰수있지만 대부분 쉽게 해결되니 침착하게 해결하라고..

 

cf. Swift는 internal type의 경우 Sendable을 자동추론하지만, public type은 자동추론하지 않습니다. 이는 클라이언트에게 수정가능성을 열어두고 싶어서라고하네요. 지금은 Sendable이 가능하지만, 나중에 Sendable이 아닌 방향으로 수정될 가능성이 높다면 자동추론이 오히려 위험하다고 판단했나봅니다. 따라서 명시적으로 표시해줘야합니다.

 

아무튼 가능한 것은 다 Sendable로 표기하면 에러가 다 사라질 것이고, 아닌 것은 위험성을 잘 판단해서 nonisolated(unsafe)를 쓰라고 합니다.

 

delegate 추가 예시

특정 동시성 API를 사용하려는데 배포버전이 맞지 않아서 못쓸때가 있습니다. 이럴땐 어쩔수없이 delegate 기반의 API를 써야겠죠.

 

이렇게요... 그런데 CoreLocation Delegate는 어떤 스레드에서 호출되는지 보장되어 있지 않습니다. 문서를 보면 호출 스레드가 아니라 CLManager를 생성한 스레드에 의해 활동 스레드가 결정됨을 알수있습니다. 따라서 이를 생성할때 적절한 스레드에서 생성되도록 해야합니다.

 

전체를 MainActor로 감싸서 생성도 MainActor에서 돌아가도록 처리했습니다.

 

그럼 이런 에러가 뜨긴할텐데

 

아까 배웠던대로 assumIsolated를 쓰면 됩니다.

반응형