Singleton Pattern in Swift

design-pattern
Written on Aug 16, 2023


Singleton Pattern은 클래스가 오직 하나의 인스턴스만 가지고 있음을 보장합니다. 프로그램 전반에 걸쳐 인스턴스를 공용으로 사용하고자 할 때나, 굳이 인스턴스를 여러 개 생성할 필요가 없을 때 사용합니다. 서버나 DB 혹은 입출력 장치와 같이 외부와 통신하는 역할을 수행하는 클래스에서 주로 사용합니다. UIKit에서도 자주 등장하는 패턴 중 하나입니다.

Singleton Pattern은 GoF의 분류 체계에서 생성(Creational) 패턴에 속합니다.

Singleton Pattern은 단순히 인스턴스를 하나만 생성할 수 있는 클래스만 있으면 됩니다.


간단한 예제를 통해 Singleton Pattern에 대해 알아봅시다. 사용자 인증 정보를 관리하는 AuthService를 구현했습니다. 프로그램 전반에서 해당 서비스에 접근하여 사용자 정보를 불러오거나 업데이트할 수 있습니다.

Language:swift
struct User {
    var id: String
    var name: String
}

final class AuthService {
    private(set) var user: User?
    
    func login(email: String, password: String) {
        // 서버에 로그인을 요청하여 사용자 정보를 불러옴
        user = User(id: "1", name: "jinyongp") 
    }
    
    func logout() {
        // 서버에 로그아웃을 요청
        user = nil
    }

    func update(user: User) {
        // 서버에 사용자 정보 변경을 요청
        self.user = user
    }
}

이렇게 Singleton이 아닌 방식으로 구현한다면 AuthService로부터 사용자 정보를 불러올 때 사용자 정보가 매번 초기화됩니다.

Language:swift
// 로그인을 통해 사용자 정보 저장
let authService = AuthService()
authService.login(email: "dev.jinyongp@gmail.com", password: "PASSWORD")
print(authService.user?.name) // jinyongp
Language:swift
// 다른 곳에서 사용자 정보를 불러오고자 인스턴스를 생성하면 사용자 정보가 없음
let authService = AuthService()
print(authService.user?.name) // nil

이를 Singleton Pattern을 적용하여 해결해봅시다.

Language:swift
final class AuthService {
    static let shared = AuthService()
    private init() { print("인스턴스 생성") }

    private(set) var user: User?
    
    func login(email: String, password: String) {
        // 서버에 로그인을 요청하여 사용자 정보를 불러옴
        user = User(id: "1", name: "jinyongp") 
    }
    
    func logout() {
        // 서버에 로그아웃을 요청
        user = nil
    }

    func update(user: User) {
        // 서버에 요청
        self.user = user
    }
}

static 키워드를 활용하여 shared라는 타입 속성을 추가했습니다. sharedAuthService의 생성자를 실행하여 인스턴스를 얻습니다. 그리고 새로운 인스턴스 생성을 막기 위해 private init() {}을 추가했습니다. 이제 외부에서 생성자를 호출할 수 없고, shared 속성으로만 인스턴스를 얻을 수 있습니다.

Swift에서 타입 속성은 lazy로 동작합니다. 즉, 타입 속성에 처음 접근할 때 생성자를 실행합니다. 그렇기에 shared를 사용하지 않는다면 인스턴스가 생성되지 않습니다.

Language:swift
// 로그인을 통해 사용자 정보 저장
let authService = AuthService.shared
authService.login(email: "dev.jinyongp@gmail.com", password: "PASSWORD")
print(authService.user?.name) // jinyongp
Language:swift
// 동일한 인스턴스를 가지므로 다른 곳에서 사용자 정보를 볼러올 수 있음
let authService = AuthService.shared
print(authService.user?.name) // jinyongp

이제 AuthService의 인스턴스는 하나만 생성됨을 보장하고 인스턴스의 데이터를 유지할 수 있습니다.