고급 안드로이드 패턴
이 가이드는 Koin을 사용한 안드로이드 전용 고급 의존성 주입(dependency injection) 패턴을 다룹니다.
INFO
플랫폼에 독립적인 패턴(컬렉션, 데코레이터, 제네릭 타입, 순환 의존성)에 대해서는 정의(Definitions) 및 모듈(Modules)을 참조하세요.
싱글톤에서의 안드로이드 컨텍스트(Context)
Activity 누수 방지
kotlin
// ❌ 나쁨 - 싱글톤을 통한 Activity 누수 발생
module {
single { SomeService(get<Activity>()) }
}
// ✅ 좋음 - Application 컨텍스트 사용
module {
single { SomeService(androidContext()) }
}
// ✅ 좋음 - Activity에 종속된 의존성에는 scoped 사용
module {
activityScope {
scoped { ActivityBoundService() }
}
}컨텍스트 타입
kotlin
module {
// Application 컨텍스트 - 싱글톤에 안전함
single { DatabaseHelper(androidContext()) }
// Application 인스턴스
single { AppConfig(androidApplication()) }
}BuildConfig를 이용한 조건부 바인딩
빌드 변리언트(Build Variant)
kotlin
fun createLogger(): Logger =
if (BuildConfig.DEBUG) DebugLogger() else ReleaseLogger()
val loggingModule = module {
single { create(::createLogger) }
}또는 어노테이션 사용 시:
kotlin
@Module
class LoggingModule {
@Single
fun provideLogger(): Logger =
if (BuildConfig.DEBUG) DebugLogger() else ReleaseLogger()
}애널리틱스 토글
kotlin
fun createAnalyticsService(): AnalyticsService =
if (BuildConfig.ENABLE_ANALYTICS) GoogleAnalytics() else NoOpAnalytics()
val analyticsModule = module {
single { create(::createAnalyticsService) }
}피처 플래그(Feature Flags)
kotlin
@Singleton
class PaymentProcessor(
private val featureFlags: FeatureFlagService,
private val newProcessor: NewPaymentProcessor,
private val legacyProcessor: LegacyPaymentProcessor
) {
fun process(amount: Double) {
if (featureFlags.isEnabled("new_payment_flow")) {
newProcessor.process(amount)
} else {
legacyProcessor.process(amount)
}
}
}안드로이드 Dialog 프로바이더
안드로이드 UI 컴포넌트를 위한 팩토리를 생성합니다:
kotlin
@Factory
class DialogProvider(private val context: Context) {
fun createConfirmDialog(title: String, onConfirm: () -> Unit): AlertDialog =
AlertDialog.Builder(context)
.setTitle(title)
.setPositiveButton("OK") { _, _ -> onConfirm() }
.create()
fun createErrorDialog(message: String): AlertDialog =
AlertDialog.Builder(context)
.setTitle("Error")
.setMessage(message)
.setPositiveButton("OK", null)
.create()
}
class MainActivity : AppCompatActivity() {
private val dialogProvider: DialogProvider by inject()
fun showConfirmation() {
dialogProvider.createConfirmDialog("Confirm") { /* action */ }.show()
}
}계층적 스코프(Hierarchical Scopes)
공유 액세스를 위해 안드로이드 스코프를 연결합니다:
kotlin
val appModule = module {
single { Database() }
scope(named("session")) {
scoped { UserSession() }
}
scope(named("shopping")) {
scoped { ShoppingCart(get()) }
}
}
// 스코프 생성 및 연결
val sessionScope = getKoin().createScope("user_session", named("session"))
val shoppingScope = getKoin().createScope("cart", named("shopping"))
shoppingScope.linkTo(sessionScope)
// 장바구니는 연결된 스코프에서 UserSession에 접근 가능
val cart = shoppingScope.get<ShoppingCart>()동적 피처 레지스트리(Dynamic Feature Registry)
설정을 기반으로 컬렉션을 빌드합니다:
kotlin
@Singleton
class FeatureRegistry(private val config: AppConfig) : KoinComponent {
fun getEnabledFeatures(): List<Feature> {
return config.enabledFeatures.mapNotNull { name ->
getKoin().getOrNull<Feature>(named(name))
}
}
}흔한 안드로이드 실수(Pitfalls)
숨겨진 순환 호출
kotlin
// ⚠️ Lazy는 순환을 숨기지만 런타임에 무한 루프가 발생합니다
class ServiceA : KoinComponent {
private val serviceB: ServiceB by inject()
fun doA() { serviceB.doB() }
}
class ServiceB : KoinComponent {
private val serviceA: ServiceA by inject()
fun doB() { serviceA.doA() } // 무한 루프!
}ViewModel 스코프 혼동
kotlin
// ❌ 나쁨 - Activity 스코프 내의 ViewModel은 회전 시 상태를 잃음
module {
activityScope {
scoped { UserViewModel(get()) }
}
}
// ✅ 좋음 - 올바른 생명주기를 위해 viewModel 사용
module {
viewModel { UserViewModel(get()) }
}싱글톤에 Activity 주입
kotlin
// ❌ 메모리 누수 - 싱글톤 내 Activity 참조
@Singleton
class ImageLoader(private val activity: Activity)
// ✅ Application 컨텍스트 사용
@Singleton
class ImageLoader(private val context: Context) // androidContext()를 통한 Application 컨텍스트