Task는 항상 부모 Context를 상속 받을까?

2025. 7. 22. 11:18·내게 필요한 개발 공부

회사에서 대망의 첫 MR을 작성하게 됐습니다.(Gitlab은 MR이라고 하더군요,,, Github의 PR) 작업 상 크게 어려운 부분은 없었지만, Swift Concurrency를 이용한 코드 작성 중 관련 리뷰를 받게 되어 간단한 실험을 해볼까 합니다.

 

어떤 상황?

간단히 말하면 Task로 비동기 작업을 생성할 때, 내부 동작은 항상 부모 Context의 메타데이터를 상속 받을까? 입니다.

final class SomeViewController: UIViewController {
    
    // ...
    
    func someFunction() {
        Task {
            // ⭐️ Task 내부 클로저는 어느 스레드에서 실행?
        }
    }
}

 

정확한 상황을 표현하자면, UIKit의 ViewController는 @MainActor로 마킹되어 있기 때문에 항상 메인스레드에서의 동작을 보장하게 되며, 내부 함수 등에서 생성한 Task는 이에 따라 메인스레드에서 동작하게 된다! 입니다.

// UIViewController 공식 문서
@available(iOS 2.0, *)
@MainActor open class UIViewController : UIResponder ... {

 

하지만 GPT에게 언제나 상속 받는 것을 보장할 수 없다,,,? 와 같은 이야기가 들려와서 혼란이 와버렸고, 어느정도 직접 실험해봐야 신뢰할 수 있는 코드를 작성할 수 있겠다 판단했습니다.

 

?????

 

간단한 실험 해보기

첫 번째 실험은 함수 내부에 하나의 Task를 생성 후 메인스레드 여부를 검증하는 것입니다. 모든 실험은 SwiftUI의 View, UIKit의 UIViewController 내부에서 진행했습니다. 예상할 수 있듯, 첫 번째 테스트는 무난히 통과했습니다.

struct ContentView: View {
    
    var body: some View {
        Text("실험실")
            .onAppear {
                test()
            }
    }
    
    func test() {
        Task {
            if !Thread.isMainThread {
                fatalError("메인스레드가 아니잖아?!")
            }
        }
    }
}

 

두 번째 실험은 여러 개의 Task를 생성 후 검증하는 것입니다. 이 또한 통과했습니다.

struct ContentView: View {
    
    var body: some View {
        Text("실험실")
            .onAppear {
                test()
            }
    }
    
    func test() {
        (1...10000).forEach { _ in
            Task {
                if !Thread.isMainThread {
                    fatalError("메인스레드가 아니잖아?!")
                }
            }
        }
    }
}

 

세 번째 실험은 자식 Task를 생성 후 검증하는 것입니다. 요상하게 생기긴 했지만 결국 가장 마지막 Task까지 메인스레드에서 동작함을 확인할 수 있었습니다.

struct ContentView: View {
    
    var body: some View {
        Text("실험실")
            .onAppear {
                test()
            }
    }
    
    func test() {
        Task {
            Task {
                Task {
                    Task {
                        Task {
                            if !Thread.isMainThread {
                                fatalError("메인스레드가 아니잖아?!")
                            }
                        }
                    }
                }
            }
        }
    }
}

 

마지막으로 어떻게든 실패 케이스를 찾고 싶어 이것저것 넣어본 코드입니다. 비동기 작업 여러 개 생성 + 스레드 제어권 양보(yield) 까지 진행해봤지만, 역시 메인스레드의 동작을 보장합니다.

struct ContentView: View {
    
    var body: some View {
        Text("실험실")
            .onAppear {
                test()
            }
    }
    
    func test() {
        Task {
            (1...10000).forEach { i in
                Task {
                    try await Task.sleep(for: .seconds(5))
                    if !Thread.isMainThread {
                        fatalError("메인스레드가 아니잖아?!")
                    }
                }
            }
        }
    }
}

 

이로써 혹여나 싶은 마음에 @MainActor 어노테이션을 붙여준다거나, await MainActor.run {} 과 같은 코드들 없이도 메인스레드에서의 동작을 보장할 수 있음을 어느정도 확신할 수 있었습니다.

 

결론 정리!

  • @MainActor 어노테이션이 적용된(SwiftUI View, UIKit UIViewController) 곳 내부에서 기본적으로 생성한 Task는 메인스레드에서의 동작을 보장한다.
  • 즉, 부모 Context가 MainActor의 격리되어 있고, 자식 Task가 분리(detached) 혹은 다른 actor에서 실행되지 않는 한 메인스레드에서의 동작을 보장한다.

 

실패 케이스

마지막으로 실패할 수 밖에 없던 케이스 몇 개를 남겨놓습니다!

struct ContentView: View {
    
    var body: some View {
        Text("실험실")
            .onAppear {
                test()
            }
    }
    
    func test() {
        Task.detached { // detached는 메타데이터를 상속 받지 않음.
            if !Thread.isMainThread {
                fatalError("메인스레드가 아니잖아?!")
            }
        }
    }
}
struct ContentView: View {
    
    var body: some View {
        Text("실험실")
            .onAppear {
                test()
            }
    }
    
    func test() {
        Task { @TestActor in // 다른 Actor를 마킹했으므로 당연히 메인스레드가 아님.
            if !Thread.isMainThread {
                fatalError("메인스레드가 아니잖아?!")
            }
        }
    }
}
struct ContentView: View {

    @State private var viewModel = ViewModel()

    var body: some View {
        Text("실험실")
            .onAppear {
                viewModel.test()
            }
    }
}

@Observable
final class ViewModel {

    func test() {
        Task { // Task가 생성된 ViewModel은 따로 actor가 지정되어 있지 않기 때문에 메인스레드 아님.
            if !Thread.isMainThread {
                fatalError("TestActor는 메인 스레드에서만 실행되어야 합니다.")
            }
        }
    }
}
저작자표시 (새창열림)

'내게 필요한 개발 공부' 카테고리의 다른 글

함수와 메서드는 다르다.  (4) 2025.08.01
멀티태스킹과 멀티프로세싱, 비슷한듯 다른 두 개념  (0) 2025.03.17
안전한 놀이터 샌드박스 알아보기  (2) 2025.02.26
iOS에서 OS 뜯어보기  (0) 2025.02.26
스마트폰의 CPU, AP 알아보기  (1) 2025.02.24
'내게 필요한 개발 공부' 카테고리의 다른 글
  • 함수와 메서드는 다르다.
  • 멀티태스킹과 멀티프로세싱, 비슷한듯 다른 두 개념
  • 안전한 놀이터 샌드박스 알아보기
  • iOS에서 OS 뜯어보기
thinkyside
thinkyside
스스로에게 솔직해지고 싶은 공간
  • thinkyside
    또 만드는 한톨
    thinkyside
  • 전체
    오늘
    어제
    • 모아보기 (60)
      • 솔직해보려는 회고 (1)
      • 꾸준히 글쓰기 (9)
      • 생각을 담은 독서 (6)
      • 내게 필요한 개발 공부 (24)
      • 트러블슈팅 (4)
      • 프로젝트 일지 (8)
      • 개발 서적 (3)
      • 취준 (3)
      • 대외활동 (1)
      • UXUI (1)
  • hELLO· Designed By정상우.v4.10.3
thinkyside
Task는 항상 부모 Context를 상속 받을까?
상단으로

티스토리툴바