시나리오: swift로 싱글톤 패턴을 구현해보자.
예제로 Atlas은행에 입금, 조회 하는 기능을 만들어 보면서 싱글톤 패턴에 대해서도 알아보고 넘어가자.
싱글톤 패턴이란?
소프트웨어 디자인 패턴에서 싱글턴 패턴(Singleton pattern)을 따르는 클래스는, 생성자가 여러 차례 호출되더라도 실제로 생성되는 객체는 하나이고 최초 생성 이후에 호출된 생성자는 최초의 생성자가 생성한 객체를 리턴한다.
.... from 위키백과
즉, 다시 한번 정리하면 공용으로 사용하고 싶은 객체를 하나 만들어 사용하는 것이라고 할 수 있겠다.
URLSession.shared , UserDefaults.standard 를 한번 사용해 본 경험이 있다면 우리는 싱글톤 패턴으로 구현된 인스턴스를
사용한 경험이 있다는 것!
이와같은걸 직접 구현해 본다고 생각하니 설레지않습니까요? ㅎㅎ
먼저 간단하게 싱글톤 패턴으로 클래스를 하나만들어 보자.
class AtlasBank {
//static let 으로 1회 생성 보장
public static let shared = AtlasBank()
//다른 곳에서 생성하지 못하도록 생성자에 private 접근지시자를 써준다.
private init(){}
}
swift는 static 프로퍼티의 Thread-safe를 보장한다.(WoooooW)
이 말은 개발자들이 우연히 여러개의 인스턴스가 생성될 걱정을 하지 않해도 된다는 것이다. !핵이득!!
그리고 입금과 조회의 기능을 각 각 deposit, checkBalance 메서드로 코드를 작성해 보자.
class AtlasBank {
public static let shared = AtlasBank()
private var history[String: Any] = [:]
//다른 곳에서 생성하지 못하도록 생성자에 private 접근지시자를 써준다.
private init(){}
public func deposit(user: String , amount: Double){
}
public func checkBalance(user: String) -> Double{
}
}
끄으읏! 이라고 하기에는 아직 한발 남은 상황 ㅠㅠ
프로젝트가 토이 프로젝트 정도의 사이즈이거나 멀티 스레딩 환경에 노출되는 경우가 없다면 이렇게 쓸 수도 있겠지만,
예제를 기준으로 동시에 여러 사람이 내 계좌로 입급을 해주고, 잔액을 조회해본다고 가정해보면
동시에 은행에 접근하여 처리하는 상황을 고려해서 설계를 해주는 것이 좋다.
이런 상황을 전문용어로 Race Condition(경쟁상태) 이라고 한다
Race Condition 이란?
둘 이상의 입력 또는 조작의 타이밍이나 순서 등이 결과값에 영향을 줄 수 있는 상태를 말한다. 입력 변화의 타이밍이나 순서가 예상과 다르게 작동하면 정상적인 결과가 나오지 않게 될 위험이 있는데 이를 경쟁 위험이라고 한다. from 위키백과
위의 코드는 정상적으로 동작할 거 같은 느낌이지만 Conccuent 상황에서는 뒤죽 박죽이 될 가능성이 농후하다
(테스트 진행 코드는 다른 포스팅으로 진행해 보려고 한다. )
개선해보기
1. SerialQueue를 하나 만들고 sync() 를 사용하여 동기적으로 수행되도록 할 수 있다.
// SerialQueue 생성
var serialQueue = DispatchQueue(label: "atlasSerialQueue")
2. sync() 메서드 내부의 로직을 구현해 준다.
public func deposit(user: String ,amount: Int ){
//sync() 메서드 안에 로직 구현
serialQueue.sync (){
self.history[user] = amount
}
}
public func checkBalance(user: String) -> Int?{
var amount:Int?
//sync() 메서드 안에 로직 구현
serialQueue.sync {
amount = self.history[user]
}
return amount
}
//FULL CODE
class AtlasBank {
var serialQueue = DispatchQueue(label: "atlasSerialQueue")
public static let shared = AtlasBank()
private var history: [String: Int] = [:]
private init(){}
public func deposit(user: String ,amount: Int ){
serialQueue.sync (){
self.history[user] = amount
}
}
public func checkBalance(user: String) -> Int?{
var amount:Int?
serialQueue.sync {
amount = self.history[user]
}
return amount
}
}
이렇게 조치를 취함으로 각 입금과 조회를 하는 부분에서 sync 를 맞추기 때문에 경쟁상태(race condition)을 대비할 수 있다.
+ 속도 최적화 시키기
위의 코드대로 사용해도 이슈는 없겠지만 속도적인 측면에서 최적화를 시킬 수 있는 방법이 있다고 한다!
바로 flags 라는 옵션에서 .barrier를 사용하는 것인데
Concurrent queue가 사용하는 여러개의 thread 에서 하나의 쓰레드만 실행하고 모든 스레드 사용을 막아준다고한다.
그래서 하나의 쓰레드만 사용하기 때문에 serial 하게 실행이 된다!.
코드로 적용해 보면,
1.DispatchQueqe에 flags 옵션에 .barrier 를 사용한다.
class AtlasBank {
var concurrentQueue = DispatchQueue(label: "atlasConcurrentQueue",attributes: .concurrent)
public static let shared = AtlasBank()
private var history: [String: Int] = [:]
private init(){}
public func deposit(user: String ,amount: Int ){
concurrentQueue.async (flags: .barrier){
self.history[user] = amount
}
}
public func checkBalance(user: String) -> Int?{
var amount:Int?
concurrentQueue.sync {
amount = self.history[user]
}
return amount
}
}
마무리
-장점만 한가득할거 같지만 사실 싱글톤패턴은 안티패턴에 속하기도 한다고 한다.
다른 클래스의 인스턴스들 간의 커플링(결합도)가 높아지기 때문에 SOLID 의 OPC(Open-Closed Principle) 을 위배하기 때문이라고 한다.
-이런 장,단점을 잘 파악하고 내가 필요한 곳에 적절할게 쓸 수 있도록 항상 생각하자 👍
gitHub :
https://github.com/PotatoArtie/Potato-iOS/tree/master/Labs/Playground/TheSingletonPotato
서적 참조:
Design patterns in swift5 -KAROLY NYISZTOR
참조:
https://ko.wikipedia.org/wiki/%EC%8B%B1%EA%B8%80%ED%84%B4_%ED%8C%A8%ED%84%B4
https://ko.wikipedia.org/wiki/%EA%B2%BD%EC%9F%81_%EC%83%81%ED%83%9C
[Let's discuss] ViewModel은 struct? class? 어떤 걸 사용해야 할까? (0) | 2022.07.20 |
---|---|
[SwiftUI] Life Cycle(생명주기) 알아보기 (0) | 2022.04.20 |
[Swift] MVVM 패턴 (아키텍처 디자인)이 무엇인지 한번 캐보자! (0) | 2022.03.01 |
[Swift] MVVM 패턴 (아키텍처 디자인) 왜? 해야하나요? MVVM 안써도 코딩은 할 수는 있지만, 쓰는데는 다 이유가 있는법! (0) | 2022.03.01 |
[Swift] SwiftUI에서는 왜 뷰에 struct를 사용할까? (1) | 2022.02.19 |
댓글 영역