[Swift] default value 격리 규칙의 변화 - nonisolated에서 isolated로
이번엔 인간 98 % + AI 2 % 정도의 노력으로 작성된 글 ◠‿◠
들어가기 전에
개-하!! MainActor 함수 useDefault 의 default value 로 MainActor context 값을 세팅할때 각각 어떤 결과가 발생할까여
@MainActor let immutableValue = 1
@MainActor var mutableValue = 1
@MainActor func myFunc() -> Int { return 1 }
// Case 1. 정상? 에러?
@MainActor func useDefault(value: Int = immutableValue) { }
// Case 2. 정상? 에러?
@MainActor func useDefault2(value: Int = mutableValue) { }
// Case 3. 정상? 에러?
@MainActor func useDefault3(value: Int = myFunc()) { }5.. 4…3… 2…1…결과는…!
바로 Swift 5 의 Concurrency Checking 을 minimal 로 돌리냐, Swift 6 로 돌리냐에 따라 달라집니다
왜 에러?
Swift 5 — minimal 에서 함수의 default value 는 항상 격리되지 않은(nonisolated) 상태입니다. 따라서 @MainActor에 격리된 함수의 default value 더라도, @MainActor에 격리된 값을 호출 하는 것을 금지합니다.
따라서 아래 코드는 안전하지만 MainActor 으로 격리된 myFunc() 함수를 동기-비격리 context 에서 실행할 수 없다 는 에러가 발생합니다.
@MainActor func myFunc() -> Int { return 1 }
@MainActor func useDefault(value: Int = myFunc()) {
// ❌ Call to main actor-isolated global function 'myFunc()' in a synchronous nonisolated context
}Swift 6 에서 왜 해결?
[SE-0411] Isolated default value expressions에서는 default value 가 자신을 감싸고 있는 함수와 같은 격리를 공유하도록 허용하는 새로운 규칙을 제시합니다.
따라서 아래와 같은 코드도 Swift 6 (혹은 Swift 5 + Concurrency Chekcing complete) 에서는 에러가 발생하지 않습니다.
@MainActor func requiresMainActor() -> Int { return 1 }
@MainActor func useDefault(value: Int = requiresMainActor()) {}requiresMainActor() 가 MainActor context 를 요구하지만, useDefault 함수의 default value 또한 Main Actor context 를 제공하고 있기 때문입니다.
비슷한 맥락으로 만약 함수가 비격리된 상태라면, default value 표현식들도 비격리입니다. 따라서 아래와 같이 MainActor 에 격리된 코드를 default value 로 넣으면 Swift 6 에서는 아래와 같은 에러가 발생합니다.
@MainActor func requiresMainActor() -> Int { return 1 }
func myNonisolatedFunc(value: Int = requiresMainActor()) {
// ❌ Main actor-isolated default value in a nonisolated context
}참고로 같은 코드를 Swift 5 (minimal) 에서 실행하면 비슷하게 nonisolated context 에서 main actor 함수를 실행할 수 없다는 에러가 뜹니다
❌ Call to main actor-isolated global function 'requiresMainActor()' in a synchronous nonisolated context그러면 MainActor 함수의 default value 로 다른 actor 의 함수를 호출하면 Swift 5 (minimal) 과 Swift 6 에서는 각각 어떤 에러가 발생할까요?
actor MyActor {
var mutableValue: CGFloat = .zero
}
@MainActor func useDefault(value: Int = MyActor().mutableValue) {}- 💥 Swift 5 (minimal) —
Actor-isolated property 'mutableValue' can not be referenced from a nonisolated context- 비격리 context 에서 다른 actor 로 격리된 값을 호출 할 수 없다고 나옵니다 - 💥 Swift 6 —
Actor-isolated default value in a main actor-isolated context: main actor context 에서 다른 actor 로 격리된 값을 호출할 수 없다고 나옵니다
보너스 1 — 같은 let인데 선언 위치에 따라 (전역 / 인스턴스) 왜 warning 이 다를까?
방금까지가 default value 의 isolated 에 대한 핵심 내용이었고요!
추가로 Swift 5 (minimal) 에서 전역 상수도 main actor 함수의 default value 로 할당해봤던거 기억 나시나요?
@MainActor let immutableValue = 1
@MainActor func useDefault(value: Int = immutableValue) {}이때도 var / func 호출과 비슷한 문구를 확인할 수 있었지만, 차이점은 에러가 아닌 단순 워닝이 발생한다는 것이었습니다.
그런데 신기한건 @MainActor let 을 전역 상수가 아닌, 인스턴스 속성으로 넣으면 이런 워닝조차 발생하지 않는다는 건데요!
class MyClass {
@MainActor let immutableValue: Int = 1
}
@MainActor func useDefault(value: Int = MyClass().immutableValue) { }let 이면 똑같이 동작해야하는거 아닌가...? 싶어서 왜 그런지는 AI 한테 물어봤습니다. (검증은.. 알아서들 하시길! ◠‿◠ )
🤖
이 현상의 핵심은 데이터의 불변성이 아닌, 액터 격리 경계를 넘는 행위에 대한 컴파일러의 판단 차이입니다.
- 전역
let에 경고가 뜨는 이유
전역 변수 @MainActor let immutableValue = 1은 명시적으로 @MainActor에 의해 격리된 공유 상태로 선언됩니다. Swift 5의 'Minimal Concurrency Checking' 모드는 nonisolated 컨텍스트에서 이러한 전역 액터 격리 경계를 넘어 접근하는 것 자체를 잠재적 위험으로 간주합니다.
비록 값이 let이라 데이터 경쟁은 없지만, 컴파일러는 immutableValue가 "액터의 보호를 받는 공유 상태 (MainActor)"에 대한 직접적인 접근임을 감지하고, 경고로 미리 알려줍니다. 이는 데이터의 안전성보다는 액터 격리 규칙 위반에 초점을 맞춘 경고입니다.
2. 클래스 내 let에 경고가 뜨지 않는 이유
class MyClass {
@MainActor let immutableValue: Int = 1
}
@MainActor func useDefault(value: Int = MyClass().immutableValue) { }위 코드에서 MyClass 자체는 @MainActor로 격리되지 않았습니다. useDefault 함수의 default value는 MyClass의 새로운 인스턴스를 생성하고(MyClass()), 그 인스턴스의 속성에 접근하는 것입니다.
Swift 5의 컴파일러는 비격리된 컨텍스트에서 생성된 로컬 객체의 불변 속성에 대한 안전한 접근으로 해석합니다. 이 접근은 MainActor 의 보호를 받는 공유 상태를 건드리는 것이 아니라, 단지 로컬 객체의 값을 읽는 것이기 때문에, Minimal 모드에서는 경고를 발생시킬 만큼 위험하다고 판단하지 않습니다.
보너스 2 — 컴파일러가 생성하는 init 의 격리 수준
🤔 음 일단 ㅇㅋ! Swift 5 에서 그럼 MyClass 자체를 @MainActor 로 만들면 initializer 도 MainActor 에 격리된 상태일테니까 에러가 발생하겠지?
@MainActor
class MyClass {
let immutableValue: Int = 1
}
@MainActor func useDefault(value: Int = MyClass().immutableValue) { }안 발생함여 ㅋ
왜냐?? initializer 가 MainActor 에 격리된 상태가 아니니까여;;
😮 어째서??????????? type 자체에 @MainActor 달아버리면 그 안에 있는 함수나 프로퍼티도 동일한 context 에 격리되는거 아니야???
명시적으로 정의를 하면 그런데요..!! 만약 저장 속성들이 모두 Sendable 타입이고 기본 이니셜라이저 표현식들이 별도의 액터 격리를 요구하지 않는다면, 컴파일러가 생성하는 이니셜라이저는 비격리(nonisolated) 상태가 됩니다.
@MainActor struct MyView {
var value: Int = 0 // @MainActor 추론됨
/* 컴파일러가 만드는 initializer 는 'nonisolated'
nonisolated init() {
self.value = 0
}
nonisolated init(value: Int = 0) {
self.value = value
}
*/
}만약 저장 속성에 class 같은 non-Sendable 타입이 들어간다? 그러면 컴파일러가 생성하는 이니셜라이저는 type의 격리 수준을 따릅니다.
class NonSendable {}
@MainActor struct MyModel {
var value: NonSendable = .init() // @MainActor 로 추론됨
/* 컴파일러가 만드는 initializer 는 @MainActor
@MainActor
init(value: NonSendable = .init()) {
self.value = value
}
*/
}그래서 다시 돌아와서! 왜 여기까지 왔냐!! 다시 짚어보자면
- Swift 5 (
minimal) 에서 아래 코드를 단순히 비격리된 컨텍스트에서 생성된 객체(MyClass())의 불변 속성에 대한 안전한 접근으로 해석해서 에러가 발생하지 않는거라면
class MyClass {
@MainActor let immutableValue: Int = 1 // Sendable
}
@MainActor func useDefault(value: Int = MyClass().immutableValue) { } // ✅ 정상2. 객체를 격리된 컨텍스트에서 생성 하면 에러가 발생하겠네?
3. MyClass 자체를 @MainActor 로 만들면 initializer 도 MainActor 에 격리된 상태일테니까 에러가 발생할듯? → 근데 왜 에러 발생안하지?
@MainActor
class MyClass {
let immutableValue: Int = 1
}
@MainActor func useDefault(value: Int = MyClass().immutableValue) { } // ✅ 정상4. type 안에 속성이 모두 Sendable 이라서 컴파일러에서 자동으로 만드는 initializer 가 nonisolated 구나..
@MainActor
class MyClass {
let immutableValue: Int = 1
/* 컴파일러가 만드는 initializer 는 'nonisolated'
nonisolated init() {
self.immutableValue = 1
}
nonisolated init(immutableValue: Int = 1) {
self.immutableValue = immutableValue
}
*/
}5. 하지만 속성에 non-Sendable 타입이 있으면 컴파일러가 생성하는 initializer는 type의 격리 수준을 따르기 때문에? 이렇게하면 객체를 격리된 컨텍스트에서 생성 할 수 있고?
class MyAnotherClass {}
@MainActor
class MyNonSendableClass {
let immutableValue: MyAnotherClass = MyAnotherClass() // non-Sendable
}6. 이제 아까와 똑같이 let immutableValue 을 default value 에서 접근해보면? → 에러가 나는군 ㅇㅇ
@MainActor func useDefault(value: MyAnotherClass = MyNonSendableClass().immutableValue) {
// ❌ Main actor-isolated default value in a nonisolated context
}추가로 만약 initializer 만들때 별도의 액터 격리가 필요하다면 이런 에러가 발생하는데요,, (Swift 5 에서는 워닝, Swift 6 에서는 에러)
@MyGlobalActor func requiresMyGlobalActor() -> Int { return 1 }
@MainActor
class MyClass {
// ⚠️ Default initializer for 'MyClass' cannot be both main actor-isolated and global actor 'MyGlobalActor'-isolated; this is an error in the Swift 6 language mode
@MyGlobalActor let immutableValue: Int = requiresMyGlobalActor()
}초기화에 대한 자세한 내용은 [SE-0411] Isolated default value expressions 의Stored property initial values 와 Stored property isolation in initializers 참고하시길!
(문서 읽을때 초기화 쪽은 대충 읽고 넘겼는데.. 이게 이렇게 연관되어있을 줄이야 ◠‿◠ …)
그럼 20000!
