이 글이 맞는 상황
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가 너무 많아지면 with와 context 중첩이 깊어집니다. 세 개를 넘어가기 시작하면 오히려 가독성이 떨어지므로 환경을 묶은 인터페이스 도입을 고려하세요.
지금 바로 시작하는 최소 단계
전체 구조를 바꿀 필요 없습니다. build.gradle.kts에 -Xcontext-parameters 플래그를 추가한 뒤, 현재 프로젝트에서 CoroutineScope를 파라미터로 받는 함수 하나만 골라 context parameter로 바꿔보세요.
파라미터 하나를 context로 옮기는 것만으로 시그니처가 얼마나 읽기 편해지는지 체감할 수 있습니다. 기존 호출부는 with(scope) { ... } 블록으로 감싸면 컴파일이 바로 통과합니다.
※ 본 글은 정보 제공 목적이며 특정 제품·서비스의 추천이 아닙니다.
'안드로이드' 카테고리의 다른 글
| SavedStateHandle과 rememberSaveable 없이 화면 상태를 온전히 복원하는 법 (0) | 2026.06.02 |
|---|---|
| 온디바이스 AI와 클라우드를 자동으로 전환하는 하이브리드 추론 구현하기 (0) | 2026.05.31 |
| AI 에이전트가 내 앱 기능을 직접 호출하게 하는 법 (0) | 2026.05.26 |
| iOS와 웹까지, 이제 Compose 하나로 충분합니다 (0) | 2026.05.16 |
| 안드로이드에서 진짜 대화가 되는 음성 AI 만들기: 상태 머신으로 파이프라인 엮기 (0) | 2026.05.15 |