Notice
Recent Posts
Recent Comments
Link
«   2025/01   »
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
Archives
Today
Total
관리 메뉴

cchanmi

[iOS] 싱글톤 패턴과 스레드 본문

iOS

[iOS] 싱글톤 패턴과 스레드

cchanmi 2024. 3. 25. 18:14

싱글톤 패턴이란?

오직 하나의 인스턴스만 생성해서 사용하는 패턴입니다.

해당 인스턴스를 전역으로 생성하고, 여기저기서 접근하여서 사용할 수 있습니다.

 

실제로 싱글톤 객체는

class SingleTon {
  static let shared = SingleTonPattern()
  private init { }
}

 

이런 식으로 static let으로 전역으로 선언합니다.

그리고 다른 곳에서는 해당 클래스를 초기화할 수 없도록 private init을 해 주고 있습니다.

그렇다면 왜 static let으로 선언해야 하는지, 싱글톤의 장점과 단점은 무엇인지에 대해 먼저 알아보도록 할게요.

 

싱글톤의 장점

- 한번의 인스턴스만 생성하기 때문에 메모리 낭비를 방지할 수 있음

- 전역으로 인스턴스를 공유하기 때문에 데이터 공유가 쉬움

 

싱글톤의 단점

- 멀티 스레드 환경에서 thread safe 하지 않음

- 객체들간의 의존성이 생김

- 더불어 테스트하기 어려움

 

이렇게 생각해 볼 수 있는데, 싱글톤의 단점에서의 멀티 스레드 환경에서의 문제점!!!에 대해 좀 더 자세히 알아보도록 할게요.

 


싱글톤은 하나의 인스턴스만 처음에 생성해 두고 쭉 사용하는 것이 특징이자, 장점이라고 앞에서 말했었는데요.

만약 멀티 스레드 환경에서 싱글톤 인스턴스를 생성하는 동시에 일어나게 되면 어떻게 될까요?

그러면 싱글톤 인스턴스가 메모리에 두 개, 세 개 올라가게 되면서 싱글톤을 사용하는 의미가 없어지게 되겠죠.

 

하지만 이 문제는 Swift에 한해서는 thread safe하게 동작합니다!

swift에서 static으로 전역 변수 인스턴스를 사용하는 경우, GDC의 distpatch_once라는 것이 자동으로 적용이 되어 인스턴스가 생성되기 때문에, 따로 어떠한 처리를 해 주지 않아도 자동으로 thread safe한 방법으로 동작하게 된다고 하네요.

dispatch_once는 objective_C에서 싱글톤을 생성할 때 불리는 작업이라고 하네요.

https://developer.apple.com/swift/blog/?id=7

 

그렇기 때문에 Swift에서의 싱글톤 생성 자체는 thread safe 하다고 말할 수 있습니다.

그렇다면 thread safe 하지 않은 경우는 어떤 경우일까요?

 

class User {
    static let shared = Infomation()
    private init() { }
        
    private var nickname: String = "짠미"
    
    func printnickName() {
    	print(nickname)
    }
    
    func changeNickname(nickname: String) {
    	self.nickname = nickname
    }
}

 

이러한 싱글톤 객체가 있을 때,

만약 멀티 스레드 환경에서 nickname 변수를 write 하거나 동시에 read하게 된다면 원하지 않는 결과를 가져오는 상황이 발생할 수 있습니다.

 

이러한 문제를 해결하기 위해서는 한번에 오직 한 작업만을 실행시키는 serial dispatch queue를 사용하여서 순차적으로 작업을 되도록 할 수 있습니다.

 

class User {
	var serialQueue = DispatchQueue(label: "serialQueue")
    static let shared = Infomation()
    private init() { }
        
    private var nickname: String = "짠미"
    
    func printnickName() {
    	serialQueue.sync() {
        	print(nickname)
        }
    }
    
    func changeNickname(nickname: String) {
	    serialQueue.sync() {
    		self.nickname = nickname
        }
    }
}

 

이렇게 serialQueue를 사용하여서 동기적으로 작업이 실행되게 하여 race condition 상황을 해결할 수 있습니다.

여기서 속도적인 측면까지 최적화 시킬 수 있는 방법은 flags의 옵션에서 .barrier를 사용하는 것인데

해당 옵션을 사용하면 Concurrent queue가 사용하는 멀티 스레드 중에서 하나의 스레드만 실행되고, 모든 스레드의 사용은 막아 준다고 합니다.

 

class User {
	var concurrentQueue = DispatchQueue(label: "concurrentQueue", attributes: .concurrent)
    static let shared = Infomation()
    private init() { }
        
    private var nickname: String = "짠미"
    
    func printnickName() {
    	concurrentQueue.sync(flags: .barrier) {
        	print(nickname)
        }
    }
    
    func changeNickname(nickname: String) {
	    concurrentQueue.sync(flags: .barrier) {
    		self.nickname = nickname
        }
    }
}

 

 

이러면 기존 serial queue보다 속도적인 측면도 향상되면서 싱글톤 객체의 사용 시점도 thread safe하게 됩니다!

 


 

지금까지는 그냥 어렴풋이 싱글톤 객체는 무조건 thread safe 하지 않다고 생각해 왔었어서 이번 내용이 꽤 흥미롭게 다가왔어요.

swift는 자체적으로 생성 시점을 thread safe를 보장해 주지만, 인스턴스에 접근하여 사용할 때에는 thread safe하지 않다는

 

정리하자면 swift의 싱글톤 패턴은 생성 시점에서는 thread safe를 보장해 줍니다. 하지만 멀티 스레드 환경에서 싱글톤 인스턴스에 접근하여 데이터를 읽거나 쓰는 행위가 동시에 일어난다면 race condition이 발생하며 thread safe하지 않을 수 있구요.

이 상황을 해결하기 위해서는 순차적으로 작업을 해 주는 serialQueue를 사용하는 방법과 속도적인 측면을 향상시켜 줄 수 있는 ConcurrentQueue를 사용하여 barrier를 이용하는 방법이 있습니다.

 

저는 대체적으로 싱글톤 객체를 네트워크 레이어 클래스에서 각각의 API 메서드를 호출하는 형태로만 많이 사용해 왔었어요. 어떠한 값을 공유하고 접근하는 것이 아니기 때문에 싱글톤 객체를 유지한다고 해도 thread safe하지 않은 상황이 발생하지 않았을 거예요.

하지만 Unit test를 도입하기 위해 내부적으로 private하게 초기화하는 싱글톤 객체를 사용하기에는 mock data를 얻기가 어려웠기 때문에 의존성 주입으로 변경하였어요.

 

각각의 장단점과 목적이 뚜렷하기 때문에 본인의 프로젝트 특성에 맞게 유동적으로 잘 사용하면 좋을 것 같습니다.

Comments