Design Pattern

[Swift] 디자인 패턴 | Singleton

iOSDEv 2024. 2. 7. 19:00
Singleton pattern

⭐️ 생성자가 여러번 호출되더라도 실제로 생성되는 객체는 하나고 최초 생성 이후에 호출된 생성자는 최초 생성자가 생성한 객체를 리턴⭐️

실제로 생성되는 객체는 하나이며 그 인스턴스는 전역적인 접근을 제공

전역변수를 사용하지 않고 객체를 하나만 생성하여 모든 코드에서 그 객체에 접근할 수 있도록 하는 목적

주로 리소스나 설정을 담당하는 클래스 활용

 

 

구조체로는 싱글톤을 만들지 못한다 이유는?

값타입이기 때문에 인스턴스화하는 순간 메모리주소가 하나 더 생기기 때문이다

 

기본 싱글톤 기본 예시
class MySingleton {
    // 정적 상수로서 sharedInstance를 사용해 싱글톤 인스턴스에 접근
    static let sharedInstance = MySingleton()

    // private으로 선언하여 외부에서 인스턴스를 직접 생성할 수 없도록 함
    private init() {
        // 초기화 코드
    }

    func someFunction() {
        // 싱글톤의 기능을 수행하는 메서드
    }
}

// 사용 예시
let myInstance = MySingleton.sharedInstance
myInstance.someFunction()
  • 유일한 객체가 될 프로퍼티에 static인 타입 메서드를 정의하고 자기 자신을 할당한다 
  • init()메서드 즉 초기화 메서드에 접근 제한을 설정해서 외부에서는 초기화가 불가능하도록 설정함
클래스 다이어그램
//클래스 다이어그램
+---------------------+
|    MySingleton      |
+---------------------+
| - sharedInstance    |
|                     |
| + someFunction()    |
|                     |
| - private init()    |
+---------------------+

 

사용하는 곳
  • 싱글톤은 앱 전체에 걸쳐서 유일한 객체를 만들어 사용하기 때문에 전역적으로 접근할 수 있는 유일한 객체이다 그래서 예를들어 공유되어야하는 데이터
    • UserDefaults
    • NotificationCenter
    • URLSession

 

다른 예시들

 

enum Milk {
    case chocolate
    case strawberry
    case banana
}

// Singleton
class MilkStorage {
    static let shared = MilkStorage()
    private var chocolateMilkCount = 100
    private var strawberryMilkCount = 100
    private var bananaMilkCount = 100

    private init() { }

    func release(_ milk: Milk, count: Int) {
        switch milk {
        case .chocolate:
            chocolateMilkCount -= count
        case .strawberry:
            strawberryMilkCount -= count
        case .banana:
            bananaMilkCount -= count
        }
    }

    func checkMilkStock() {
        print("남은 재고: 초코 우유(\(chocolateMilkCount)), 딸기 우유(\(strawberryMilkCount)), 바나나 우유(\(bananaMilkCount))")
    }
}

class OnlineStore {
    func orderMilk(_ milk: Milk, count: Int) {
        MilkStorage.shared.release(milk, count: count)
 }
 
 
 let naverSmartStore = OnlineStore()
let coupang = OnlineStore()
let weMakePrice = OnlineStore()

naverSmartStore.orderMilk(.chocolate, count: 15)
MilkStorage.shared.checkMilkStock()

coupang.orderMilk(.banana, count: 30)
coupang.orderMilk(.chocolate, count: 10)
MilkStorage.shared.checkMilkStock()

weMakePrice.orderMilk(.chocolate, count: 50)
weMakePrice.orderMilk(.strawberry, count: 70)
MilkStorage.shared.checkMilkStock()

//참고 
//https://yagom.net/courses/design-pattern-in-swift/lessons/%ec%83%9d%ec%84%b1-%ed%8c%a8%ed%84%b4/topic/singleton/

 

/// 싱글톤 클래스는 `shared` 필드를 정의하여 클라이언트가 유일한 싱글톤 인스턴스에
/// 접근할 수 있게 합니다.
class Singleton {

    /// 싱글톤 인스턴스에 대한 액세스를 제어하는 정적 필드입니다.
    ///
    /// 이 구현은 싱글톤 클래스를 확장하면서 각 하위 클래스의 인스턴스를 하나만 유지합니다.
    static var shared: Singleton = {
        let instance = Singleton()
        // ... 인스턴스 구성
        // ...
        return instance
    }()

    /// 싱글톤의 이니셜라이저는 항상 직접적인 생성 호출을 방지하기 위해 private로
    /// 선언되어야 합니다.
    private init() {}

    /// 마지막으로, 어떤 싱글톤은 인스턴스에서 실행될 수 있는 비즈니스 로직을 정의해야 합니다.
    func someBusinessLogic() -> String {
        // ...
        return "'someBusinessLogic' 호출의 결과"
    }
}

/// 싱글톤은 복제될 수 없어야 합니다.
extension Singleton: NSCopying {

    func copy(with zone: NSZone? = nil) -> Any {
        return self
    }
}

/// 클라이언트 코드.
class Client {
    // ...
    static func someClientCode() {
        let instance1 = Singleton.shared
        let instance2 = Singleton.shared

        if (instance1 === instance2) {
            print("싱글톤 작동 중, 두 변수는 동일한 인스턴스를 포함합니다.")
        } else {
            print("싱글톤 실패, 변수에는 서로 다른 인스턴스가 포함되어 있습니다.")
        }
    }
    // ...
}

func testSingletonConceptual() {
     Client.someClientCode()
}


//result 싱글톤 작동 중, 두 변수는 동일한 인스턴스를 포함합니다.

//참고
//https://refactoring.guru/ko/design-patterns/singleton/swift/example

 

장점과 단점
  • 장점
    • 다양한 객체들에게 공유되는 객체를 만들 수 있다는 장점
    • 재사용성이 가능하여 낭비되는 메모리를 방지할 수 있다
  • 단점
    • 객체지향적으로 본다면 인스턴스 간에 결합도가 높아져 개방폐쇄원칙(Open Closed Priciple - OCP)를 위배함