애플사이다의 iOS 개발 일지

[디자인 패턴] Singleton 본문

프로그래밍 철학

[디자인 패턴] Singleton

Applecider 2023. 3. 26. 21:09

문제 상황

  • 현재 로그인한 사용자의 id, nickname, email 등의 User Data는 어떻게 관리하는 게 좋을까?
  • 앱을 사용하는 동안 로그인 상태가 수시로 변경될 수 있으므로 여러 화면에서 접근할 수 있어야 한다. 예를 들어 앱을 최초실행 했을 때 로그인화면에서 sign in 할 수 있고, 로그아웃 화면에서 sign out 할 수 있고, 다시 비로그인 상태에서 좋아요 버튼을 눌렀을 때 modal로 띄워지는 로그인 화면에서 sign in 할 수도 있다.
  • 이때 여러 화면에서 어떠한 시점에 접근하더라도 로그인 상태는 동일해야 한다. 따라서 공유 리소스 형태로 관리하고 싶다.
  • 위 상황에서 Singleton 패턴이 해결책이 될 수 있다.

패턴 설명

  • 클래스의 인스턴스를 1개만 만들고, 이 인스턴스에 대해 여러 화면에서 접근하도록 한 패턴이다.
    • 의문 - 전역변수를 쓰면 되지 않나? : 예상치 못한 시점에 인스턴스를 덮어쓰기할 수 있어서 위험하다.
      (싱글턴은 만들어둔 인스턴스가 덮어쓰기되지 않도록 보호 가능)

 
  • 위 그림에서는 getInstance() 메서드를 통해 인스턴스를 반환하도록 했지만,
    Swift 문법에 따라 static 프로퍼티를 선언하여 항상 똑같은 인스턴스를 반환하도록 해도 된다.
  • default initializer를 private로 설정하여 외부에서 인스턴스를 추가 생성하지 못하도록 방지한다.

장점

  • 클래스가 단 1개의 인스턴스만 가지는 것을 보장한다.
  • 인스턴스에 대한 전역 접근이 가능하다.
  • 메모리 사용 측면에서 효율적일지도 모른다. (아래 단점 참고) Swift에서 static 프로퍼티는 최초 접근할 때 lazy한 방식으로 초기화되기 때문이다.

단점

Singleton은 다른 디자인 패턴에 비해 단점이 많다. 그래서 Singleton은 디자인 패턴이 아니라는 말까지 있다.
따라서 아래의 단점에 유의해서 사용해야 한다.

  • 단일책임 원칙을 위반한다. 한 객체가 여러 가지 역할을 담당한다.
    (1개의 인스턴스만 생성하도록 제어하는 역할을 하면서, 동시에 User Data를 관리하는 역할을 하는 등 두 가지 기능을 동시에 한다는 뜻인듯?)
  • 코드 간 결합도가 높아진다. 너무 많은 데이터를 공유할 경우, 개방폐쇄 원칙을 위반한다. 추적이 어렵다.
  • 테스트 코드 작성이 어렵다. (공유 리소스이므로 테스트 코드를 위한 목적만으로 사용하는 게 불가능함. 또한 해당 클래스를 상속하는 Mock 객체를 초기화하는 것도 불가능함)
  • 메모리 사용 측면에서 비효율적이다. static 프로퍼티에 인스턴스를 저장하므로 앱의 lifecycle 동안 메모리에 해당 인스턴스를 올려서 사용하기 때문이다.
  • 다중 스레드 환경에서 여러 스레드가 인스턴스를 여러 번 생성하지 않도록 스레드 lock이 필요할 수 있다.

예시 코드

class UserManager {
  static let shared = UserManager() // 단 하나의 인스턴스를 공유하여 사용함
  private var id: Int?

  private init() { } // 외부에서 인스턴스를 추가 생성 못하도록 방지
  
  func saveId(_ id: Int) {
    self.id = id
  }
  
  func deleteId() {
    self.id = nil
  }
}

// Client
// sign in 하는 시점
UserManager.shared.saveId(100)

// sign out 하는 시점
UserManager.shared.deleteId()

초기화 과정이 복잡하다면 약간 복잡한 아래 방법도 가능하다.

class NetworkManager {
    // MARK: - Properties
    // 클로저를 통해 복잡한 형태의 초기화가 가능함
    private static var sharedNetworkManager: NetworkManager = {
        let networkManager = NetworkManager(baseURL: API.baseURL)

        // Configuration
        // ...

        return networkManager
    }()

    let baseURL: URL

    // MARK: - Initializers
    private init(baseURL: URL) {
        self.baseURL = baseURL
    }

    // MARK: - Accessors
    class func shared() -> NetworkManager {
        return sharedNetworkManager
    }
}

Reference

Comments