...
본문 바로가기

swift

앞으로 swift6에 대응하기 위한 swift concurrency 느낀점

 

동기가 빌려준 Kodeco (구 레이웬더리치) 에서 나온 Modern Concurrency in swift를 읽었다. 

그동안 대충 개념만 인지하고 async, await, Task, MainActor.run 정도만 썼었는데, 좀 더 개념에 대해 알아야할것 같아서 읽게됐다. 

 

https://liam777.tistory.com/48 동기가 이 블로그를 운영하는데, 위에 나온 Kodeco를 리뷰하면서 동시에 공식문서까지 읽으면서 정리했다.  각 챕터별로 다 정리해놔서 Kodeco 금액이 부담스럽다면 여기 블로그를 참고하면 굉장히 좋을것 같다. 

 

그래서 나는.. 정리하는 리뷰는 하지않고, 느꼈던 점들을 작성하려고 한다!

또, 이 개념을 바탕으로 swift6 대응하도록 리팩토링도 하면서 느꼈던 부분도 작성하려고 한다. 

 

 

swift conccurrency 느낀점 

쓰레드 개념이 얕아진다.

swift-concurrency는 GCD의 대항마로 swift 스러우면서 개발자가 손쉽게 작성하기위해 만들어졌다고 한다. 

그러다보니 GCD로 접했던 사람들이라면 (나처럼) 쓰레드에 대해 생각을 했는데, 이 책을 읽을수록 얕아졌다. 

swift-concurrency가 쓰레드 로우개념보다 엑터 라는 상위 개념으로 바꿔서 사용한다라고 생각한다. 

그래서 swift-concurrency를 사용하면 내부적으로 쓰레드풀 을 관리하고, cpu가 가용가능한 쓰레드수를 넘지않도록 한다고 한다. 그래서 더더욱 개발자들이 쓰레드에 대한 고민을 덜하도록 하고자 한다. ( 근데 GCD도 내부적으로 쓰레드풀 관리한다고...ㅎㅎ ) 

어떤쓰레드인지는 swift concurrency가 알아서 결정해준다. 메인쓰레드로 하고싶다면, MainActor로 하고.

 

 

@MainActor와 actor는 다르다

물론 둘다 같은 actor이다. actor는 내부적으로 serial하게 실행할 수 있는 executor를 가진다. 따라서 이 actor에 접근하는 모든 task들은 serial하게 동작하게되어, 동시성에서 안전하게 사용될 수 있다. 그래서 @MainActor는 내부적으로 메인쓰레드에서만 serial하게 실행될수있고, 그래서 많은 UI들이 @MainActor가 되었다.

 

하지만 @MainActor는 좀 더 컴파일단에서 편의기능이 많다. 

@MainActor끼리는 async메소드가 없어도 await 구문없이도 사용이 가능하다는게 차이다.

 

BackupStore와 ViewModel은 서로 @MainActor이여서 ViewModel-useBackup 메소드안에서 await없이도 사용이 가능하다.

actor로 표기하는 순간부터 해당 actor에 serial executor로 isolated되기 때문에, 바로 접근할 수 없지만, @MainActor끼리는 가능하다.  

MainActor가 아닌, 다른 타입(StructViewModel)에서 접근한다면 당연하게도 컴파일에러가 난다. 

 

 

isolated 의 의미 ? 

actor-isolated, Main actor-isolated 란 이유는 actor가 serial 하게 실행될 수 있는 executor가 있으니 isolated, 즉 serial하게 실행될수있도록 격리되어있다라고 이해하고있다. 

 

그래서 struct, class, Task, TaskGroup, 클로저들은 non-isolated이다. 

그래서 isolated한, 격리된 타입을 접근하기위해서는 await 와 같이 구분해줘서 따로 실행될 수 있다라고 컴파일러에게 알리는것으로 이해하고 있다. 

그래서 swift6 대응하다보면,  기존코드에서 요런 컴파일에러도 많이보곤했다.  

  • Main actor-isolated property 'customButton' can not be referenced from a nonisolated context 
  • 요건 아래 swift 6대응 에서 다루려고한다 

 

 

Task와 Task.detached의 차이

일단 둘의 공통점은,  컨텍스트를 제공하는 top-level 이다. top-level에 대해서는 다음 내용에서 다룰거다. 

가장 큰 차이는, Task는 현재 컨텍스트를 그대로 상속받는다는점이고, 

Task.detached는 독립적인 컨텍스트를 생성한다. 

컨텍스트가 추상적인데, 내가 이해한 바로는 actor의 컨텍스트, 즉 특정 actor에 isolated된 영역을 이어받는다라고 이해했다.  실행할때의 priority도 상속받는다. 다음 내용에서도 다룰건데, 상위 Task를 상속받지않는다. 

 

아래 코드처럼, 동일한 코드를 Task { } 안에서 사용한건 컴파일에러가 안나고, Task.detached { } 는 컴파일 에러가 난다. 

애초에 ViewModel이 @MainActor이므로,  같은 @MainActor인 BackupStore를 사용하는건 문제가 없었고, Task { } 또한 컨텍스트를 그대로 상속받기 때문에 컴파일에러가 안난다. 

 

 

Task 안에 Task 는 캔슬이 되지 않는다. 

아까 위에서 언급한 top-level이여서, Task를 생성하면 가장 꼭대기 계층에 있게 된다. 

swift-concurrency의 장점중 하나인 작업들을 계층구조로 만들수 있다. 

상위 작업이 cancel된다면, 하위 작업들도 cancel이 된다. Task라고 하지않고 작업이라고 한 이유는, await 들이 붙은 녀석들을 작업이라고 의미했다. 

하지만 Task는 top-level이므로,  Task { Task { } } 이런 식은, 계층구조가 형성되지않고, 각자 다른 top-level을 가지게된다. 

  • if you nest syntactically two or more tasks (in other words they are netsted visually in your code) that doesn't create a task hierarchy at runtime, they will all be top-level tasks. 

 

아래와같은 방법은 쓰지않겠지만.... 이게 메소드로 분리되어있고, 잘 인지하지 못한다면 충분히 가능성있다 (내가그랬..) 

 

따라서 task를 캔슬한다고해도 그다음 Task들은 캔슬되지않는다. 

만약 캔슬되게 하고싶다면, TaskGroup을 이용해야한다! 

 

그래서 Task { } 는 최대한 앞단에서 한번만 쓰는식으로 해야할것 같다. 

 

 

 

Swift 6 대응하면서 느낀점

기존에 대충 쓰던 Task, await, async 들은 후환이 두렵다

swift concurrency의 장점중 하나가 actor, await, async 등의 구문으로 컴파일러가 많이 추론할수가 있다. 그말은 컴파일단에서 판단하여 최적화를 더 할 수 있다는 여지를 주는것이다. (물론 최적화가 된다고 한다) 

하지만... Swift6에서는 굉장히 엄격해졌다. 그전까지는 대충 써도 괜찮았다...

 

async, await 읽기도 편하고 좋네, 트렌드를 따르는것 같은 기분을 주어서 막 남용하다가.. Swift6로 바꾸는순간 어질어질했다. 

그래서 최근에 작업한내용들만이라도 기억에 남아있을때 지금이라도 해야겠다 싶어서 대응했는데, 어중간한게 쓰다가는 나중에 정말 swift6 대응할때 힘들것 같다. 정말 개념을 제대로 이해해서 사용해야 뒷탈이 없을것 같다. 

 

 

Task { } 안에서 self를 참조하는 경우는 무조건 self가 Sendable 이여야한다. 

아닛...?  그동안 좋았잖아..우리...

 

Task의 클로저가 아래처럼 @isolated(any)이다.  요건 swift6에 바꼈는데, 즉 @Sendable인데

 

Sendable은 동시성에서 값을 안전하게 전달할수있는 것이고,  self를 참조하게 된다면, self가 Sendable이여야한다. 

그래서 아래와같은 코드는 컴파일에러가난다... 그래서 Sendable인 Struct로 바꾸던가, Sendable을 채택하거나, actor로 바꾸거나 해야한다.  아니면...다음에서 다룰 @MainActor로 하거나. 

 

 

 

ViewModel은 @MainActor여야겠다. 

actor로 하게되면, 제약사항이 생긴다. #selector를 못쓴다. swiftUI는 상관없겠지만, UIKit에서 쓴다면 #selector를 써야하는 경우엔 쓸수없다. 

그러면 class & Sendable이면 되겠지만,  이경우에는 내부 프로퍼티가 변경되면 안된다! 

struct로 바꾸자니, @Published를 못쓴다!  (가지가지..)

뭔가 @MainActor를 남발하는게 뭔가 부담스러웠는데, @MainActor가 답인것 같다. 

생각할수록 괜찮은게 ViewModel은 결국 View에서 사용되니까, MainActor가 전혀이상하지않고,  @MainActor - @MainActor의 편리성 이점도 누릴 수 있으니까 괜찮은것 같다. 

 

 

protocol은 @MainActor이거나 Sendable을 채택해야겠다. 

protocol로 의존성을 분리한 경우들을 ViewModel에서 사용한다면, Sendable하도록 하는게 정신건강에 좋을것 같다. 

View에서 Delegate패턴으로 protocol을 쓴경우는 @MainActor로 하는게 마찬가지로 정신건강에 좋을것 같다. 

  • Main actor-isolated instance method 'makeView()' cannot be used to satisfy nonisolated protocol requirement.
  • 이런 에러가 발생할텐데,   async로 붙이게되면, 결국 앞단에 Task를 추가하거나, 끝없는 async를 추가해야하는데, 그것도 골치아프고... 

 

앞으로 싱글톤 사용할거면, @gloablactor로 만들어야겠다. 

일단 컴파일단에서 static 이런거 있으면 귀신같이 찾아내서 컴파일에러 발생시키는데, 암튼, 

지금까지 datarace 없도록 lock을 걸거나, queue를 사용하거나 했지만 이제는 마법같은 단어 actor로 쉽게 할수있으니, 편할것 같긴한다.  ( 믿음직스럽진 못하지만.. 따라야지 뭐.. ) 

 

 

awakeFromNib은 NSObject이므로 MainActor따로 컴파일해줘야한다. 

UIViewController 네 ? @MainActor지 ?  그럼 당연히 모든 메소드들이 @MainActor일거라고 생각했지만...

awakeFromNib에서 이런 컴파일에러가 나와서 당황스러웠다. 

  • Main actor-isolated property 'someLabel' can not be referenced from a nonisolated context

 

awakeFromNib은 NSObject의 메소드로,  NSObject는 MainActor로 되어있지않다. 

그래서 MainActor가 아닌 non-isolated 메소드이므로 위에서 설명한, actor-isolated에 참조할수없게된다. 

 

검색해보니, 메인쓰레드에서 동작할거라고 하기때문에,  가정하는 코드로 대응해두었다. ㅡ____ ㅡ 

 

 

 

Xcode 버전별로 SDK 다르므로 컴파일에러가 사라질수도 있다. 

AppKit에서 겪었던건데, Xcode16에서는 컴파일에러가 나는데, Xcode16.1에서는 컴파일에러가 안났다. 

왜그런고 하니, Xcode16은 macOS 15.0 SDK였고, Xcode16.1은 macOS 15.1 SDK였다. 

 

그래서 직접 Xcode에서 커맨드 클릭 눌러서 코드보면, Xcode16.1에서 @MainActor가 붙은걸 발견할수있었다. -_____ - 

 

 

struct에서 Task를 사용한다면..  주의하자

swift 6 대응하면서 거의 Sendable하면서 값싼 struct를 적극 활용하려고 하다보니까 이런컴파일러를 봤다. 

  • Escaping closure captures mutating 'self' parameter

 

Task  { } 구문도 클로저이고, struct는 클로저안에서 self를 참조를 못한다고 한다... (몰랐..)

 

self로 캡처해서 하면 컴파일에러가 사라지긴 한다. struct라 이슈도 없을것 같지만... 

 

struct이므로! 캡처하게되면 새로운 복사본이 생성될거고 ? 만약 내부적으로 상태관련 프로퍼티들이 있다면, 다른 값이 되지않을까 하는 우려가 생겼다.  또 파트원분한테 말씀드리니, 메모리도 2배생길수도 있겠다라고 조언도 해주시고, 근데 COW로 실제로 메모리가 2배까진 안될것 같긴한데. 암튼. 

 

간단한 메소드가 아니라면, class로 바꾸는게 낫지않을까 싶다. 

이게 어떻게 보면 struct대신 class를 사용해야하는 또다른 이유가 되지 않을까 싶기도  ? ? 

 

 

deinit 에서... 

deinit에서 self를 사용하는 코드가 굉장히 많아서 이게 참 골치아프다. 

이건 좀 더 봐야되는데,  deinit은 어느쓰레드에서나 호출될수있다고 해서, 쉽사리 기존코드 유지하면서 간단한 방법이 떠오르지가 않는다.

앞으로는 deinit에 있는 self관련된 코드들은 제거하는게 나아보이긴 한다.  ViewWillDisappear라던가, 이걸로 다 옮기기에는 코드도 다 봐주기도 해야하고, 크흠........ 

 

 

 

앞으로

이렇게 11월 목표중 하나를 이루었다. swift concurrency 보는게 내목표였다. 

이 책만으로 어느정도 이해는 간것같지만, 관련 wwdc도 봐야겠다. 

 

 

12월은 swiftUI 공부가 목표다.  앞으로 swiftUI를 써먹어야해서 봐야만한다.

오랜만에 Kodeco를 봐서그런지 옛날에 공부하던 기분도 들고, 레이웬더리치에 좋았던 기억도 떠올라서 Kodeco의 SwiftUI by Tutorials 를 결제했다.  지금한 챕터3까지 봤는데, 오우.. 아는게 힘이고, 아는만큼 보인다. 재밌다! 

 

이제 곧 올해도 지나가니, 아주 오랜만에 회고도 작성해봐야겠다. 올해는 너무 뿌듯한 한해였으므로!