Service Locator — 패턴의 본질, 중앙 레지스트리 및 DI

저자: IT Sectr 게시일: 2026-02-18 읽는 시간: 9 분

Service Locator는 서비스의 중앙 레지스트리를 제공하는 아키텍처 패턴입니다. 클라이언트 코드는 직접 생성하거나 생성자를 통해 받지 않고 정적 로케이터를 통해 서비스를 요청합니다. Service Locator는 종종 Dependency Injection의 대안으로 간주됩니다. 구현은 간단하지만 의존성을 숨기고 테스트를 복잡하게 만듭니다. 이 패턴은 서비스 등록 및 해결을 위한 전역 Singleton 클래스를 통해 구현됩니다. 자세한 내용은 — Martin Fowler의 DI와 Service Locator 비교를 참조하세요.

핵심 요점

  • Service Locator — 서비스를 얻기 위한 중앙 레지스트리
  • DI의 대안 — 구현은 간단하지만 클라이언트로부터 의존성을 숨깁니다
  • 안티패턴 — 많은 개발자들이 숨겨진 의존성 때문에 Service Locator를 안티패턴으로 간주합니다
  • 전역 접근 — 로케이터는 생성자를 통해 전달하지 않고 어디서나 정적으로 접근 가능합니다
  • 테스트 — DI보다 복잡함: 각 테스트마다 전역 로케이터 구성 필요

Service Locator란: 패턴의 본질과 구조

Service Locator는 서비스의 생성과 제공을 중앙화하는 패턴입니다. 서비스 레지스트리를 포함하는 Singleton 클래스(Locator)를 기반으로 합니다. 키는 서비스 유형(또는 식별자)이고 값은 구체적인 구현인 딕셔너리입니다. 클라이언트 코드는 ServiceLocator.resolve(ServiceProtocol.self)를 호출하고 준비된 인스턴스를 받습니다. 패턴은 서비스 생성 방법(팩토리, DI 컨테이너 또는 로케이터 내부의 new)을 규정하지 않습니다.

패턴 구조 — 레지스트리([String: Any] 타입의 딕셔너리), 로케이터(registerresolve를 가진 정적 클래스), 서비스(등록되는 서비스). 레지스트리는 팩토리(객체 생성을 위한 클로저/람다) 또는 준비된 인스턴스를 저장할 수 있습니다. 로케이터는 전역(애플리케이션당 하나) 또는 범위 지정(기능/모듈별)이 가능합니다. 서비스 해결은 타입별 딕셔너리 조회입니다. Swift와 Kotlin은 메타타입을 통해 타입을 키로 사용합니다: ObjectIdentifier(ServiceProtocol.self).

구성 요소책임Swift/Kotlin
ServiceLocator서비스에 대한 전역 접근class ServiceLocator
Registry팩토리/인스턴스 저장소[ObjectIdentifier: Any]
Service구체적인 구현NetworkService()

패턴의 역사 — Service Locator는 Java Patterns(1998) 책과 이후 Core J2EE Patterns(2001)에서 설명되었습니다. Martin Fowler의 2004년 기사는 Service Locator를 DI와 비교하며, Service Locator가 "더 간단한 대안이지만 테스트에는 더 나쁘다"고 언급합니다. 모바일 개발에서는 Dagger와 Swinject가 등장하기 전에 초기 Android 프로젝트와 iOS 앱에서 Service Locator가 사용되었습니다. 현재 이 패턴은 레거시 프로젝트와 프로토타입에서 더 흔히 발견됩니다.

Swift의 Service Locator: 전역 서비스 레지스트리

Swift의 Service Locator — 정적 속성과 스레드 안전 딕셔너리를 통한 구현. ObjectIdentifier(Protocol.self)를 키로, 팩토리(() -> Any)를 값으로 하는 레지스트리가 사용됩니다. 지연 초기화(lazy var)는 표준 관행입니다: 서비스는 첫 번째 요청 시 생성됩니다. Swift는 resolve 시 명시적 타입 캐스팅이 필요합니다: guard let service = locator.resolve(ServiceProtocol.self) else { return }.

swift
final class ServiceLocator {
    static let shared = ServiceLocator()
    private var registry: [ObjectIdentifier: Any] = [:]
    private let lock = NSLock()

    func register<T>(_ type: T.Type, factory: @escaping () -> T) {
        lock.lock()
        registry[ObjectIdentifier(type)] = factory
        lock.unlock()
    }

    func resolve<T>(_ type: T.Type) -> T {
        lock.lock()
        defer { lock.unlock() }
        guard let factory = registry[ObjectIdentifier(type)] as? () -> T else {
            fatalError("Service \(type) not registered")
        }
        return factory()
    }

    func reset() {
        lock.lock()
        registry.removeAll()
        lock.unlock()
    }
}

// 등록
ServiceLocator.shared.register(NetworkServiceProtocol.self) { NetworkService() }

// 사용
let network = ServiceLocator.shared.resolve(NetworkServiceProtocol.self)

범위 관리 — 로케이터는 팩토리(transient — 매번 새 객체) 또는 준비된 인스턴스(singleton)를 저장할 수 있습니다. 팩토리의 경우 각 resolve 시 호출되는 클로저가 등록됩니다. Singleton의 경우 생성된 인스턴스를 캡처하는 클로저가 사용됩니다. 범위 추가: .transient, .singleton, .weak(약한 참조 — 누군가 참조를 유지하는 동안 객체가 살아 있음). UIKit UIViewController에서 pop/dismiss 시 누수를 방지하기 위해 Weak 범위가 편리합니다.

Kotlin의 Service Locator: 지연 등록

Kotlin의 Service Locator — 타입 안전성을 위한 inline reified 함수와 함께 object(Singleton)를 통한 간결한 구현. Kotlin은 위임자를 사용한 간결한 로케이터를 가능하게 합니다: val service by locator(). Reified generics()는 ObjectIdentifier를 대체합니다 — 타입은 제네릭에서 얻습니다. Kotlin 로케이터는 명시적 잠금 없이 스레드 안전성을 위해 ConcurrentHashMap을 자주 사용합니다.

kotlin
object ServiceLocator {
    private val registry = ConcurrentHashMap<Class<*>, () -> Any>()

    inline fun <reified T: Any> register(noinline factory: () -> T) {
        registry[T::class.java] = factory
    }

    @Suppress("UNCHECKED_CAST")
    inline fun <reified T: Any> resolve(): T {
        val factory = registry[T::class.java]
            ?: throw IllegalStateException("Service ${T::class.simpleName} not registered")
        return factory() as T
    }

    fun clear() {
        registry.clear()
    }
}

// 등록
ServiceLocator.register<ApiService> { RetrofitApiService() }

// 클래스에서 사용
class UserRepository {
    private val api: ApiService = ServiceLocator.resolve()
    private val db: Database = ServiceLocator.resolve()
}

// 지연 해결을 위한 위임자
class LocatorDelegate<reified T: Any> : Lazy<T> {
    override val value: T get() = ServiceLocator.<T>resolve()
    override fun isInitialized(): Boolean = true
}

inline fun <reified T: Any> locator(): Lazy<T> = LocatorDelegate()
// 사용: val api by locator()

Android의 Service Locator — Dagger 이전의 오래된 프로젝트에서 발견됩니다. Jetpack Hilt와 Koin은 Android 커뮤니티에서 Service Locator를 대체했습니다. 그러나 로케이터는 단위 테스트에서 여전히 관련성이 있습니다: Hilt 없이 모의 서비스를 사용하는 간단한 ServiceLocator 스텁. 장점: 테스트를 위해 Dagger 컴파일을 기다릴 필요가 없습니다. 단점: 테스트에서 로케이터 재정의를 잊으면 테스트가 프로덕션 서비스를 사용합니다.

Service Locator vs DI: 비교 및 선택 기준

의존성의 명시성 — 주요 차이점. DI는 의존성을 명시적으로 선언합니다: init(service: ServiceProtocol) — 모든 IDE가 클래스 의존성을 표시합니다. Service Locator는 이를 숨깁니다: dependencies = ServiceLocator.resolve() — 메서드 내부에 숨겨짐. DI를 사용하면 모든 클래스 의존성을 즉시 볼 수 있습니다. Service Locator를 사용하면 전체 클래스 본문을 읽어야 합니다. 이로 인해 Service Locator 코드의 예측 가능성이 낮아집니다: 레지스트리를 변경하면 로케이터를 사용하는 모든 클래스가 손상될 수 있습니다.

특성Service LocatorDependency Injection
의존성 가시성메서드 본문 내에 숨겨짐생성자에서 명시적
테스트전역 레지스트리 구성생성자에 모의 객체
모듈성전역 레지스트리 — 모듈화되지 않음별도 컨테이너를 가진 모듈
복잡성간단한 구현, 50-100줄Dagger/Swinject 필요
시장 출시 시간빠른 시작컨테이너 설정 필요

Service Locator가 정당화되는 경우 — 프로토타입 및 MVP(설정 없이 빠른 시작). DI 프레임워크를 추가할 수 없는 레거시 프로젝트(복잡한 빌드, 린터 제약). 계측 라이브러리(로깅, 충돌 보고) — 이미 전역적입니다. 3명 이상의 개발자 팀이 있는 프로덕션 애플리케이션의 경우 DI가 권장됩니다: 명시적 의존성은 리팩토링 중 오류 수를 줄이고 새 개발자 온보딩을 간소화합니다.

Service Locator의 문제점 및 대안

숨겨진 의존성 — 메서드 내에서 ServiceLocator.resolve()를 사용하는 클래스는 정적으로 분석할 수 없습니다. IDE는 의존성을 표시하지 않고, 컴파일러는 서비스가 등록되었는지 확인하지 않습니다. "Service not registered" 오류는 런타임에만 발생합니다. 리팩토링이 위험해집니다: 레지스트리에서 서비스를 제거하면 애플리케이션의 모든 클래스가 손상될 수 있습니다. DI는 컴파일 타임 검사(Dagger) 또는 명시적 생성자를 통해 이 문제를 해결합니다.

테스트 문제 — 각 테스트는 모든 의존성으로 ServiceLocator.shared를 구성해야 합니다. 테스트 후 — 상태를 재설정합니다. 병렬 테스트 실행 중 ServiceLocator.shared의 전역 상태는 경쟁 조건을 초래합니다: 한 테스트가 모의 객체를 등록하고 다른 테스트가 다른 사람의 모의 객체를 받습니다. 해결책: 범위 지정 로케이터(테스트당 하나) 또는 ThreadLocal. DI는 처음부터 이 문제를 해결합니다: 각 테스트가 모의 의존성으로 자체 인스턴스를 생성합니다.

swift
// Service Locator 테스트 문제
class LoginViewModelTests: XCTestCase {
    override func setUp() {
        super.setUp()
        // 테스트용 전역 레지스트리 설정
        ServiceLocator.shared.register(AuthServiceProtocol.self) { MockAuthService() }
    }

    override func tearDown() {
        ServiceLocator.shared.reset()
        super.tearDown()
    }

    func testLogin() {
        let viewModel = LoginViewModel() // 내부적으로 ServiceLocator 사용
        // 테스트...
    }
}

Service Locator의 대안 — DI(Dagger, Swinject), Factory Method, Ambient Context. Factory Method — 전역 상태가 없는 간단한 패턴: 팩토리 클래스가 서비스를 생성하고 생성자를 통해 전달됩니다. Ambient Context — 교차 관심사(로깅, 권한 부여)를 위한 스레드 안전 대안. 가장 좋은 대안은 DI 프레임워크 없이 수동 팩토리를 사용한 Constructor Injection입니다: 생성자 전달과 함께 팩토리에서 의존성을 명시적으로 생성하면 Dagger/Swinject 설정의 복잡성 없이 DI의 명확성을 제공합니다.

자주 묻는 질문

Service Locator는 안티패턴인가요?

많은 개발자들은 Service Locator가 의존성을 숨기고, 테스트를 복잡하게 만들며, 클래스 간의 숨겨진 결합을 생성하기 때문에 안티패턴으로 간주합니다. 그러나 프로토타입, 소규모 프로젝트 및 전역 서비스(로깅, 분석)의 경우 Service Locator가 정당화될 수 있습니다. 결정은 컨텍스트에 따라 다릅니다: 팀이 있는 프로덕션 애플리케이션에는 DI, 프로토타입의 단독 개발자에게는 Service Locator입니다.

Service Locator와 DI 컨테이너의 차이점은?

DI 컨테이너(Dagger, Swinject)는 객체에 자동으로 의존성을 주입합니다 — 객체는 컨테이너의 존재를 알지 못합니다. Service Locator — 객체 자체가 레지스트리에서 의존성을 요청합니다. DI 컨테이너는 IoC 원칙을 따릅니다. Service Locator는 이를 위반합니다: 객체가 자신의 의존성 획득을 관리합니다. DI 컨테이너는 객체 생성 전(생성자 통해)에 작동하고, Service Locator는 코드 어디에서나 작동합니다.

iOS에서 Service Locator는 언제 사용하나요?

Service Locator는 iOS에서 다음과 같은 경우 정당화됩니다: 전역 서비스(Analytics, Logger, Crashlytics), Swinject 설정이 과도한 프로토타입, 대규모 레거시 모듈의 단위 테스트. 새 iOS 프로젝트에는 Swinject 또는 생성자를 통한 수동 DI가 권장됩니다. @Environment와 함께하는 SwiftUI도 DI의 한 형태로, Service Locator를 피합니다.

Service Locator에서 경쟁 조건을 피하려면?

스레드 안전 컬렉션을 사용하세요(Swift의 NSLock, Kotlin의 ConcurrentHashMap). 테스트용 — ThreadLocal 또는 범위 지정 로케이터. 대안: 비동기 로컬 저장소 — 서비스가 코루틴/액터에 바인딩됩니다. 가장 좋은 해결책은 병렬 테스트에 Service Locator를 피하고 각 테스트에 명시적 객체 생성과 함께 DI를 사용하는 것입니다.

Service Locator는 싱글톤인가요?

Service Locator는 일반적으로 Singleton으로 구현되지만 필수는 아닙니다. 모듈용 로케이터 인스턴스(기능 범위 지정 로케이터)를 생성하고 생성자를 통해 전달할 수 있습니다. 기능 범위 지정 로케이터는 전역 상태 문제를 해결하지만 숨겨진 의존성 문제는 해결하지 않습니다. 이 패턴을 Ambient Context 또는 Scoped Locator라고 합니다.

요약

  • Service Locator — 정적 접근을 통해 서비스를 얻기 위한 중앙 레지스트리
  • 숨겨진 의존성 — 주요 단점: 의존성이 클래스 시그니처에 표시되지 않음
  • 테스트 — 전역 상태 및 재설정 필요성으로 인해 DI보다 복잡함
  • Swift/Kotlin — 스레드 안전 딕셔너리 및 reified generics를 통한 구현
  • 대안 — DI(Dagger Hilt, Swinject) — 프로덕션 프로젝트의 표준
  • 정당화됨 — 프로토타입, 전역 서비스 및 레거시 프로젝트에서

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

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

프로젝트 논의

더 읽어보기