본문 바로가기
iOS/Concurrency

Sendable 프로토콜에 대해!

by 지금갑시다 2022. 12. 20.

2023.10.04 1차 수정


 

프로토콜 중 하나인 Sendable!

 

써져있는 그대로 Sendable하다는 것이다.

두괄식으로 말하자면! 비동기상황에서 값을 전달할때, 전달하는 값이 thread safe해서 변경될 여지가 없다고 보장하는 표시이다!

 

그럼 어떤 상황에서 Sendable을 명시적으로 적어줘야 할까? 간단한 예시와 알아보자!

 

actor CurrentUserManager {
    func updateDataBase(userInfo: String) {
        
    }
}

class SendableViewModel: ObservableObject {
    
    let manager = CurrentUserManager()
    
    func updateCurrentUserInfo() async {
                
        await manager.updateDataBase(userInfo: "name")
    }
    
}

struct SendablePractice: View {
    @StateObject private var vm = SendableViewModel()
    
    var body: some View {
        Text("Hello, World!")
            .task {
                await vm.updateCurrentUserInfo()
            }
    }
}

간단한 데이터를 업데이트하는 가정으로 만들어진 actor와 이를 뷰와 이어 actor에 접근해 데이터를 넘기는 ViewModel로 이루어져있다.

 

이때, 가장 위에서 말한 데이터를 전달하는 과정에서 thread safe함을 고려해주어야 하는 부분은 ViewModel의 async한 함수 안에서 manager에 접근하는 부분의 데이터다. 위에서는 String값인 "name"이 그 데이터이다.

 

지금 "name"은 상수값이고, Value값이므로, thread safe하고 변경될 여지가 없다.

이걸 같은 Value값인 Struct로 바꾸어 데이터를 넘겨주자

 

actor CurrentUserManager {
    func updateDataBase(userInfo: MyUserInfo) {
        
    }
}

struct MyUserInfo: Sendable {
    let name: String
}

class SendableViewModel: ObservableObject {
    
    let manager = CurrentUserManager()
    
    func updateCurrentUserInfo() async {
                
        await manager.updateDataBase(userInfo: MyUserInfo(name: "name"))
    }
    
}

struct SendablePractice: View {
    @StateObject private var vm = SendableViewModel()
    
    var body: some View {
        Text("Hello, World!")
            .task {
                await vm.updateCurrentUserInfo()
            }
    }
}

 

Struct 인 MyUserInfo를 추가해주고 Sendable 프로토콜을 채택해주었다. 

 

MyUserInfo 역시 Struct이고 Value값이므로, 데이터가 전달되며 그 값이 바뀔 위험이 없다. 즉 thread safe하다. -> 값이 바뀌면 원래의 데이터가 바뀌는 것이 아니라, 새로운 데이터로 갈아끼워지는 것이다.

그런 의미에서 MyUserInfo Struct에 Sendable을 채택해주었고, 이는 프로세스의 퍼포먼스 향상에 도움을 준다고 한다.

 

그럼 Reference 타입인 Class는 어떨까? Class는 thread safe하지 않다. Reference 타입이기때문에 class를 건들이면 class가 저장되어 있는 Heap메모리 데이터가 바뀌기 때문이다.

 

// Sendable을 사용하려면 final class로 만들어 주어야 함
final class MyClassUserInfo: Sendable {
    
    // name이 var라면, mutable함으로 에러 발생함,
    // let으로 name을 만들거나,
    let name: String
    
    init(name: String) {
        self.name = name
    }
    
}

class에 Sendable을 사용하려면 class가 final이어서 상속받지 않음을 알려야 하고, name을 불변하는 타입인 let으로 만들어 주어야 한다. name을 var로 만든다면, 값이 바뀔 수 있으므로 오류가 뜬다.

 

하지만, var로 둘 수 있는 경우가 있는데,

final class MyClassUserInfo: @unchecked Sendable {
    
    // @unchecked로 만들어 줘라 super dangerous -> queue를 만들어줘서 thread safe하게 만들어주어야 합니다.
    private var name: String    
    let queue = DispatchQueue(label: "com.MyClassUserInfo")
    
    init(name: String) {
        self.name = name
    }
    
    func updateName(name: String) {
        queue.async {
            self.name = name
        }
    }
    
}

Sendable 프로토콜에 @unchecked로 만들어주고, class가 thread safe하게 동작하도록 직접 코딩해주는 방법이다.

 

위와 같이 MyClassUserInfo의 내부에서만 그 값을 변경할 수 있게 하고, DispatchQueue를 만들어주어 Concurrency를 보장해주면 된다! 

이렇게 만들어주게 되면, name을 var로 두고도 Sendable을 명시적으로 채택할 수 있게 된다!

 

 

 

REF: https://www.youtube.com/watch?v=wSmTbtOwgbE&list=PLwvDm4Vfkdphr2Dl4sY4rS9PLzPdyi8PM&index=12