Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Metro 마이그레이션 가이드

Metro 마이그레이션 가이드

MashUp 16기 안드로이드 세미나 자료

Avatar for Jaesung Lee

Jaesung Lee

March 20, 2026
Tweet

More Decks by Jaesung Lee

Other Decks in Programming

Transcript

  1. 필요 기반 지식 Session 1. DI가 무엇인지 이해하고 있다. 2.

    DI 프레임워크로 Hilt를 사용한 경험이 있다. 3. Dagger와 Hilt에서 지원하는 Annotation들이 어떤게 있는지 알고있다. • @Binds, @Provides, @HiltAndroidApp, @AndroidEntryPoint, .. 4. ViewModel이 생성되는 과정을 알고있다.
  2. 🤔 의존성 주입이 뭐였더라? Session 오브젝트 - 조영호 • 과도한

    의존성은 수정하기 어렵게 만들기 때문에 필요한 의존성만 유지하면서 변경을 방해하는 의존성을 제거하는게 객체지향 설계의 핵심 • 추상화 의존을 통해 컴파일 타임 의존성과 런타임 의존성을 분리해 컨텍스트 독립성을 확보해야 한다. 의존성 주입(DI)은 외부 객체의 의존성을 제공하는 방법이며, 외부 객체에 대한 생성과 사용에 대한 관심을 분리하는 것에 의의가 있다.
  3. class MashUp { val team = AndroidTeam() fun printTeam() {

    Log.d("MashUp", "팀이름: ${team.name}") } } class AndroidTeam() { val name: String = "Android팀" } 예시) 매쉬업 안드팀을 KMP팀으로 교체하기 Session
  4. interface Team { val name: String } class AndroidTeam :

    Team { override val name: String = "Android팀" } class KmpTeam : Team { override val name: String = "KMP팀" } class MashUp( private val team: Team, ) { fun printTeam() { Log.d("MashUp", "팀이름: ${team.name}") } } Session 예시) 매쉬업 안드팀을 KMP팀으로 교체하기
  5. interface Team { val name: String } class AndroidTeam :

    Team { override val name: String = "Android팀" } class KmpTeam : Team { override val name: String = "KMP팀" } class MashUp( private val team: Team, ) { fun printTeam() { Log.d("MashUp", "팀이름: ${team.name}") } } MashUp이 의존하는 Team에 대한 추상화 Session 예시) 매쉬업 안드팀을 KMP팀으로 교체하기
  6. interface Team { val name: String } class AndroidTeam :

    Team { override val name: String = "Android팀" } class KmpTeam : Team { override val name: String = "KMP팀" } class MashUp( private val team: Team, ) { fun printTeam() { Log.d("MashUp", "팀이름: ${team.name}") } } MashUp은 구체타입이 아닌 추상화에 의존! 교체가 쉽다~ (컨텍스트 독립성 확보) Session 예시) 매쉬업 안드팀을 KMP팀으로 교체하기
  7. 🤔 그럼 객체 생성은 어디서 어케함? Session class MashUp( private

    val team: Team, ) { fun printTeam() { Log.d("MashUp", "팀이름: ${team.name}") } } fun main() { val team = AndroidTeam() val mashUp = MashUp(team) mashUp.printTeam() } • 결국 어디선가 객체를 생성해서 수동 주입 해야함 • Hilt는 주입할 객체를 생성하기 위해 @HiltAndroidApp 어노테이션을 사용 • 컴파일 타임에 필요한 의존성을 만들어놓고 Application#onCreate시점에 의존성 그래프를 초기화함 DI 프레임워크를 사용하는 이유다!
  8. Session @Generated("dagger.hilt.android.processor.internal.androidentrypoint.ApplicationGenerator") public abstract class Hilt_TestApplication extends Application implements GeneratedComponentManagerHolder

    { .. @CallSuper @Override public void onCreate() { hiltInternalInject(); super.onCreate(); } protected void hiltInternalInject() { if (!injected) { injected = true; ((TestApplication_GeneratedInjector) componentManager.generatedComponent()) .injectTestApplication(UnsafeCasts.<Test>unsafeCast(this)); } } } @HiltAndroidApp 간단 내부 동작
  9. 🤔 Metro가 뭐에요? Session • Kotlin Compiler Plugin에 의해 동작되는

    컴파일 타임 DI 프레임워크 • 기존 DI 프레임워크들의 여러 장점들을 결합해 만든 Kotlin 친화적 DI 프레임워크 • 멀티플랫폼 지원 (JVM, JS, WASM, Native)
  10. 🤔 Hilt와의 차이점은 뭔가요? Session • Hilt는 Annotation Processor (KAPT,

    KSP)를 기반으로 동작 • 중간 여러 단계를 거쳐 generated code가 생성됨 • Metro는 Kotlin Compiler에 의해 동작, IR과정을 거쳐 .class파일 생성 (KAPT나 KSP가 없어도 됨) Hilt (KSP) Metro
  11. 🤔 Hilt와의 차이점은 뭔가요? Session • Hilt는 Annotation Processor (KAPT,

    KSP)를 기반으로 동작 • 중간 여러 단계를 거쳐 generated code가 생성됨 • Metro는 Kotlin Compiler에 의해 동작, IR과정을 거쳐 .class파일 생성 (KAPT나 KSP가 없어도 됨) Hilt (KSP) Metro Dagger, Hilt와 유사한 내용도 많으니 어떤게 비슷한지 잘 비교해봅시다 !
  12. @DependencyGraph Session • Metro의 진입점에 해당하는 그래프 • Compiler Plugin에서

    의존성에 따라 컴파일 타임에 구성 : Compiler 내장함수를 통해 구성하게 됨 (createGraph) • Dagger에서는 @Component와 동일한 역할 : @Module에서 생성된 의존성을 주입하기 위한 브릿지 인터페이스 역할 @DependencyGraph interface AppGraph { val app: WeatherApp } fun main() { val app = createGraph<AppGraph>.app }
  13. @DependencyGraph Session • Metro의 진입점에 해당하는 그래프 • Compiler Plugin에서

    의존성에 따라 컴파일 타임에 구성 : Compiler 내장함수를 통해 구성하게 됨 (createGraph) • Dagger에서는 @Component와 동일한 역할 : @Module에서 생성된 의존성을 주입하기 위한 브릿지 인터페이스 역할 • 그래프에 초기 입력 값이 필요한 경우 Factory 선언 가능 : 이 때는 createGraphFactory를 통해 그래프 구성 @DependencyGraph interface AppGraph { val app: WeatherApp @DependencyGraph.Factory fun interface Factory { fun create(@Provides useMetric: Boolean): AppGraph } } fun main() { val app = createGraphFactory<AppGraph.Factory>() .create(useMetric = true) .app }
  14. @DependencyGraph Session • Metro의 진입점에 해당하는 그래프 • Compiler Plugin에서

    의존성에 따라 컴파일 타임에 구성 : Compiler 내장함수를 통해 구성하게 됨 (createGraph) • Dagger에서는 @Component와 동일한 역할 : @Module에서 생성된 의존성을 주입하기 위한 브릿지 인터페이스 역할 • 그래프에 초기 입력 값이 필요한 경우 Factory 선언 가능 : 이 때는 createGraphFactory를 통해 그래프 구성 • 입력 값 매개변수에는 @Provides나 @Includes가 있어야 함 @DependencyGraph interface AppGraph { val app: WeatherApp @DependencyGraph.Factory fun interface Factory { fun create(@Provides useMetric: Boolean): AppGraph } } fun main() { val app = createGraphFactory<AppGraph.Factory>() .create(useMetric = true) .app }
  15. @Includes Session @Includes • 그래프 내에서 사용 가능한 인스턴스의 바인딩을

    담는 컨테이너 제공 • enum을 제외한 모든 클래스 유형에 대해 바인딩 가능함 @DependencyGraph interface AppGraph { val app: WeatherApp @DependencyGraph.Factory fun interface Factory { fun create(@Includes subGraph: ForecastGraph): AppGraph } @DependencyGraph interface ForecastGraph { val forecast: String @Provides fun provideForecast(): String = "Sunshine!" } }
  16. @Provides Session • 그래프 내에서 사용 가능한 인스턴스의 바인딩 제공

    @DependencyGraph interface AppGraph { val app: WeatherApp @Provides fun provideCache(): Cache = Cache() }
  17. @Provides Session • 그래프 내에서 사용 가능한 인스턴스의 바인딩 제공

    • 슈퍼 타입에 정의하면 여러 그래프에서 재사용할 수 있음 ◦ 단, override 형태의 바인딩 제공은 compile error! interface CacheProvider { @Provides fun provideCache(): Cache = Cache() } @DependencyGraph interface AppGraph : CacheProvider { val app: WeatherApp // DO NOT // override fun provideCache(): Cache = FakeCache() }
  18. @Provides Session • 그래프 내에서 사용 가능한 인스턴스의 바인딩 제공

    • 슈퍼 타입에 정의하면 여러 그래프에서 재사용할 수 있음 ◦ 단, override 형태의 바인딩 제공은 compile error! • 동일 타입의 여러 바인딩을 제공할 경우 @Qualifier 사용 가능 ◦ 간편하게 @Named를 통해 Type Key로 활용 가능 @Qualifier annotation class RealCache() interface CacheProvider { @RealCache @Provides fun provideRealCache(): Cache = RealCache() @Named("TestableCache") @Provides fun provideTestableCache(): Cache = TestableCache() } @DependencyGraph interface AppGraph : CacheProvider { val app: WeatherApp }
  19. Injection : @Inject Session 생성자 주입 • 가장 일반적인 방법

    • 여러 생성자에 대한 바인딩이 필요할 경우 클래스 바인딩 형태로 사용 가능 @Inject class HttpClient(private val cache: Cache) // ========================================== class HttpClient( private val cache: Cache, private val timeout: Duration, ) { @Inject constructor(cache: Cache): this(cache, 30.seconds) }
  20. Injection : @Inject Session 멤버 주입 • 생성자 주입이 불가능한

    클래스 (Android Activity)에 인스턴스를 주입하는 용도 • 프로퍼티, 메서드 주입 지원 • Dagger와는 다르게 멤버 주입 프로퍼티는 private해야 함 class ProfileActivity : ComponentActivity() { // property injection @Inject private lateinit var db: UserDatabase @Inject private var notification: Notification? = null // function injection @Inject private fun injectUser(user: User) { .. } }
  21. Injection : @AssistedInject Session 보조 주입 • 바인딩 제공 시

    런타임 의존성이 필요한 경우 사용 • 모든 바인딩이 반드시 Graph상에 필요한게 아닌, 런타임에 동적으로 제공해 인스턴스를 제공할 수 있음 @AssistedInject class HttpClient( @Assisted val timeout: Duration, val cache: Cache ) { @AssistedFactory fun interface Factory { fun create(timeout: Duration): HttpClient } } @Inject class ApiClient(httpClientFactory: HttpClient.Factory) { private val httpClient = httpClientFactory.create(30.seconds) }
  22. @DependencyGraph interface AppGraph { val mashUp: MashUp @DependencyGraph.Factory fun interface

    Factory { fun create(@Provides team: Team): AppGraph } } fun main() { val mashUp = createGraphFactory<AppGraph.Factory>() .create(team = KMPTeam()) .mashUp mashUp.printTeam() } interface Team { val name: String } class AndroidTeam : Team { override val name: String get() = "Android Team" } class KMPTeam : Team { override val name: String get() = "KMP Team" } @Inject class MashUp(private val team: Team) { fun printTeam() { Log.d("MashUp", "팀이름 : ${team.name}") } } 🤔 그렇다면 KMP 팀은 어떻게 구성될 수 있나? Session
  23. Scoping : @SingleIn Session • @Scope : 컴파일러에 Graph의 수명

    동안 하나의 인스턴스만 생성되도록 보장하는 마커 어노테이션 • @SingleIn : Metro의 표준 스코프 어노테이션 @Target(AnnotationTarget.ANNOTATION_CLASS) public annotation class Scope @Scope public annotation class SingleIn(val scope: KClass<*>)
  24. Scoping : @SingleIn Session • 스코프는 반드시 사용되는 그래프와 일치해야

    함 ◦ 범위가 지정되지 않은 그래프가 범위가 지정된 바인딩에 접근하는 것은 ERROR ◦ 범위가 일치하지 않는 바인딩에 접근하는 것도 ERROR @DependencyGraph interface AppGraph { // This is an error! val exampleClass: ExampleClass } @SingleIn(AppScope::class) @Inject class ExampleClass // ================================ @SingleIn(AppScope::class) @DependencyGraph interface AppGraph { // This is an error! val exampleClass: ExampleClass } @SingleIn(UserScope::class) @Inject class ExampleClass
  25. Scoping : @SingleIn Session @DependencyGraph(AppScope::class) @SingleIn(AppScope::class) // <-- Redundant! (Aggregation)

    interface AppGraph { // ... } • @DependencyGraph.scope를 지정하면 지정된 스코프 내에서의 하나의 인스턴스만 제공하는 것으로 간주됨 • @SingleIn과 함께 지정하는 것은 중복 @SingleIn(AppScope::class) • Dagger에서 사용하는 @Singleton과 동일한 의미를 가짐 ◦ 동일하게 Double-Check와 synchronized를 통해 인스턴스 일관성 확보 @Singleton
  26. @DependencyGraph(AppScope::class) interface AppGraph { val mashUp: MashUp @DependencyGraph.Factory fun interface

    Factory { fun create(@Provides team: Team): AppGraph } } @Inject @SingleIn(AppScope::class) class MashUp(private val team: Team) { fun printTeam() { Log.d("MashUp", "팀이름 : ${team.name}") } } 🤔 그렇다면 KMP 팀은 어떻게 구성될 수 있나? Session
  27. Scoping : Hilt to Metro Session @ActivityRetainedScoped @DefineComponent(parent = SingletonComponent.class)

    public interface ActivityRetainedComponent {} Hilt (KSP) @GraphExtension(ActivityRetainedScope::class) interface ActivityRetainedGraph { @ContributesTo(AppScope::class) @GraphExtension.Factory fun interface Factory { fun createActivityRetainedGraph(): ActivityRetainedGraph } } Metro @ActivityScoped @DefineComponent(parent = ActivityRetainedComponent.class) public interface ActivityComponent {} @GraphExtension(ActivityScope::class) interface ActivityGraph { @ContributesTo(ActivityRetainedScope::class) fun interface Factory { fun createActivityGraph(): ActivityGraph } }
  28. Binding : @Binds Session • 추상화에 대한 의존성 바인딩을 제공할

    때 사용 • 추상 함수 또는 확장 함수 형태로 바인딩 제공 가능 interface RepositoryGraph { @Binds fun bindRepository(impl: RepositoryImpl): Repository @Binds val RepositoryImpl.bind: Repository } @Inject class UseCase(val repository: Repository)
  29. Binding : @Binds Session • 추상화에 대한 의존성 바인딩을 제공할

    때 사용 • 추상 함수 또는 확장 함수 형태로 바인딩 제공 가능 • 멀티 모듈 환경에서 자동으로 바인딩을 탐색/제공하기 위해서는 @ContributesBinding 사용 // :data @ContributesBinding(DataScope::class) interface RepositoryGraph { @Binds fun bindRepository(impl: RepositoryImpl): Repository @Binds val RepositoryImpl.bind: Repository } // :domain @Inject class UseCase(val repository: Repository)
  30. Aggregation : @ContributesBinding Session • Aggregation은 Graph간의 합성을 위한 기능

    • @Binds를 통해 Cache 타입을 자동으로 검색해서 바인딩을 제공할 수 있음 @DependencyGraph(scope = AppScope::class) interface AppGraph { private val cache: Cache @Binds val CacheImpl.bind: Cache } @Inject class CacheImpl(..): Cache
  31. Aggregation : @ContributesBinding Session • @ContributesBinding은 주입된 의존성을 지정된 바인딩

    형태로 스코프에 제공하는 데 사용 @DependencyGraph(scope = AppScope::class) interface AppGraph { val cache: Cache } @ContributesBinding(AppScope::class) @Inject class CacheImpl(..): Cache
  32. Aggregation : @ContributesBinding Session • @ContributesBinding은 주입된 의존성을 지정된 바인딩

    형태로 스코프에 제공하는 데 사용 • 상위 타입이 여러 개인 경우 @Contributes.binding을 통해 명시적으로 정의할 수 있음 @DependencyGraph(scope = AppScope::class) interface AppGraph { val cache: Cache } @ContributesBinding( scope = AppScope::class, binding = binding<Cache>(), ) @Inject class CacheImpl(..): Cache, Closable
  33. Aggregation : @ContributesTo Session • @ContributesTo는 스코프에 인터페이스를 제공하여 그래프

    처리 시점에 최종 병합하여 바인딩을 제공하는데 사용 • 멀티 모듈 프로젝트에서 각 모듈별로 독립된 그래프를 생성할 수 있음 @ContributesTo(AppScope::class) interface NetworkProviders { @Provides fun provideHttpClient(): HttpClient }
  34. Aggregation : @ContributesTo Session • @ContributesTo는 스코프에 인터페이스를 제공하여 그래프

    처리 시점에 최종 병합하여 바인딩을 제공하는데 사용 • 멀티 모듈 프로젝트에서 각 모듈별로 독립된 그래프를 생성할 수 있음 • 여러 스코프에 동일하게 제공하는 것도 가능 @ContributesTo(AppScope::class) interface NetworkProviders { @Provides fun provideHttpClient(): HttpClient } @ContributesTo(AppScope::class) @ContributesTo(LoggedInScope::class) interface NetworkProviders { @Provides fun provideHttpClient(): HttpClient }
  35. Application Session @HiltAndroidApp class WeatherApplication : Application() Hilt (KSP) class

    WeatherApplication : Application() { val appGraph by lazy { createGraphFactory<AppGraph.Factory>().create(this) } } @DependencyGraph(AppScope::class) interface AndroidAppGraph : AppGraph { @Multibinds(allowEmpty = true) val activityProviders: Map<KClass<out Activity>, Provider<Activity>> @DependencyGraph.Factory fun interface Factory { fun create( @ApplicationContext @Provides applicationContext: Context ): AndroidAppGraph } } Metro • AppComponentFactory는 애플리케이션에서 액티비티 생성을 정의하는 Default Factory • Metro에서는 Activity에 대한 의존성 제공을 위해 MetroAppComponentFactory로 재정의 필요 • Manifest에서 기본 AppComponentFactory를 대체해줘야 함 <application android:name=".WeatherApplication" android:appComponentFactory="..MetroAppComponentFactory" ... tools:replace="android:appComponentFactory">
  36. Activity Session @AndroidEntryPoint class MainActivity : ComponentActivity() Hilt (KSP) @ContributesIntoMap(

    AppScope::class, binding = binding<Activity>() ) @ActivityKey(MainActivity::class) @Inject class MainActivity(..) : ComponentActivity() { Metro
  37. ViewModel - Hilt Session @Composable internal fun MainRoute( val modifier:

    modifier = Modifier, val viewModel: WeatherViewModel = hiltViewModel(), .. ) { .. } @HiltViewModel class WeatherViewModel @Inject constructor( private val repository: Repository, ) : ViewModel() { .. }
  38. ViewModel - Metro Session @DependencyGraph(AppScope::class) interface AppGraph : ViewModelGraph {

    .. @Provides @SingleIn(AppScope::class) fun provideMetroViewModelFactory( viewModelProviders: Map<KClass<out ViewModel>, Provider<ViewModel>>, manualAssistedFactoryProviders: Map<KClass<out ManualViewModelAssistedFactory>, Provider<ManualViewModelAssistedFactory>>, ): MetroViewModelFactory = object : MetroViewModelFactory() { override val viewModelProviders get() = viewModelProviders override val manualAssistedFactoryProviders get() = manualAssistedFactoryProviders } }
  39. ViewModel - Metro Session @Composable internal fun MainRoute( val modifier:

    modifier = Modifier, ) { val viewModel = assistedMetroViewModel<WeatherViewModel, WeatherViewModel.Factory>( create("...") ) ... } @AssistedInject class WeatherViewModel( @Assisted val argument: String, private val repository: Repository, ) : ViewModel() { ... @AssistedFactory @ManualViewModelAssistedFactoryKey(Factory::class) @ContributesIntoMap(AppScope::class) fun interface Factory : ManualViewModelAssistedFactory { fun create(argument: String): WeatherViewModel } }
  40. Network - DataGraph Session @Module @InstallIn(SingletonComponent::class) internal interface DataModule {

    @Binds @Singleton fun bindRepository(impl: DefaultRepository): Repository ... } Hilt (KSP) abstract class DataScope private constructor() @DependencyGraph(DataScope::class) interface DataGraph { @Binds val DefaultRepository.bind: Repository ... } Metro
  41. Network - NetworkGraph Session @Module @InstallIn(SingletonComponent::class) internal class NetworkModule {

    @Provides @Singleton fun provideOkHttpClient(...): OkHttpClient { return OkHttpClient.Builder() ... .build() } @Provides @Singleton fun provideRetrofit(httpClient: OkHttpClient): Retrofit { return Retrofit.Builder() .baseUrl(BASE_URL) .client(okHttpClient) ... .build() } @Provides @Singleton fun provideWeatherService(retrofit: Retrofit): WeatherService = retrofit.create() } Hilt (KSP) @ContributesTo(DataScope::class) interface NetworkGraph { @Provides fun provideOkHttpClient(...): OkHttpClient { return OkHttpClient.Builder() ... .build() } @Provides fun provideRetrofit(httpClient: OkHttpClient): Retrofit { return Retrofit.Builder() .baseUrl(BASE_URL) .client(okHttpClient) ... .build() } @Provides fun provideWeatherService(retrofit: Retrofit): WeatherService = retrofit.create() } Metro
  42. 시간 상 다루지 못한 내용 Session 1. Metro Multi-Binding -

    @IntoSet, @IntoMap 2. Metro Intrinsics - Provider, Lazy 3. Metro Build Pipeline - IR, FIR, ... 4. Metro Multiplatform