Singleton(싱글턴)이란 — iOS와 Android에서 클래스의 단일 인스턴스

저자: IT Sectr 게시일: 2026-02-17 읽는 시간: 8 분

Singleton(싱글턴) — 클래스의 단일 인스턴스를 보장하고 이에 대한 전역 접근 지점을 제공하는 생성 패턴입니다. Singleton은 모바일 개발에서 공유 리소스(네트워크 클라이언트, 데이터베이스, 설정 관리자)에 널리 사용됩니다. 이 패턴은 GoF(1994)의 고전적인 책에 설명되어 있으며 가장 잘 알려진 패턴 중 하나입니다. 자세한 내용은 Refactoring Guru: Singleton에서 확인하세요.

핵심 포인트

  • Singleton — 애플리케이션 전체에서 클래스의 하나의 인스턴스 보장
  • 전역 접근 지점 — 정적 속성 shared 또는 companion object
  • Thread safety — 멀티스레드 환경에서 올바른 작동을 위해 동기화 필요
  • 비판 — Singleton은 테스트를 복잡하게 만들고 숨겨진 의존성을 생성
  • 대안 — Dependency Injection, Service Locator로 Singleton 대체

Singleton이란: 싱글턴 패턴의 본질

Singleton — GoF(Gang of Four)가 1994년에 설명한 생성 디자인 패턴입니다. 이 패턴은 두 가지 문제를 해결합니다: 클래스의 인스턴스화를 단일 객체로 제한하고 해당 객체에 대한 전역 접근을 제공합니다. Singleton은 고유해야 하는 리소스(세션 팩토리, 이미지 캐시, 데이터베이스 연결 관리자, Crashlytics 또는 Analytics 클라이언트)에 유용합니다.

Singleton 구현에는 비공개 생성자(외부 생성 방지), 단일 인스턴스가 있는 정적 필드, 정적 접근 메서드(shared, instance, getInstance)가 필요합니다. 클라이언트는 객체 생성에 신경 쓰지 않고 Singleton.shared.method()를 호출합니다. 이 패턴은 iOS와 Android에서 널리 사용됩니다: URLSession.shared, UserDefaults.standard, FirebaseApp.sharedInstance — 모두 Singleton입니다. 그러나 Singleton의 과도한 사용은 Global State 안티패턴으로 이어집니다.

Singleton 문제 — 숨겨진 의존성(클래스가 암시적으로 Singleton 객체에 의존), 테스트 복잡성(추가 노력 없이 테스트에서 인스턴스를 교체할 수 없음), 단일 책임 원칙 위반(Singleton이 자신의 인스턴스와 비즈니스 로직을 모두 관리). 현대 모바일 개발에서는 단일 인스턴스 관리를 위해 DI(Dagger, Hilt, Swinject)를 선호합니다 — DI 컨테이너가 객체를 한 번 생성하고 생성자를 통해 주입합니다.

iOS Swift의 Singleton: shared와 정적 속성

Swift Singleton은 비공개 이니셜라이저와 함께 정적 shared 속성을 통해 구현됩니다. Swift 3부터 정적 속성의 지연 초기화는 스레드 안전이 보장됩니다 — 컴파일러가 dispatch_once를 통해 자동으로 동기화를 추가합니다. static let shared = Class()를 선언하고 init()을 비공개로 만들면 충분합니다. Swift는 초기화 후 단일 스레드 접근에 추가 동기화가 필요하지 않습니다.

swift
final class NetworkManager {
    // 스레드 안전 Singleton
    static let shared = NetworkManager()

    private init() {
        URLSessionConfiguration.default.timeoutIntervalForRequest = 30
    }

    private var cache = NSCache<NSString, NSData>()

    func fetchData(from url: URL) async throws -> Data {
        let key = url.absoluteString as NSString
        if let cached = cache.object(forKey: key) {
            return cached as Data
        }
        let (data, _) = try await URLSession.shared.data(from: url)
        cache.setObject(data as NSData, forKey: key)
        return data
    }
}

// 사용
let data = try await NetworkManager.shared.fetchData(from: url)

Apple Singleton — 많은 iOS SDK 객체가 Singleton을 사용합니다: UIApplication.shared, UIScreen.main, FileManager.default, NotificationCenter.default, UserDefaults.standard. Apple은 물리적으로 고유한 서비스(하나의 화면, 하나의 애플리케이션)에 Singleton을 사용합니다. 개발자들은 자신의 서비스에 이 패턴을 복사합니다. SwiftUI에서는 Singleton에 대한 전역 접근이 Environment와 @EnvironmentObject로 대체되어 테스트 용이성이 향상됩니다.

Android Kotlin의 Singleton: companion object와 object

Kotlin Singleton — 가장 간단한 방법: object 키워드가 첫 번째 접근 시 지연 초기화되는 싱글턴 클래스를 선언합니다. Kotlin object는 스레드 안전하며 추가 동기화가 필요하지 않습니다. 생성자 매개변수가 있는 Singleton이 필요한 경우 lazy 위임자와 함께 companion object가 사용됩니다. Android에서는 Application 컨텍스트와 Application.onCreate()를 통해 초기화되는 서비스에 Singleton이 자주 필요합니다.

kotlin
// 옵션 1: object — 매개변수 없는 간단한 Singleton
object AppPreferences {
    private val prefs = Application.instance
        .getSharedPreferences("app", Context.MODE_PRIVATE)

    var isFirstLaunch: Boolean
        get() = prefs.getBoolean("first_launch", true)
        set(value) = prefs.edit { putBoolean("first_launch", value) }
}

// 옵션 2: companion object — 매개변수가 있는 Singleton
class ApiClient private constructor(baseUrl: String) {
    companion object {
        @Volatile
        private var instance: ApiClient? = null

        fun getInstance(baseUrl: String): ApiClient {
            return instance ?: this.synchronized {
                instance ?: ApiClient(baseUrl).also { instance = it }
            }
        }
    }

    fun request(endpoint: String): String { /* ... */ }
}

Android SDK Singleton — 많은 Android 시스템 서비스가 Singleton을 구현합니다: context.getSystemService(), Room.databaseBuilder(), Retrofit.Builder(). 예로는 SharedPreferences, MediaPlayer, AudioManager가 있습니다. Android 애플리케이션에서 Singleton은 리포지토리, 관리자, 팩토리에 자주 사용됩니다. Google은 Singleton을 DI(Hilt, Koin)로 대체할 것을 권장하며, Singleton 스코프(Scope.Singleton 또는 @Singleton)는 컨테이너가 관리하고 클래스는 테스트 가능하게 유지됩니다.

Thread safety: dispatch_once, synchronized, lock

Thread safety — 멀티스레드 환경에서 Singleton에 중요한 요구사항입니다. 동기화 없이 두 스레드가 동시에 instance == null을 확인하고 두 개의 인스턴스를 생성할 수 있습니다. 해결책은 첫 번째 생성 중에 잠그고 초기화 후에 해제하는 것입니다. Swift에서 정적 속성(static let)은 기본적으로 스레드 안전합니다. Kotlin에서 object는 스레드 안전합니다. Kotlin에서 Java 스타일의 경우 synchronized 또는 @Volatile + double-check locking이 사용됩니다.

언어메커니즘스레드 안전성지연 초기화
Swiftstatic letdispatch_once (자동)예, 첫 번째 접근 시
Kotlin objectObject 선언클래스 초기화자 스레드 안전예, 첫 번째 접근 시
Kotlin companionsynchronized + @VolatileDouble-checked locking예, lazy 또는 synchronized 통해
Javasynchronized + volatileDouble-checked locking예, getInstance()에서

Double-checked locking — Singleton의 지연 초기화를 위한 패턴입니다. 첫 번째 검사는 동기화 없이(인스턴스가 이미 존재하면 빠름), 두 번째는 synchronized 내부(하나의 스레드만 생성). @Volatile은 모든 스레드에 대한 변경 가시성을 보장합니다. volatile이 없으면 다른 스레드가 부분적으로 생성된 객체를 볼 수 있습니다. Kotlin에서 LazyThreadSafetyMode.SYNCHRONIZED를 사용한 lazy 위임자는 자동으로 double-checked locking을 구현합니다.

Singleton vs Dependency Injection: 사용 시기

Dependency Injection — 단일 인스턴스 관리를 위한 Singleton의 대안입니다. DI 컨테이너(Dagger, Hilt, Koin, Swinject)는 Singleton 스코프에서 객체를 한 번 생성하고 생성자를 통해 주입합니다. 클래스는 자신의 Singleton 상태를 알지 못합니다 — 컨테이너가 결정합니다. 코드가 테스트 가능해집니다: 테스트에서 DI 모듈이 모의 모듈로 교체됩니다. DI 장점: 생성자의 명시적 의존성, 재정의 가능성, 통합된 라이프사이클.

Singleton이 정당화되는 경우 — 시스템 수준 객체: Crashlytics, Analytics, Logging. 이러한 서비스는 AppDelegate/Application에서 한 번 초기화되고 모든 곳에서 사용됩니다. 이들에게 DI는 과도합니다. Singleton은 이미지 캐시(NSCache, Coil, Glide)에도 편리하며, 전역 접근이 성능으로 정당화됩니다. 다른 모든 경우에는 DI가 선호됩니다: 의존성을 가시화하고 테스트와 리팩토링을 단순화합니다.

하이브리드 접근 방식 — 테스트를 위해 재정의 가능한 Singleton. Swift에서는 프로토콜 + 테스트가 교체할 수 있는 정적 속성(예: URLSession용 URLProtocol 통해). Kotlin에서는 주입 가능한 속성이 있는 open 클래스로, 테스트가 리플렉션 또는 세터를 통해 모의를 설정합니다. 이 접근 방식은 Singleton의 단순성을 유지하면서 테스트 기능을 제공합니다. Google은 Android에 Hilt를 권장, Apple은 iOS에 DI를 강요하지 않음 — 선택은 팀에 달려 있습니다.

자주 묻는 질문

Singleton은 안티패턴인가요?

아니요, Singleton은 GoF 패턴이지만, 빈번한 잘못된 사용이 Global State 안티패턴으로 만듭니다. Singleton은 물리적으로 고유한 리소스(화면, 프린터, 파일 시스템)에 정당화됩니다. Singleton이 데이터 관리에 사용될 때 문제가 발생합니다: 숨겨진 의존성, 테스트 복잡성, 단일 책임 원칙 위반. 현대적 대안은 Singleton 스코프를 가진 DI입니다.

Singleton을 사용하는 코드를 테스트하는 방법은?

세 가지 접근 방식: (1) 프로토콜 통해 — Singleton이 프로토콜 구현, 테스트가 구현 교체; (2) DI 통해 — Singleton이 생성자를 통해 의존성으로 주입; (3) reset 메서드 통해 — Singleton이 테스트에서 상태를 재설정하는 메서드 보유(테스트 빌드만). 첫 번째 접근 방식이 선호되고, 세 번째는 프로덕션에 위험합니다. Swift는 테스트에서 런타임 조작을 통해 shared 속성을 교체할 수 있습니다.

Kotlin object와 Java Singleton의 차이점은?

Kotlin object는 바이트코드 수준에서 Singleton을 생성하는 언어 구조입니다. 비공개 생성자와 getInstance()가 있는 Java 구현과 달리, object는 스레드 안전성, 지연 초기화를 보장하고 상속을 금지합니다. Java Singleton은 멀티스레드 환경에서 올바른 작동을 위해 수동 동기화(synchronized)와 volatile이 필요합니다. Kotlin object는 Android에서 가장 안전하고 간결한 방법입니다.

Singleton을 상속할 수 있나요?

Singleton 상속은 패턴을 위반합니다: Singleton 클래스를 상속할 수 있으면 하위 클래스가 두 번째 인스턴스를 생성하여 고유성을 위반할 수 있습니다. Swift에서 final class는 상속을 금지합니다. Kotlin object는 상속될 수 없습니다(object는 sealed). 가변성이 있는 Singleton이 필요한 경우 Singleton 스코프의 DI 컨테이너를 사용하세요: 단일 인스턴스를 보장하고 인터페이스를 통한 상속을 지원합니다.

Android에서 Singleton에 매개변수를 전달하는 방법은?

매개변수는 init(context: Application) 또는 getInstance(param)을 통해 전달됩니다. Kotlin object는 매개변수를 받지 않습니다 — 팩토리 메서드 getInstance(param)가 있는 companion object를 사용하세요. Hilt가 문제를 해결합니다: @Singleton + @Inject constructor(context: Application) — DI 컨테이너가 자동으로 Application 컨텍스트를 주입합니다. Retrofit 클라이언트의 경우 매개변수(baseUrl, interceptors)는 DI 모듈의 빌더를 통해 전달됩니다.

요약

  • Singleton — 단일 인스턴스와 전역 접근이 있는 패턴
  • Swift shared — 컴파일러 보장 스레드 안전성을 가진 static let
  • Kotlin object — 추가 코드 없는 지연 초기화
  • Thread safety — Java는 double-checked locking, Swift/Kotlin은 자동
  • Apple SDK — UIApplication.shared, UserDefaults.standard, FileManager.default
  • Android SDK — Retrofit, Room, SharedPreferences를 Singleton 관리자 통해
  • 대안 — 테스트 가능한 코드를 위한 Dependency Injection

턴키 방식의 모바일 애플리케이션을 개발해 드립니다

IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.

프로젝트 논의

더 읽어보기