오늘의 주제는 Singleton Pattern입니다.
개인적인 생각으로 Creational Patterns 중 가장 유명(?)하고 익숙한 Pattern이라고 생각합니다.
1. 설계 목적
Singleton은 Class 인스턴스가 하나만 있도록 하고 이 인스턴스를 전역에서 접근할 수 있도록 하는 패턴입니다.
2. 문제 상황
예를 들어 데이터베이스 혹은 파일 시스템과 상호작용하는 클래스가 있다고 가정합니다.
만약 2개 이상의 인스턴스가 존재할 경우, DB에 중복으로 연결되어 데이터의 불일치가 생기거나 파일 데이터의 Data Race가 발생할 수 있습니다.
이러한 상황을 미리 예방하기 위해 인스턴스의 개수를 한 개로 유지합니다.
또한 클래스가 너무 복잡한 초기화 과정을 거치거나, Cache
나 Log
를 관리하는 클래스처럼 동일한 기능을 하는 여러 개의 인스턴스로 관리했을 때 관리의 복잡성이 증가할 경우에도 인스턴스를 한 개로 유지할 필요가 있습니다.
3. 해결책
클래스의 인스턴스를 반드시 하나만 있도록 설계합니다.
그러기 위해서는 아래와 같은 조건이 필요합니다.
- Class의 생성자를 Private하도록 정의합니다.
class Database {
private init() {
// Initialize Code...
}
}
- 생성자 역할을 하는 정적(static
) 메소드를 정의합니다.
class Singleton {
private static var instance = Singleton?
public static func shared() -> Singleton {
guard let instance = Singleton.instance else {
self.instance = Singleton()
return self.instance!
}
return instance
}
private init() {
// Initialize Code...
}
}
4. 구조
Multi-Threading
을 지원하는 경우, 인스턴스가 중복으로 생성되는 것을 막기 위해 Thread Lock
을 해야합니다.
Swift에서는 Actor
타입으로 선언하여 Thread 안전성을 보장할 수 있습니다.
5. 예제 코드 사용
class Singleton {
private static var instance = Singleton?
public static func shared() -> Singleton {
guard let instance = Singleton.instance else {
self.instance = Singleton()
return self.instance!
}
return instance
}
private init() {
// Initialize Code...
}
public func query(sql: String) -> Any? {
// Query Code...
}
}
// 모두 같은 인스턴스로부터 실행
Database foo = Database.getInstance()
foo.query("SELECT ...")
Database bar = Database.getInstance()
bar.query("SELECT ...")
6. 장단점
일반적으로 전역에서 접근이 가능한 구조이기 때문에 상태관리가 용이합니다.
하나의 인스턴스만 사용하고 전역에서 접근이 가능하지만 보통은 실제로 필요한 시점에 생성되기 때문에 메모리와 리소스를 절약할 수 있습니다.
하지만 하나의 객체에서 관리하는 전역 상태가 많아질 경우 코드가 복잡해집니다.
이론상 SOLID 원칙 중 단일 책임 원칙(SRP; Single Responsibility Principle)을 위배하게 됩니다.
Test를 위한 Mock 객체 설계가 어려워져 Test 용이성이 떨어집니다.
Dependency Injection(DI; 의존성 주입)과 충돌하는 이유가 대표적입니다.
다음의 예시를 살펴봅시다.
class NetworkManager {
static let shared = NetworkManager()
private init() {}
func fetchData() {
print("Fetching data from network...")
}
}
class DataManager {
func loadData() {
NetworkManager.shared.fetchData()
}
}
이와 같은 코드에서 DataManager
는 NetworkManager
의 Singleton 객체에 완전히 의존되어 있기 때문에 Mock 객체를 주입하는 것이 불가능하거나 까다로워집니다. 이는 실제 작동하는 코드를 테스트할 수 없고, 테스트를 위한 코드를 테스트 하는 꼴이 됩니다.
class NetworkManager {
static let shared = NetworkManager()
private init() {}
func fetchData() {
print("Fetching data from network...")
}
}
class DataManager {
var networkManager: NetworkManager
init(networkManager: NetworkManager = NetworkManager.shared) {
self.networkManager = networkManager
}
func loadData() {
networkManager.fetchData()
}
}
// in Release Env
let dataManager = DataManager()
dataManager.loadData()
// in Test Env
let mockNetworkManager = MockNetworkManager()
let testDataManager = DataManager(networkManager: mockNetworkManager)
testDataManager.loadData()
때문에 Singleton 객체를 주입받을 수 있는 방식으로 설계한다면, 정상적인 Test 수행이 가능하게 됩니다.
Singleton 시 반드시 유의해야하는 포인트입니다.
7. 실제 사용 사례
import Foundation
final class CacheManager {
// Private
private var cache = [String: Any]()
private init() {}
// Public
public static let shared = CacheManager()
public func saveData(key: String, value: Any) {
cache[key] = value
print("Data saved to cache: [\(key): \(value)]")
}
public func loadData(key: String) -> Any? {
return cache[key]
}
public func removeData(key: String) {
cache.removeValue(forKey: key)
}
public func clearCache() {
cache.removeAll()
}
}
실제로 이와 같이 CacheManager
를 구성하여 사용할 수 있습니다.
let cacheManager = CacheManager.shared
cacheManager.saveData(key: "userProfile", value: ["name": "John", "age": 30])
cacheManager.saveData(key: "token", value: "abcdef12345")
if let userProfile = cacheManager.loadData(key: "userProfile") as? [String: Any] {
print("User Profile: \(userProfile)")
}
cacheManager.removeData(key: "token")
cacheManager.clearCache()
8. Conclusion
Singleton Pattern을 마지막으로 객체 생성에 관한 Creational Patterns가 마무리 되었습니다.
5가지 Pattern들을 잘 활용해보면 좋겠습니다.
다음 주제는 설계 구조에 관한 Structural Patterns가 시작됩니다.
Reference
Singleton
Real-World Analogy The government is an excellent example of the Singleton pattern. A country can have only one official government. Regardless of the personal identities of the individuals who form governments, the title, “The Government of X”, is a g
refactoring.guru
'Design Pattern > GoF - Creational Patterns' 카테고리의 다른 글
[GoF Design Patterns] Prototype (0) | 2024.10.01 |
---|---|
[GoF Design Patterns] Builder (0) | 2024.09.28 |
[GoF Design Patterns] Abstract Factory - 실제 사용 예제 (1) | 2024.09.26 |
[GoF Design Patterns] Abstract Factory (0) | 2024.09.26 |
[GoF Design Patterns] Factory Method (0) | 2024.09.24 |