Metro 찍먹해보기

easyhooon·2025년 9월 22일

Metro

목록 보기
1/2
post-thumbnail

React Native에서 사용하는 Javscript Bundler 아님 주의

서두

DroidKaigi 2025 컨퍼런스 앱을 분석해보면서 의존성 주입을 위해 Metro 라는 프레임워크를 사용하는 것을 확인할 수 있었다.

주로 사용해왔던 Hilt와 매우 유사한 형태인데, 무려 멀티플랫폼을 지원한다ㄷㄷ
게다가 Circuit을 개발하신 Zac Sweers님이 Maintainer ㄷㄷ 팬입니다.

Zac Sweers님께서 새로 개발하신 DI 프레임워크인 Metro에 대해 알아보고, 기존에 사용하던 프레임워크들과의 차이를 알아보고자 한다.

DI(Dependency Injection)가 무엇인지에 대한 설명은 생략하도록 하겠다.

가볍게 찍먹해보는 글이기 때문에, 더 자세한 내용들은 Metro 소개 글공식 문서, 그리고 DroidKaigi 발표 영상을 참고해보면 좋을듯하다.

본론

다양한 DI 프레임워크들이 이미 존재하는데, 왜 만들어진건가?

기존 DI 프레임워크가 가진 문제점들을 해결하기 위함이다. 또한 Metro는 Dagger/Hilt, Anvil, kotlin-inject의 장점들을 하나의 통합된 솔루션으로 묶어내기 위함이다.

Metro는 바퀴를 재발명하려 하지 않고, 기존의 바퀴들이 서로 더 잘 굴러가도록 만든 것이며, 거인의 어깨 위에 서 있는 것이라고 소개글에 언급하셨다.

기존 프레임워크들의 문제점

Hilt: Android 전용으로 설계되어 KMP 지원 X, 지원 예정이라는데 깜깜무소식

kotlin-inject: KMP를 지원하지만 Aggregation과 같은 고급 기능들 부족, Android 특화 기능 지원 및 레퍼런스 부족 -> Anvil의 Aggregation 기능이 추가된 kotlin-inject-anvil이 이를 보완

Koin: 런타임 의존성 주입으로 인한 성능 오버헤드 -> 최근 어노테이션 방식을 지원하여 컴파일 타임 의존성 검증을 수행할 수 있게 됨

Dagger: 높은 러닝 커브와 수많은 보일러 플레이트 코드 -> Hilt

Kotlin 커뮤니티에서는 오랫동안 이런 다양한 프레임워크들의 장점을 모두 합친 프레임워크를 원해왔다. 나만 그런게 아니었군

각각의 단점은 해결하고, 장점은 하나로 묶어내며, 컴파일러 플러그인이 제공하는 새로운 기능들을 받아들이는 통합된 솔루션은 아직 없었기에 이를 개발하셨다고 한다.

Metro가 가진 장점들

1. 빌드 성능 향상

기존 프로젝트에 새로운 기술을 도입할 때 팀과 조직을 설득할 수 있는 가장 강력한 근거는 명확한 성능 향상이다.

Metro는 컴파일러 플러그인으로 구현되어 CatchUp 앱에서 이를 적용하였을 때, ABI 변경 시 빌드 시간이 47% 빨라지고, Non-ABI 변경 시 56% 빨라졌다고 한다.

여기서 ABI(Application Binary Interface) 변경은 메서드 시그니처나 클래스 구조 등이 바뀌어서 바이너리 호환성이 깨져 다른 모듈에도 영향을 주는 변경이고, Non-ABI 변경은 메서드 내부 구현만 바뀌어서 바이너리 호환성을 유지하며 다른 모듈에는 영향을 주지 않는 변경을 의미한다.

바이너리 호환성이 유지되면 이미 컴파일된 코드, 모듈 간의 인터페이스가 변경되지 않아 재컴파일 없이도 동작할 수 있다.

이러한 성능 향상이 가능한 이유는 새로운 소스 생성을 피하고, KSP/KAPT과 같은 Annotation Processor의 실행을 피하며, 컴파일러 플러그인으로서 Kotlin 컴파일러의 내부 API를 활용하기 때문이다.

기존 DI 프레임워크들이 여러 단계의 코드 생성과 재컴파일을 거쳐야 했던 것과 달리, Metro는 컴파일 과정에서 한 번에 처리하여 빌드 시간을 단축시킨다.

기존 방식 vs Metro

KAPT 방식:
소스 코드 → Kotlin 컴파일 (Java 스텁 생성) → javac (어노테이션 처리) → Kotlin 컴파일 → 최종 코드

KSP 방식:
소스 코드 → KSP 실행 (코드 생성) → Kotlin 컴파일 → 최종 코드

Metro:
소스 코드 → 컴파일러 플러그인이 FIR/IR에서 직접 변환 → 최종 코드

KAPT/KSP와 같은 Anotation Processor(어노테이션 처리기)는 소스코드에서 어노테이션을 찾아낸 뒤, 이 어노테이션을 분석해서 의존성 그래프를 구성하거나, 필요한 팩토리 클래스, 컴포넌트 구현체 코드를 자동으로 생성하는 역할을 한다.
ex. @Inject class UserService -> UserService_Factory.java

2. 멀티플랫폼 지원

Kotlin Multiplatform을 완전 지원하면서도 JVM, JS, Native(iOS, Mac 등) 타겟 모두 지원 하는 동시에, Android 특화 기능(ex. ApplicationContext 주입)까지 제공한다.

Kotlin-Inject와 같은 KMP를 위해 설계된 DI 프레임워크의 경우, Android 프로젝트에서는 사용하기가 어려운 부분(Context를 어떻게 주입하는지 레퍼런스 없음...못 찾은 걸지도)이 있었는데, Metro는 Android 프로젝트에서 사용하는 경우에 대한 sample도 github에 존재하여 무리없이 기존 Android 프로젝트에 도입 가능할 것으로 보인다.

글을 작성하는 현 시점에선 아직 @Contributes 어노테이션을 Native 타겟을 대상으론 사용할 수 없는 이슈가 있는 듯 하다.

https://youtrack.jetbrains.com/issue/KT-75865

3. Top-level Function Injection

기존 DI에서는 불가능했던 함수 레벨 의존성 주입을 지원한다. 미쳤다

@Inject
@Composable
fun UserScreen(repository: UserRepository): Unit {
    // Composable 함수에 직접 의존성 주입!
}
// 사용시
@Composable
fun App() {
    // Metro가 자동으로 의존성 주입
    UserScreen()
}

Top-level Function은 클래스 내부가 아닌, 파일의 최상위 레벨에 정의된 함수를 의미한다.

아쉽게도 DroidKaigi에서는 Composable 함수인 Presenter에 @Inject 어노테이션을 사용하여, Repository와 같은 필요한 의존성들을 주입하는 방식을 사용하진 않았다.

만약 이 Top-level Function Injection 방식을 적용한다면 좀 더 직관적이고 이해하기 쉬운 코드가 될 듯하다.

현재의 의존성 주입 방식은 다소 현학적이라 이해하는데 시간이 많이 걸렸다...
활용 예시는 다음 문단에 서술해 두었다.

4. 통합된 API

Dagger 스타일의 컴파일 타임 코드 생성과 kotlin-inject 스타일의 API(ex. @Component), Anvil 스타일의 Aggregation을 모두 하나의 프레임워크에서 제공한다.

@ContributesBinding(AppScope::class)
@Inject
class CacheImpl(...) : Cache

Aggregation은 여러 모듈에 분산된 의존성 정의들을 컴파일 타임에 자동으로 수집하여 하나의 컴포넌트로 결합해주는 기능이다.

적용 전(순수 kotlin-inject)

// 각 모듈에서 개별적으로 정의
@Component 
abstract class AppComponent(
    @Component val networkComponent: NetworkComponent,
    @Component val databaseComponent: DatabaseComponent,
    @Component val analyticsComponent: AnalyticsComponent,
    @Component val authComponent: AuthComponent
    // 새로운 모듈 추가 시마다 여기에 수동으로 추가해야 함
) {
    abstract val userRepository: UserRepository
}

// 각 모듈의 Component들을 일일이 정의하고 연결
@Component abstract class NetworkComponent { ... }
@Component abstract class DatabaseComponent { ... }
@Component abstract class AnalyticsComponent { ... }
@Component abstract class AuthComponent { ... }

적용 후(kotlin-inject-anvil)

// 각 모듈에서 자동으로 기여
@ContributesTo(AppScope::class)
interface NetworkModule {
    @Provides fun provideHttpClient(): HttpClient = ...
}

@ContributesTo(AppScope::class) 
interface DatabaseModule {
    @Provides fun provideDatabase(): Database = ...
}

@ContributesTo(AppScope::class)
interface AnalyticsModule {
    @Provides fun provideAnalytics(): Analytics = ...
}

// 메인 Component는 자동으로 모든 기여 모듈들을 수집
@Component
@MergeComponent(AppScope::class)  // 자동으로 모든 @ContributesTo(AppScope::class) 수집
abstract class AppComponent {
    abstract val userRepository: UserRepository
    // 개별 Component 나열 불필요!
}

5. Private Member Injection

클래스 내부의 private 멤버에 직접 의존성을 주입할 수 있는 기능으로, 생성자에 파라미터들이 쌓여 복잡도가 상승하는 문제를 해결하면서도 필요한 의존성들을 안전하게 주입받을 수 있다

class UserService {
    @Inject private lateinit var repository: UserRepository
    @Inject private lateinit var cache: CacheManager
    @Inject private lateinit var logger: Logger
    
    // 생성자에 파라미터를 늘어놓을 필요 없이
    // 필요한 의존성들은 private으로 주입됨
    fun getUser(id: String): User {
        logger.info("Fetching user: $id")
        return cache.get(id) ?: repository.findById(id)
    }
}

Validation & Error Reporting

Metro는 컴파일 타임에 의존성 그래프의 무결성 검증을 수행한다.

Tarjan 알고리즘으로 순환 참조 의존성을 탐지하고, 발견 시 정확한 순환 경로를 역추적한다. Provider나 Lazy를 통한 유효한 순환인지 자동으로 판별하여 지연 키를 별도 관리한다.

또한, 검증된 DAG(Directed Acylic Graph)Kahn의 위상 정렬로 처리하여 의존성 순서에 따른 최적의 바인딩 초기화 순서를 결정한다.

마지막으로 누락된 바인딩, 순환 의존성, 스코프 불일치 등에 대해 구체적인 해결 방안을 포함한 상세한 컴파일 타임 진단 메시지를 제공한다.

예시

ExampleGraph.kt:6:1 [Metro/DependencyCycle] Found a dependency cycle:
    kotlin.Int is injected at
        [test.ExampleGraph] test.ExampleGraph.provideString(..., int)
    kotlin.String is injected at
        [test.ExampleGraph] test.ExampleGraph.provideDouble(..., string)
    kotlin.Double is injected at
        [test.ExampleGraph] test.ExampleGraph.provideInt(..., double)
    kotlin.Int is injected at
        [test.ExampleGraph] test.ExampleGraph.provideString(..., int)
ExampleGraph.kt:6:1 [Metro/GraphDependencyCycle] Dependency graph dependency cycle detected! The below graph depends on itself.
    test.CharSequenceGraph is requested at
        [test.CharSequenceGraph] test.CharSequenceGraph.Factory.create()

라이브러리 또는 프레임워크(이젠 둘의 경계가 모호해진듯)를 분석하다보면, 이처럼 알고리즘 및 자료구조들을 활용한 예시들을 마주치게 된다.
또한, AI가 발전 할수록 문제 해결을 위해 적절한 자료구조를 선택하고 설계하는 능력(명령하는 능력)이 중요해질 것으로 예상된다. 실제로 이러한 관점에서 자료구조의 중요성을 다룬 흥미로운 DroidKaigi 발표 영상이 있으니, 참고해보면 좋을 듯 하다. Dagger의 이름이 왜 DAGger 일까?

활용 예시

DroidKaigi 레포에서 사용된 Rin 기반의 Presenter의 의존성 주입은 Metro와 Kotlin의 Context parameter 문법을 통해 구현되었다.

TimetableScreenContext.kt#L12

import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesGraphExtension
// ...

// ScreenContext: 화면별 의존성 정의(TimtableScope, TimeTableDetailScope, SearchScope...etc)
@ContributesGraphExtension(TimetableScope::class)
interface TimetableScreenContext : ScreenContext {
    val timetableQueryKey: TimetableQueryKey
    val favoriteTimetableIdsSubscriptionKey: FavoriteTimetableIdsSubscriptionKey
    val favoriteTimetableItemIdMutationKey: FavoriteTimetableItemIdMutationKey

    @ContributesGraphExtension.Factory(AppScope::class)
    fun interface Factory {
        fun createTimetableScreenContext(): TimetableScreenContext
    }
}

TimetableScreenPresenter.kt#L29

// Rin 기반 Presenter 패턴
@Composable
context(screenContext: TimetableScreenContext)
fun timetableScreenPresenter(
    eventFlow: EventFlow<TimetableScreenEvent>,
    timetable: Timetable,
): TimetableScreenUiState = providePresenterDefaults {
    // screenContext를 통해 필요한 의존성에 접근
    val favoriteTimetableItemIdMutation = rememberMutation(screenContext.favoriteTimetableItemIdMutationKey)

    var uiType by rememberRetained { mutableStateOf(TimetableUiType.List) }
    var selectedDay by rememberRetained { mutableStateOf(DroidKaigi2025Day.ConferenceDay1) }

    // 이벤트 처리 로직
    EventEffect(eventFlow) { event ->
        when (event) {
            is TimetableScreenEvent.Bookmark -> {
                favoriteTimetableItemIdMutation.mutate(TimetableItemId(event.sessionId))
            }

            is TimetableScreenEvent.SelectTab -> selectedDay = event.day

            is TimetableScreenEvent.UiTypeChange -> {
                uiType = if (uiType == TimetableUiType.Grid) {
                    TimetableUiType.List
                } else {
                    TimetableUiType.Grid
                }
            }
        }
    }

    // UI 상태 반환
    TimetableScreenUiState(
        timetable = timetableSheet(
            sessionTimetable = timetable,
            uiType = uiType,
            selectedDay = selectedDay,
        ),
        uiType = uiType,
    )
}

@Composable
fun <UI_STATE> providePresenterDefaults(
    uiStateProducer: @Composable PresenterContext.() -> UI_STATE,
): UI_STATE {
    usePresenterContext {
        return uiStateProducer()
    }
}

/**
 * Context for presenter composable functions.
 */
interface PresenterContext

class DefaultPresenterContext : PresenterContext

@OptIn(ExperimentalContracts::class)
inline fun usePresenterContext(
    block: PresenterContext.() -> Unit,
) {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block(DefaultPresenterContext())
}

TimetableScreenRoot.kt#L19

// ScreenRoot: 화면의 진입점
@Composable
context(screenContext: TimetableScreenContext)
fun TimetableScreenRoot(
    onSearchClick: () -> Unit,
    onTimetableItemClick: (TimetableItemId) -> Unit,
) {
    // Soil을 통한 데이터 바운더리
    SoilDataBoundary(
        state1 = rememberQuery(screenContext.timetableQueryKey),
        state2 = rememberSubscription(screenContext.favoriteTimetableIdsSubscriptionKey),
        fallback = SoilFallbackDefaults.appBar(
            title = stringResource(SessionsRes.string.timetable),
            colors = TopAppBarDefaults.topAppBarColors().copy(
                containerColor = Color.Transparent,
                scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer,
            ),
            contentBackground = {
                TimetableBackground()
            },
        ),
    ) { timetable, favoriteTimetableItemIds ->
        val eventFlow = rememberEventFlow<TimetableScreenEvent>()

        // Presenter 호출
        val uiState = timetableScreenPresenter(
            eventFlow = eventFlow,
            timetable = timetable.copy(bookmarks = favoriteTimetableItemIds),
        )

        // UI 렌더링
        TimetableScreen(
            uiState = uiState,
            onSearchClick = onSearchClick,
            onTimetableItemClick = onTimetableItemClick,
            onBookmarkClick = { sessionId ->
                eventFlow.tryEmit(TimetableScreenEvent.Bookmark(sessionId))
            },
            onTimetableUiChangeClick = { eventFlow.tryEmit(TimetableScreenEvent.UiTypeChange) },
            onDaySelected = { day ->
                eventFlow.tryEmit(TimetableScreenEvent.SelectTab(day))
            },
        )
    }
}

SessionNavGraph.kt

// AppGraph context를 통한 네비게이션 레벨 DI
context(appGraph: AppGraph)
fun NavGraphBuilder.timetableTabNavGraph(
    onSearchClick: () -> Unit,
    onTimetableItemClick: (TimetableItemId) -> Unit,
    onBackClick: () -> Unit,
    onLinkClick: (String) -> Unit,
    onShareClick: (TimetableItem) -> Unit,
    onAddCalendarClick: (TimetableItem) -> Unit,
) {
    navigation<TimetableTabRoute>(
        startDestination = TimetableRoute,
    ) {
        timetableNavGraph(
            onSearchClick = onSearchClick,
            onTimetableItemClick = onTimetableItemClick,
        )
        timetableItemDetailNavGraph(
            onBackClick = onBackClick,
            onLinkClick = onLinkClick,
            onShareClick = onShareClick,
            onAddCalendarClick = onAddCalendarClick,
        )
        searchNavGraph(
            onBackClick = onBackClick,
            onTimetableItemClick = onTimetableItemClick,
        )
    }
}

// Factory를 통한 ScreenContext 생성
context(appGraph: AppGraph)
private fun NavGraphBuilder.timetableNavGraph(
    onSearchClick: () -> Unit,
    onTimetableItemClick: (TimetableItemId) -> Unit,
) {
    composable<TimetableRoute> {
        with(rememberTimetableScreenContextRetained()) {
            TimetableScreenRoot(
                onSearchClick = onSearchClick,
                onTimetableItemClick = onTimetableItemClick,
            )
        }
    }
}

context(factory: TimetableScreenContext.Factory)
@Composable
public fun rememberTimetableScreenContextRetained(): TimetableScreenContext = rememberRetained {
    factory.createTimetableScreenContext() 
}

App.kt#L8

import android.app.Application
import android.content.Context
import dev.zacsweers.metro.createGraphFactory

class App : Application() {
    // Metro의 createGraphFactory로 AppGraph 인스턴스 생성
    val appGraph: AppGraph by lazy {
        createGraphFactory<AndroidAppGraph.Factory>()
            .createAndroidAppGraph(
                applicationContext = this, 
                licensesJsonReader = AndroidLicensesJsonReader(this),
                useProductionApi = BuildConfig.USE_PRODUCTION_API,
            )
    }
}

// Context 확장 함수로 어디서든 AppGraph에 접근 가능
val Context.appGraph: AppGraph get() = (applicationContext as App).appGraph

@DependencyGraph(
    // 앱 전체 스코프
    scope = AppScope::class,
    // 데이터 레이어 관련 스코프 추가(ktor, json, ApiBaseUrl...etc
    additionalScopes = [DataScope::class],
    // 다른 모듈에서 이 그래프를 확장 가능하도록 설정
    isExtendable = true,
)
interface AndroidAppGraph : AppGraph {
    @DependencyGraph.Factory
    fun interface Factory {
        fun createAndroidAppGraph(
            // Android Application Context 주입
            @Provides applicationContext: Context,
            // 오픈소스 라이선스 정보 리더 주입
            @Provides licensesJsonReader: LicensesJsonReader,
            // production/staging API 선택
            @Provides @UseProductionApi useProductionApi: Boolean,
        ): AndroidAppGraph
    }
}

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        // ...

        // Context receiver 패턴으로 appGraph를 Compose 트리에 전달
        with(appGraph) {
            setContent {
                // 이제 KaigiApp과 하위 Composable들이 appGraph에 접근 가능
                KaigiApp() 
            }
        }
    }
}

DataGraph.kt

import dev.zacsweers.metro.ContributesTo
import dev.zacsweers.metro.Provides
import dev.zacsweers.metro.Qualifier
// ...

// 데이터 레이어 전용 스코프 - 네트워크, DB 등 데이터 관련 의존성들을 그룹화
public abstract class DataScope private constructor()

// production/staging 환경 구분용
@Qualifier
public annotation class UseProductionApi 

@Qualifier  
public annotation class ApiBaseUrl 

// 이 인터페이스를 DataScope에 기여
@ContributesTo(DataScope::class) 
public interface DataGraph {
    @UseProductionApi
    public val useProductionApi: Boolean

    @Provides
    @ApiBaseUrl
    public fun provideApiBaseUrl(
        @UseProductionApi useProductionApi: Boolean,
    ): String = if (useProductionApi) {
        "https://ssot-api.droidkaigi.jp/"
    } else {
        "https://ssot-api-staging.an.r.appspot.com/"
    }

    @Provides
    public fun provideKtorfit(
        httpClient: HttpClient,
         // Qualifier로 구분된 API URL 주입
        @ApiBaseUrl apiBaseUrl: String,
    ): Ktorfit {
        return Ktorfit.Builder()
            .httpClient(httpClient)
            .baseUrl(apiBaseUrl)
            .build()
    }

    @Provides
    public fun provideJson(): Json {
        // 공통 JSON 직렬화 설정
        return defaultJson() 
    }
}

Soil이 무엇인지는 해당 글에서 간략하게 소개해두었다.

작년 컨퍼런스 앱에서는 CompositionLocal을 사용하여, Repository를 주입하는 등 Compose Runtime을 극한으로 사용하는 형태를 보여주었는데, 올해는 이를 지양하고 의존성 전파 방식을 보다 명시적으로 개선한 것을 확인할 수 있다.

결론 및 느낀점

Metro가 왜 만들어졌고, Metro의 장점들이 무엇인지, 그리고 이를 실제로 어떻게 사용할 수 있는지 활용 예시를 통해 알아보았다.

KMP 프로젝트에서 기존의 주로 사용했던 Koin 외에, 컴파일 타임 검증과 성능 개선을 제공하는 선택지가 생겨서 매우 반갑다.

Android 개발자로서, Hilt가 KMP를 지원하지 않는 것이 KMP의 개발 생태계 확장에 가장 큰 병목 중에 하나라고 생각했다. 하지만 Metro가 기존에 Hilt를 사용했던 방식과 동일한 JSR-330 스타일의 어노테이션을 사용하기 때문에, 이제는 큰 공수 없이 기존 Android 프로젝트를 KMP로 확장할 수 있을 것으로 예상된다. 세상이 점점 편해짐을 느낀다.

Hilt를 적용해둔 토이 프로젝트가 있는데, Metro로 migration하여 Hilt를 사용했을 때와 어떤 차이점이 있고, 적용했을 때 장점들을 직접 체험해봐야겠다.

또한 Soil을 적용하기 위해, DroidKaigi 레포를 확인해봤을 때, Composable 함수인 Presenter에 Soil과 관련된 의존성(ex. QueryKey, SubscriptionKey...etc)을 어떻게 주입할 수 있는지가 이해가 되지 않아 블랙 박스 상태였는데, 이번 분석을 통해 어느 정도 그 구조를 파악할 수 있었다. Soil도 빨리 적용 완료하여 내 것으로 만들어 봐야겠다...!

번외

IR? FIR?

Metro의 성능이 뛰어난 이유는 Kotlin 컴파일러의 FIR/IR을 직접 활용하기 때문인데, 사실 이부분에 대해서는 아직 잘 알지 못하므로 간단하게만 정리해보도록 하겠다.

FIR (Frontend Intermediate Representation):

K2 컴파일러에서 사용하는 프론트엔드 중간 표현으로, 컴파일러 백엔드와 독립적인 의미론적 모델이다. Metro는 FIR 단계에서 의존성 그래프 분석 및 검증, 타입 정보 수집을 수행한다.

IR (Intermediate Representation):

Kotlin 컴파일러가 사용하는 내부 표현으로, 다양한 컴파일러 백엔드에 대한 공통 표현으로 설계되었다. Metro는 IR 단계에서 실제 의존성 주입 코드 생성과 최적화된 바인딩 코드를 생성한다.

FIR/IR을 사용하면 ASTPSI를 사용하는 것에 비해 정적 분석의 복잡성을 크게 줄일 수 있으며, 특히 타입 해결이 필요한 분석에서 이러한 이점이 두드러진다.

여기서 AST(Abstract Syntax Tree)는 소스 코드를 트리 구조로 나타낸 추상 구문 트리이고, PSI(Program Structure Interface)는 IntelliJ 기반 IDE에서 사용하는 코드 구조 분석 인터페이스다.

Kotlin을 사용하지만 Kotlin 컴파일러에 대해선 너무나도 무지하다... 추가적인 학습을 진행 해봐야겠다.

reference)
https://www.zacsweers.dev/introducing-metro/
https://youtu.be/jVfmtVKa604?si=wTl-naiz35CDvkOX
https://zacsweers.github.io/metro/latest/?ref=zacsweers.dev
https://github.com/zacsweers/metro?ref=zacsweers.dev
https://docs.oofbird.me/spring/core/ioc/1_11.html#beans-named
https://bnorm.medium.com/exploring-kotlin-ir-bed8df167c23
https://akuleshov7.com/2021-06-24-kotlin-representation.html
https://insert-koin.io/docs/reference/koin-annotations/start/
https://insert-koin.io/docs/reference/koin-annotations/start/
https://hucet.tistory.com/46
https://github.com/evant/kotlin-inject
https://github.com/amzn/kotlin-inject-anvil
https://github.com/square/anvil
https://www.ibm.com/kr-ko/think/topics/directed-acyclic-graph
https://www.ibm.com/kr-ko/think/topics/directed-acyclic-graph
https://www.geeksforgeeks.org/dsa/topological-sorting-indegree-based-solution/
https://www.youtube.com/watch?v=6Y8ir1WE-UU
https://kotlinlang.org/docs/context-parameters.html
https://github.com/DroidKaigi/conference-app-2024

profile
실력은 고통의 총합이다. Android Developer

3개의 댓글

comment-user-thumbnail
2025년 9월 26일

이런 게 나왔군요! 특히 함수 레벨 주입과 private 멤버 주입이 굉장히 유용한 것 같습니다. 한번 써봐야겠네요

1개의 답글