본문 바로가기
안드로이드

함수 파라미터가 너무 많아졌을 때: Kotlin Context Parameters 도입기

by 안드뽀개기 2026. 5. 29.
반응형

이 글이 맞는 상황

Kotlin 2.2 이상, 멀티모듈 또는 KMP 구조를 사용 중인 프로젝트에 적합합니다. CoroutineScope, Logger, Analytics처럼 "항상 필요하지만 주인공은 아닌" 의존성이 함수 시그니처를 오염시키고 있다면 바로 오늘 적용해볼 수 있습니다. Compose 버전은 무관하며, 안드로이드 전용 API를 사용하지 않으므로 순수 Kotlin 모듈에도 동일하게 적용됩니다.


익숙한 고통

중규모 앱을 운영하다 보면 어느 순간 이런 함수가 생깁니다.

fun loadUserProfile(
    userId: String,
    scope: CoroutineScope,
    logger: Logger,
    analytics: Analytics,
    dispatcher: CoroutineDispatcher
) {
    scope.launch(dispatcher) {
        logger.d("Loading user: $userId")
        analytics.track("profile_view")
        // 실제 로직은 여기서부터...
    }
}

파라미터 다섯 개 중 실제 비즈니스 입력은 userId 하나입니다. 나머지 넷은 "이 함수가 실행되는 환경"입니다. 환경을 매번 직접 건네는 이 패턴은 콜스택이 깊어질수록 걷잡을 수 없이 퍼집니다.

Extension function으로 해결하려 해도, 수신 객체는 하나뿐입니다. CoroutineScope의 확장으로 만들면 Logger를 또 넘겨야 하고, Logger의 확장으로 만들면 CoroutineScope를 또 넘겨야 합니다.


Context Parameters 활성화

build.gradle.kts에 한 줄 추가로 충분합니다. Kotlin 2.2 이상이 전제 조건입니다.

// build.gradle.kts
kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xcontext-parameters")
    }
}

기존에 context(Logger) 형태의 context receivers를 사용하고 있었다면, 새 문법으로 마이그레이션이 필요합니다. IntelliJ 2025.2 이상의 Change Signature 리팩터링 기능이 이를 지원합니다.


Before / After: 실제 개선 코드

Before — 환경 파라미터가 시그니처를 잠식한 상태:

fun loadUserProfile(
    userId: String,
    scope: CoroutineScope,
    logger: Logger,
    analytics: Analytics
) {
    scope.launch {
        logger.d("Loading: $userId")
        analytics.track("profile_view")
        val profile = repository.fetch(userId)
        logger.d("Loaded: ${profile.name}")
    }
}

// 호출부 — 매번 환경을 직접 건네야 함
loadUserProfile(
    userId = currentUser.id,
    scope = viewModelScope,
    logger = logger,
    analytics = analytics
)

After — context parameter로 환경을 분리한 상태:

context(scope: CoroutineScope, logger: Logger, analytics: Analytics)
fun loadUserProfile(userId: String) {
    scope.launch {
        logger.d("Loading: $userId")
        analytics.track("profile_view")
        val profile = repository.fetch(userId)
        logger.d("Loaded: ${profile.name}")
    }
}

// 호출부 — 환경을 with 블록으로 한 번에 제공
with(viewModelScope) {
    context(logger, analytics) {
        loadUserProfile(userId = currentUser.id)
    }
}

함수 자체는 "무엇을 할 것인가"만 담게 됩니다. 환경 구성은 호출부에서 한 번에 처리합니다.


ViewModel에서 쓸 만한 패턴

Flow를 공유할 때 자주 반복되는 stateIn 보일러플레이트를 context parameter로 추출할 수 있습니다.

context(scope: CoroutineScope)
fun <T> Flow<T>.stateInWhileSubscribed(
    initialValue: T,
    stopTimeout: Duration = 5.seconds
): StateFlow<T> = stateIn(
    scope = scope,
    started = SharingStarted.WhileSubscribed(stopTimeout),
    initialValue = initialValue
)

ViewModel에서는 이렇게 씁니다:

class UserViewModel(
    private val repository: UserRepository
) : ViewModel() {

    val uiState: StateFlow<UserUiState> = with(viewModelScope) {
        repository.observeUser()
            .map { UserUiState.Success(it) }
            .stateInWhileSubscribed(UserUiState.Loading)
    }
}

같은 확장 함수를 KMP 공통 모듈에서도 with(coroutineScope) { ... } 형태로 그대로 재사용할 수 있습니다. ViewModel 의존성 없이 동일한 Flow 공유 로직이 동작합니다.


주의사항

Beta 단계입니다. 앱 코드에는 안정적으로 쓸 수 있지만, 라이브러리를 배포한다면 API 변경 가능성을 감안해야 합니다.

context parameter는 암묵적 수신 객체가 아닙니다. logger.d(...) 처럼 이름을 붙여서 명시적으로 호출해야 합니다. d(...) 단독 호출은 컴파일 에러가 납니다. 기존 context receivers에 익숙했다면 이 점이 가장 큰 차이입니다.

callable reference(::loadUserProfile)는 현재 context parameter가 있는 함수에서 지원되지 않습니다. 람다 래퍼로 우회해야 하며 2.3에서 해결될 예정입니다.

context parameter가 너무 많아지면 withcontext 중첩이 깊어집니다. 세 개를 넘어가기 시작하면 오히려 가독성이 떨어지므로 환경을 묶은 인터페이스 도입을 고려하세요.


지금 바로 시작하는 최소 단계

전체 구조를 바꿀 필요 없습니다. build.gradle.kts-Xcontext-parameters 플래그를 추가한 뒤, 현재 프로젝트에서 CoroutineScope를 파라미터로 받는 함수 하나만 골라 context parameter로 바꿔보세요.

파라미터 하나를 context로 옮기는 것만으로 시그니처가 얼마나 읽기 편해지는지 체감할 수 있습니다. 기존 호출부는 with(scope) { ... } 블록으로 감싸면 컴파일이 바로 통과합니다.


※ 본 글은 정보 제공 목적이며 특정 제품·서비스의 추천이 아닙니다.

반응형