Swift

[Swift 5.7] Actor 관련 warning들 원인 파악해보기

Zedd0202 2023. 1. 31. 15:31
반응형

 

Xcode 14.2로 올리고 Actor관련 warning 3종류를 보게 되었다. 

Swift 5.7에 대한 변경사항때문인지는 확실하지는 않지만;; 일단 그런것으로 생각하기로함

일단 내가 파악해본것을 써보려고 한다. 

 

1. Actor-isolated instance method 'testMethod()' can not be referenced from a non-isolated context; this is an error in Swift 6

2. Actor-isolated property 'name' can not be mutated from a non-isolated context; this is an error in Swift 6

3. Cannot access property 'disposeBag' here in non-isolated initializer; this is an error in Swift 6

 

1. Actor-isolated instance method 'testMethod()' can not be referenced from a non-isolated context; this is an error in Swift 6

actor Person {
    
    init() {
        testMethod() // ⚠️ Actor-isolated instance method 'testMethod()' can not be referenced from a non-isolated context; this is an error in Swift 6
    }
    
    func testMethod() {}
    
}

나는 actor의 프로퍼티, 인스턴스 메소드는 전부 isolation하니 initializer도 그런 줄 알았는데, 아닌 것 같다. 

testMethod -> isolation

initializer -> non-isolation

non-isolation한 곳에서 isolation한 메소드를 호출하는것은 암시적으로 비동기로 호출된다고 한다.

Calls to instance method 'testMethod()' from outside of its actor context are implicitly asynchronous

 

해결방법 1 - init을 async하게 만든다. (비동기로 호출되고 있다고 했으니까) 

actor Person {
    
    init() async {
        testMethod()
    }
    
    func testMethod() {}
    
}

 

해결방법 2 - Task & await 조합

Task로 묶어주고..

init() {
    Task {
        self.testMethod()
    }
}

원래 Task로 묶어줘도 async가 아닌 메소드는 await을 안써도 되는 것은 알고있을것이다. 

testMethod는 async메소드가 아니지만.. 암시적으로 비동기로 호출되고 있기 때문에 await을 붙혀주지 않으면 컴파일 에러가 난다.

최종!

actor Person {
    
    init() {
        Task {
            await self.testMethod()
        }
    }
    
    func testMethod() {}
}

 

 

2.  Actor-isolated property 'name' can not be mutated from a non-isolated context; this is an error in Swift 6

actor Person {
    
    var name: String = ""
    let disposeBag = DisposeBag()
    
    init() {
        Observable<Int>.interval(.seconds(1), scheduler: MainScheduler.asyncInstance)
            .subscribe(onNext: { _ in
                self.name = "Zedd" // ⚠️ Actor-isolated property 'name' can not be mutated from a non-isolated context; this is an error in Swift 6
            }).disposed(by: disposeBag)
    }
}

대충 이런 컨텍스트에서 warning이 나고있는데, 설명으로 아래와 같이 나오고 있었다. 

Mutation of this property is only permitted within the actor

이 프로퍼티의 변경은 actor 내에서만 허용된다.. 

처음에는 actor안에서 변경중인데 왜이래;; 하면서 너무 헷갈렸는데, 

저렇게 비동기 컨텍스트를 가지면서 저게 actor바깥에서 실행되기 때문인 것 같다.

actor Person {
    
    var name: String = ""

    init() {
        Task {
            self.name = "zedd" // ⛔️ error! Actor-isolated property 'name' can not be mutated from a non-isolated context
        }
    }
}

Task로 하면 warning이 아니라 error가 뜬다. RxSwift로 짠 코드도 Swift 6가면 에러가 된다고 하니.. 미리미리 수정해두면 좋을 것 같다.

당연히 위의 Task, RxSwift 코드를 actor의 인스턴스 메소드에서 실행하면 아무 warning도 뜨지 않는데, initializer안에서 해서 그런 것 같다. 

 

해결방법 

굳이 initializer에서 name을 바꿔야한다면 

actor Person {
    
    var name: String = ""
    let disposeBag = DisposeBag()
    
    init() {
        Observable<Int>.interval(.seconds(1), scheduler: MainScheduler.asyncInstance)
                   .subscribe(onNext: { _ in
                       Task {
                           await self.setName("Zedd")
                       }
                   }).disposed(by: disposeBag)
    }
    
    func setName(_ name: String) async {
        self.name = name
    }
}

initializer내부에서 수정하는게 아니라 name만 처리하는 인스턴스 메소드 하나 만들어서 처리하기 

 

그게 아니라면 인스턴스 메소드로 하나 빼고 1번의 해결방법으로 수정하기

actor Person {
    
    var name: String = ""
    let disposeBag = DisposeBag()
    
    init() {
        Task {
            await bind()
        }
    }
    
    func bind() {
    	Observable<Int>.interval(.seconds(1), scheduler: MainScheduler.asyncInstance)
            .subscribe(onNext: { _ in
                self.name = "Zedd"
            }).disposed(by: disposeBag)
    }
}

 

 

3. Cannot access property 'disposeBag' here in non-isolated initializer; this is an error in Swift 6

actor Person {
    
    var name: String = ""
    let disposeBag = DisposeBag()
    
    init() {
        Observable<Int>.interval(.seconds(1), scheduler: MainScheduler.asyncInstance)
            .subscribe(onNext: { _ in
                self.name = "Zedd" 
            }).disposed(by: disposeBag) // ⚠️ Cannot access property 'disposeBag' here in non-isolated initializer; this is an error in Swift 6
    }
}

2번과 똑같은 예제에서 disposeBag부분에서 warning이 뜨고 있었다.

자세한 설명을 보면 

After making a copy of 'self', only non-isolated properties of 'self' can be accessed from this init

'self'의 복사본을 만든 후에는 이 initializer에서(== non-isolated initializer) 'self'의 non-isolated 프로퍼티에만 액세스할 수 있다고 나온다. 

subscribe부분에서 self가 캡쳐되어 self의 복사본이 만들어진 후에 actor의 isolated 프로퍼티에 접근하는것이 안되는 것 같다?..

nonisolated 키워드를 붙히고 싶은 욕구가 들지만 computed와 메소드에만 붙힐 수 있다. 즉 date race를 일으키지 않는것들만 붙힐 수 있고,

 var같은 mutable property는 non-isolated가 되면 data race를 일으킬 수 있기 때문에 붙힐 수 없다. 

위 RxSwift 예제 말고 좀 더 쉽게 보면..

actor Person {
    
    var name: String = ""

    init() {
        Task {
            await self.testMethod()
        }

        self.name = "Zedd" // ⚠️ Cannot access property 'name' here in non-isolated initializer; this is an error in Swift 6
    }
    
    func testMethod() {}
}

Task내부에서 self가 복사된 후에 name에 접근하는 이런 흐름.. 

결국 initializer가 non-isolated하기 때문에 이런 일들이 일어나는 것 같다. 

아무튼 3번문제도 2번의 해결방법으로 자연스럽게 해결됨. 

 

SE-0327을 이해하는데 너무 어려웠어서 내가 우아하게 해결한건지는 잘 모르겠는데.. 

(status는 Accepted이나 Xcode 14 Release Notes를 보면 SE-0327에 대한 내용이 일부 구현이 된 것 같다.)

내가 틀린 말을 했거나;; 더 좋은 방법이 있다면 댓글 부탁드립니다. 

 

반응형