아티클 목록으로 가기

학습: 코틀린으로 간단한 의존성 주입 컨테이너 직접 구현하기

skydovesJaewoong Eum (skydoves)||5분 소요

학습: 코틀린으로 간단한 의존성 주입 컨테이너 직접 구현하기

의존성 주입(Dependency Injection, DI)은 소프트웨어 설계의 핵심 패턴으로, 느슨한 결합(loose coupling)을 촉진하고 애플리케이션의 테스트 용이성과 확장성을 높여 줍니다. Hilt이나 Koin 같은 강력한 라이브러리가 프로덕션 안드로이드 앱에서 표준으로 자리 잡고 있지만, 간단한 DI 컨테이너를 직접 만들어 보는 것은 매우 가치 있는 학습 경험입니다. 이 과정을 통해 라이브러리가 내부적으로 수행하는 "마법"의 실체를 파악할 수 있고, DI의 핵심 개념인 "클래스가 스스로 의존성을 생성하는 대신 외부에서 주입받는다"는 원칙을 확실하게 체득할 수 있습니다.

이번 학습에서는 기본적인 생명주기 인식(lifecycle-aware) DI 컨테이너를 설계하고 구현해 보겠습니다. 최종적으로 만들 컨테이너는 다음과 같은 기능을 갖추게 됩니다.

  1. UserRepositoryAnalyticsService 같은 의존성을 등록할 수 있습니다.
  2. 필요한 시점에 해당 의존성의 인스턴스를 제공합니다.
  3. 싱글톤과 같은 의존성 스코프(scope)를 관리하여 인스턴스 생명주기를 제어합니다.
  4. 안드로이드 ViewModel 아키텍처와 자연스럽게 통합됩니다.

Step 1: 핵심 DIContainer 설계

컨테이너의 핵심은 의존성을 보관하고 생성하는 역할을 담당하는 클래스입니다. 등록된 의존성을 저장하는 가장 간단한 방법은 Map을 활용하는 것으로, 키에는 클래스 타입(KClass)을, 값에는 해당 클래스의 인스턴스를 생성하는 팩토리 람다를 저장합니다.

아울러 서로 다른 스코프도 관리해야 합니다. 이번 학습에서는 가장 일반적인 싱글톤 스코프(singleton scope), 즉 하나의 의존성에 대해 단 하나의 인스턴스만 생성되는 방식에 집중하겠습니다.

import kotlin.reflect.KClass

/**
 * 간단한 의존성 주입 컨테이너입니다.
 */
object DIContainer {

    // 의존성의 팩토리 람다를 보관하는 맵입니다.
    // 키는 클래스 타입이고, 값은 인스턴스를 생성하는 함수입니다.
    private val factories = mutableMapOf<KClass<*>, () -> Any>()

    // 이미 생성된 싱글톤 인스턴스를 캐싱하는 맵입니다.
    private val singletons = mutableMapOf<KClass<*>, Any>()

    /**
     * 의존성을 싱글톤으로 등록합니다.
     * 전달된 팩토리 람다는 인스턴스를 생성할 때 단 한 번만 호출됩니다.
     *
     * @param type 등록할 의존성의 클래스 타입입니다.
     * @param factory 의존성 인스턴스를 생성하는 람다 함수입니다.
     */
    inline fun <reified T : Any> registerSingleton(noinline factory: () -> T) {
        factories[T::class] = factory
    }

    /**
     * 등록된 의존성을 해석(resolve)하여 인스턴스를 제공합니다.
     * 싱글톤의 경우, 캐싱된 인스턴스를 반환하거나
     * 아직 생성되지 않았다면 새로 만들어 반환합니다.
     *
     * @return 요청한 의존성의 인스턴스입니다.
     * @throws IllegalStateException 의존성이 등록되지 않은 경우 발생합니다.
     */
    inline fun <reified T : Any> resolve(): T {
        // 먼저 싱글톤 인스턴스가 이미 존재하는지 확인합니다.
        val existingSingleton = singletons[T::class]
        if (existingSingleton != null) {
            return existingSingleton as T
        }

        // 존재하지 않으면 생성을 위한 팩토리를 찾습니다.
        val factory = factories[T::class]
            ?: throw IllegalStateException("Cannot resolve dependency of type ${T::class.simpleName}. Not registered.")

        // 팩토리를 사용하여 새 인스턴스를 생성합니다.
        val newInstance = factory() as T

        // 이후 요청을 위해 새 인스턴스를 캐싱합니다.
        singletons[T::class] = newInstance
        return newInstance
    }

    // 의존성을 초기화하는 헬퍼 함수로, 테스트 시 유용합니다.
    fun reset() {
        factories.clear()
        singletons.clear()
    }
}

이 아티클은 구독자 전용입니다

Dove Letter를 구독하시면 안드로이드, 코틀린 개발 관련 독점 아티클의 전체 내용을 볼 수 있습니다.

구독하기
아티클 목록으로 가기