[Swift] Singleton Multithread Strategy / 싱글톤 멀티스레드 전략

2024. 12. 27. 22:52🍏/DesignPattern

안전한 Singleton Pattern을 위환 Thread-Safe 관리

우리는 앱에서 전역적인 리소스 공유, 앱 상태 관리, 네트워크 요청, 로깅 및 분석 등과 같은 곳에서 싱글톤 패턴을 활용합니다.
이때 어디서든 동일한 인스턴스에 접근하게 되고, 앱이 복잡해질수록 데이터 레이스가 발생할 가능성이 높아질 수 있습니다. 이때 사용할 수 있는 방안에 대해서 몇 가지 예시를 제시합니다.

 

1. DispatchQueue를 활용한 데이터 동기화

final class Singleton {
    static let shared = Singleton()
    
    private let queue = DispatchQueue(label: "com.singleton.threadsafe", attributes: .concurrent)
    private var internalData: [String] = [] // 내부 데이터
    
    private init() {}
    
    // 데이터 읽기: 동시적 읽기 허용
    func getData() -> [String] {
        return queue.sync {
            internalData
        }
    }
    
    // 데이터 쓰기: 동기화하여 단일 스레드만 접근
    func addData(_ newData: String) {
        queue.async(flags: .barrier) { // Barrier로 쓰기 작업 보호
            self.internalData.append(newData)
        }
    }
}
  • 읽기 작업 (sync):
    • 데이터를 읽을 때는 sync를 사용하여 Thread-Safe하게 읽음.
    • 동시 읽기(Concurrent Read)는 안전하므로 추가 동기화 없이 처리.
  • 쓰기 작업 (async + barrier):
    • barrier 플래그를 사용하여 쓰기 작업이 실행되는 동안 다른 읽기/쓰기 작업을 차단.

 


 

2. NSLock을 사용한 데이터 동기화

final class Singleton {
    static let shared = Singleton()
    
    private let lock = NSLock()
    private var internalData: [String] = [] // 내부 데이터
    
    private init() {}
    
    // 데이터 읽기
    func getData() -> [String] {
        lock.lock()
        defer { lock.unlock() }
        return internalData
    }
    
    // 데이터 쓰기
    func addData(_ newData: String) {
        lock.lock()
        defer { lock.unlock() }
        internalData.append(newData)
    }
}
  • lock.lock():
    • 특정 스레드가 데이터에 접근하기 전에 Lock을 걸어 다른 스레드의 접근을 차단.
  • lock.unlock():
    • 작업이 끝난 후 Lock을 해제하여 다른 스레드가 접근 가능하도록 함.

 


 

3. OperationQueue를 사용한 데이터 동기화

final class Singleton {
    static let shared = Singleton()
    
    private let operationQueue: OperationQueue = {
        let queue = OperationQueue()
        queue.maxConcurrentOperationCount = 1 // 직렬 실행
        return queue
    }()
    private var internalData: [String] = [] // 내부 데이터
    
    private init() {}
    
    // 데이터 읽기
    func getData(completion: @escaping ([String]) -> Void) {
        operationQueue.addOperation {
            completion(self.internalData)
        }
    }
    
    // 데이터 쓰기
    func addData(_ newData: String) {
        operationQueue.addOperation {
            self.internalData.append(newData)
        }
    }
}
  • 작업 큐 기반:
    • OperationQueue는 작업을 순서대로 처리하여 Thread Safety를 보장.
  • 비동기 작업 처리:
    • 쓰기 작업이나 대량의 데이터를 처리할 때 유용.

 


 

3. Thread-Safe한 싱글톤 데이터 접근 방식 비교

방법 장점 단점
DispatchQueue - 간단하고 효율적.
- Barrier로 쓰기 보호.
- Custom Lock보다 추상적.
NSLock - Lock 메커니즘으로 직접 제어 가능. - 코드가 길어질 수 있음.
- 성능 저하 가능.
OperationQueue - 비동기 작업 처리.
- 작업 관리 용이.
- 작업 큐 관리가 필요.
- 간단한 작업엔 과도.

 


 

4. 각 방식의 동작 원리

  1.  DispatchQueue
    1. barrier 작업은 이전에 큐에 추가된 모든 작업이 완료된 후 실행되며, barrier 작업이 완료될 때까지 이후의 작업이 실행되지 않습니다.
  2. NSLock
    1. Mutex(Mutual Exclusion, 상호 배제) 락의 구현체
    2. Deadlock을 조심해야함
    3. 병렬 작업 성능 저하
    4. 락 경쟁 비용 - OS 커널 레벨에서 동작하는 락 메커니즘이므로 컨텍스트 스위칭이 오래 걸릴 수 있다.
  3. OperationQueue
    1. OperationQueue BlockOperation을 사용하여 비동기 작업 간 Thread Safety를 관리

 


그 외 Lock 성능 비교

동기화 메커니즘 컨텍스트 스위칭 Thread-Safe 성능 특징
NSLock 있음 보장 느림 (커널 오버헤드) 간단한 락, 중간 수준의 성능
DispatchQueue 없음 보장 빠름 읽기-쓰기 동시성 관리에 유리
NSRecursiveLock 있음 보장 느림 (커널 오버헤드) 재귀 락 지원
os_unfair_lock 없음 보장 매우 빠름 경량화된 저수준 락, Spin Lock 기반

 

https://github.com/chanhihi