일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- RxSwift
- SwiftUI
- Creating Operators
- ios14
- Operater
- RxCocoa
- @State
- Operators
- @EnvironmentObject
- graphql
- init?
- @Binding
- NavigationLink
- @Environment
- Xcode
- operator
- swift6
- RFC1738/1808
- vim
- typeorm
- IOS
- nonisolated
- init
- SWIFT
- subject
- Bug
- nestjs
- dismiss
- NullObject
- URL(string:)
- Today
- Total
Tunko Development Diary
Swift6 migration 정리 본문
Swift6 데이터 레이스
데이터 경쟁은 한 스레드가 메모리에 접근하는 동안 다른 스레드가 동일한 메모리를 변경할 때 발생합니다. Swift 6 언어 모드는 컴파일 시점에 데이터 경쟁을 감지하고 방지하여 이러한 문제를 해결합니다.
Swift에서 사용하는 async/await와 actor는 다른 언어의 유사한 기능들과 표면적으로 비슷해 보입니다. 하지만 내부 동작 방식이나 사용 방법에 중요한 차이가 있을 수 있으므로 혼동하지 않도록 주의해야 합니다.
Data Isolation
Swift의 동시성 시스템은 컴파일러가 모든 가변 상태의 안정성을 이해하고 검증할 수 있도록 합니다.
이는 데이터 격리라는 메커니즘을 통해 이루어집니다. 데이터 격리는 가변 상태에 대한 상호 배타적 접근을 보장합니다. 이는 개념적으로 잠금과 유사한 동기화 형태입니다.
그러나 잠금과 달리 데이터 격리가 제공하는 보호는 컴파일 타임에 발생합니다.
swift에 노출된 Objective-c 유형은 안전한 사용을 보장하기에 충분한 정보를 컴파일러에 제공하지 못할 수 있습니다. 이러한 상황을 수용하기 위해 격리 요구사항을 동적으로 표현할 수 있는 추가 기능이 있습니다.
정적이든 동적이든 데이터를 격리하면 컴파일러가 작성한 swift코드가 데이터 경합 없이 작성되도록 보장할 수 있습니다.
Isolation Domain
격리 영역(Isolation Domain)은 프로그래밍에서 공유하는 데이터나 상태가 엉망이 되는 것을 막아주는 보호 장치라고 생각할 수 있습니다. 특정 코드 블록이나 변수를 한 번에 한 곳에서만 안전하게 사용하도록 제한하는 방식입니다. 이 방식 덕분에 여러 곳에서 동시에 접근해서 데이터가 꼬이거나 충돌하는 일을 방지할 수 있습니다.
고립 영역을 이해할 때는 세 가지 중요한 상태 구분이 있습니다:
- 비격리(Non-isolated): 특정 보호 장치에 의존하지 않는 상태로, 여러 곳에서 접근될 수 있습니다.
- 액터 격리(Actor-isolated): 상태나 변수가 특정 액터(작업 단위)에 의해 보호되는 상태입니다. 액터는 코드의 특정 부분만 이 데이터를 수정할 수 있도록 격리시키는 역할을 합니다.
- 글로벌 액터 격리(Global Actor-isolated): 여러 영역에서 통합적으로 보호하기 위해 글로벌 액터에 의해 관리되는 상태입니다.
쉽게 말해, 고립 영역은 데이터가 여러 곳에서 동시에 변경되어 충돌하거나 오류가 나는 상황을 막기 위해 만들어진 보호 구역입니다.
Non-isolated
비격리 상태는 특정 보호 영역(격리 영역 Actor 등) 에 포함되지 않은 상태를 뜻합니다.
func test() {
// 비격리 상태
}
이 함수는 특정 고립 영역에 속하지 않아서 non-isolated 상태 입니다. 이 상태에서는 비격리 상태의 다른 함수나 변수에는 마음대로 접근할 수 있지만, 다른 고립 영역 (예: actor) 에는 접근할 수 없습니다.
일반적으로 class 등도 비격리 상태 입니다.
Actor와 고립 영역
액터는 특별한 고립 영역을 만들어 , 그안에 있는 데이터를 보호하는 역할을 합니다. 액터의 모든 속성은 해당 액터 인스턴스에 고립되므로, 다른 코드가 액터의 데이터를 직접 변경하지 못하게 보호됩니다.
actor Orchard {
var fruits: [Fruit]
var waterSupply: [WaterSource]
func addFruit() {
fruits.append(Fruit())
}
}
이 Island 액터는 각 인스턴스마다 독립된 고립 영역을 가지고 있습니다. addToFlock메서드는 flock 속성에 접근 할 수 있지만, 외부에서는 Island의 데이터에 접근할 수 없습니다. 이를 통해 동시성 문제를 방지합니다.
비격리 메서드 사용(nonisolated)
액터 안에 있으면서도 고립 규칙을 무시하고 싶다면, 메서드를 nonisolated로 정의 할 수 있습니다.
actor Orchard {
var fruits: [Fruit]
var availableWater: [WaterSource]
nonisolated func isSuitableForGrowth() -> PlantType {
// 여기서는 fruits나 availableWater에 접근할 수 없음
}
}
이 nonisolated 메서드는 Orchard 액터 안에 있지만, 고립 영역에 있는 데이터에 접근하지 않고 실행됩니다.
여기서 말하는 고립 영역은 fruits과 availableWater 변수를 의미합니다.
(isolated) 고립 파라미터 사용
함수에서 특정한 액터의 데이터를 안전하게 접근하고 싶을 때는 고립 파라미터를 사용합니다.
func addFruit(to orchard: isolated Orchard) {
orchard.fruits.append(Fruit())
}
Global Actor
Global actor는 일반 액터와 유사하지만, 특정 코드 그룹 전체가 하나의 고립된 영역을 공유할 수 있도록 설계되었습니다. 이를 통해 다양한 타입이 서로 공유하는 데이터와 상태를 단일 보호 영역 내에서 안전하게 사용할 수 있습니다.
Swift에서 글로벌 액터를 (@) 으로 표시해서 특정 고립 영역에 할당할 수 있습니다.
@MainActor
class FruitOrchard {
var fruits: [Fruit]
var waterSources: [WaterSource]
}
때때로 특정 메서드를 고립에서 제외하고 싶을 때가 있습니다. nonisolated 키워드를 사용하면 해당 함수는 글로벌 액터의 고립 규칙에서 벗어납니다. 하지만, 고립된 데이터에는 접근할 수 없게 됩니다
@MainActor
class FruitOrchard {
var fruits: [Fruit]
var waterSources: [WaterSource]
nonisolated func isSuitableForGrowth() -> PlantType {
// 여기서는 fruits, waterSources와 같은 MainActor에 의해 보호된 상태에는 접근할 수 없습니다.
}
}
nonisolated 키워드를 사용하면 고립을 무시할 수 있지만, 해당 함수에서는 고립된 데이터를 접근할 수 없습니다.
MainActor의 명시적 컨텍스트 전환
MainActor.run은 비동기 환경에서 특정 코드 블록을 MainActor 고립 영역 안에서 실행하도록 강제하는 메서드입니다. 이 메서드는 특히 기존 코드가 MainActor에 의해 고립되지 않았을 때 유용합니다. MainActor로 전환해야 하는 부분을 명시적으로 지정함으로써, 코드가 올바른 고립 영역에서 실행되도록 보장합니다.
MainActor 사용 예시
class PersonalTransportation {
// 아직 @MainActor가 붙지 않은 클래스
}
await MainActor.run {
// 이 블록 안에서는 MainActor에 의해 고립됨
let transport = PersonalTransportation()
// 이 블록 내에서 다른 MainActor 고립 상태의 코드와 안전하게 상호작용 가능
}
주의할 점
- MainActor.run은 임시 조치로 생각할 수 있습니다. 특히 기존 코드가 완전히 고립되지 않은 상태에서 고립 영역을 준수하도록 하는데 도움을 줍니다.
- 하지만, 최종적으로는 클래스 자체에 @MainActor를 명시하여 고립 상태를 자동으로 처리하도록 하는 것이 이상적입니다. 이는 컴파일러가 해당 클래스를 MainActor에 고립된 상태로 간주해, 필요할 때 자동으로 고립을 전환해 줄 수 있기 때문입니다.
따라서 MainActor.run 은 기존 코드의 마이그레이션 중 잠시 사용하는 도구로 이해할 수 있으며, 궁극적으로는 @MainActor를 클래스나 함수에 명시하여 정적 고립을 적용하는 것이 목표입니다.
Unmarked Sendable Closures
Swift에서는 코드가 동시성 처리 규칙을 재대로 따르도록 하기 위해 고립(isolation)을 정적으로 (컴파일 타임) 설정하는 방법 외에도, 동적 고립(런타임)을 설정할 수 있습니다. 특히 swift6이전의 모듈에서 작성된 코드라면 이러한 동시성 관련 속성이 누락 되었을 가능성이 있는데, 이런 경우는 주로 @Sendable과 같은 주석(annotation)이 빠진 클로저로 인해 발생합니다.
예제
// Swift 6 이전 모듈에서 정의된 함수
extension OrchardAutomation {
// @Sendable 주석이 없음
static func configureOrchard(_ callback: @escaping () -> Void) {
// 고립 영역을 넘나들 수 있음
}
}
@MainActor
class FruitOrchard {
func startConfiguration() {
OrchardAutomation.configureOrchard {
// MainActor 고립이 추론됨
self.applyConfiguration()
}
}
func applyConfiguration() { }
}
해결방법은 클로저에 @Sendable 을 명시합니다.
@Sendable 주석을 명시하면 클로저의 고립 추론이 비활성화되고, 컴파일러가 고립 변경 가능성을 인식할 수 있습니다. 이로 인해 호출 시 Task와 await를 사용하여 고립을 명확히 지정할 수 있게 됩니다.
@MainActor
class PersonalTransportation {
func configure() {
JPKJetPack.jetPackConfiguration { **@Sendable** in
// 이 클로저는 고립을 추론하지 않음
Task {
await self.applyConfiguration()
}
}
}
func applyConfiguration() {
}
}
이렇게 하면 appleConfiguration()이 다른 고립 영역에서도 안전하게 호출 될 수 있습니다.
두번째 방법으로는 컴파일러 플래그 사용하여 전체 모듈에서 동적 고립 검증을 비활성화 하는 방법으로
-disable-dynamic-actor-isolation
플래그를 사용할 수도 있습니다. 이 방법은 모든 동적 고립 규칙을 무시하고 컴파일되지만, 예상치 못한 동시성 문제를 일으킬 수 있어 권장되지 않습니다.
경고 이 플래그는 주의해서 사용해야 합니다. 이러한 런타임 검사를 비활성화하면 데이터 격리 위반이 허용됩니다.
DispatchSerialQueue와 Actor 통합
기본적으로 액터는 시스템이 정의한 방식으로 작업을 스케줄링하고 실행합니다. 그러나 특정 상황에서는 이를 커스터마이징하여 직접 정의한 큐로 작업을 제어할 수 있습니다. DispatchSerialQueue는 이러한 커스텀 큐 지원을 제공하여 액터와 DispatchQueue 간의 호환성을 유지하면서 점진적으로 액터 모델로 전환할 수 있도록 돕습니다.
이 방식은 기존 DispatchQueue에 의존하는 코드를 유지하면서도 액터 모델로 점진적으로 전환할 때 유용합니다. 이를 통해 기존 시스템의 안정성을 해치지 않으면서 새로운 동시성 모델을 도입할 수 있습니다.
예시)
actor FruitDeliverySite {
private let queue = DispatchQueue(label: "com.fruit.delivery.queue")
nonisolated var unownedExecutor: UnownedSerialExecutor {
queue.asUnownedSerialExecutor()
}
func receiveDelivery(_ delivery: FruitDelivery) {
// 이 함수는 queue에서 실행됩니다
}
}
기존 코드와의 호환성 유지
static isolation(정적 고립)은 타입 시스템의 일부이기 때문에 공개 API에 영향을 미칠 수 있습니다. 그러나 기존 사용자들에게 문제가 생기지 않도록 하면서, Swift6 새로운 고립 기능으로 API를 점진적으로 개선 할 수 있습니다.
예를 들어, FruitStyler가 공개 API라면
@preconcurrency @MainActor
public class FruitStyler {
// ...
}
@preconcurrency 를 사용하면 고립 영역이 조건부로 설정되어 클라이언트 모듈에서 Swift6 의 완전한 검사 기능이 활성 화 될 떄만 적용됩니다.
따라서 Swift6를 채택하지 않은 기존 사용자와의 소스 호환성을 유지할 수 있습니다.
의존성 모듈과의 호환성 문제
swift6으로 코드베디스를 업데이트할때 외부 의존성 모듈(본인이 관리하지 않는 외부 모듈)이 아직 Swift6를 지원하지 않는다면 (isolated)와 관련된 여러 오류가 발생할 수 있습니다. 이때, 일부 문제는 직접 해결하기 어렵거나 불가능할 수 있습니다.
@preconcurrency는 이러한 호환성 문제를 해결하는데 도움을 줄 수 있습니다.
Non-Sendable 타입: Swift 6에서 Sendable로 지정되지 않은 타입은 동시성 처리에 어려움을 겪을 수 있습니다. @preconcurrency를 사용하면, 의존성 모듈이 Sendable 요구 사항을 충족하지 않더라도 호환성을 유지할 수 있습니다.
프로토콜 일치 문제: Swift 6에서는 프로토콜의 고립 규칙이 추가되어, 이전 버전과의 프로토콜 준수에서 고립 차이가 발생할 수 있습니다. @preconcurrency는 이 경우에도 Swift 6에서 발생하는 고립 문제를 줄여줍니다.
'Development > iOS 개발' 카테고리의 다른 글
iOS17에서 URL(string:) 사용 유의점 (0) | 2023.10.10 |
---|---|
xcode Build Clean으로도 알 수 없는 에러가 남을때 ☹️ (0) | 2023.01.01 |
[Swift 5] @propertyWrapper 란? (0) | 2022.09.26 |
SwiftUI) NavigationLink 터치 에니메이션 제거 (0) | 2022.09.06 |
[Swift 5.1] .Self 키워드 (0) | 2022.08.27 |