Context
안드로이드 프로그래밍에 필수불가결한 존재인 Context 는 정말 자주 사용됩니다. 그 경우의 수가 어마무시하게 많은데, 그렇게 자주 사용하고 있으면서도, 'Context 가 정확히 뭐야?' 라는 얘기를 들으면 머릿속이 새하얘집니다.
전부를 알 수는 없겠지만, 조금이라도 알고 쓰자라는 마인드로 조금 알아봅니다.
1. What 'Context' truly is?
보통 '문맥' 이라는 의미를 사용하고자 할 때 Context 라는 단어를 씁니다. 조금 더 일상생활과 가까운 의미로는 '분위기', '맥락', (일이나 사건 등의) '정황' 이라는 의미를 위해 사용합니다. 안드로이드에서의 Context 는 현재 애플리케이션 실행 환경에 대한 정보를 제공하는 개념입니다. 기기 및 애플리케이션에 대한 정보를 담고 있는 Object 로, 리소스에 접근하고 시스템 서비스를 호출하며, 애플리케이션의 실행 환경과 상호작용할 수 있도록 도와줍니다.
Context 는 추상 클래스로 정의되어있으며, 기기나 앱의 정보 및 상태는 물론이고, 커널로의 접근 및 작업 수행도 가능케합니다. 해외에서는 God Object 라고 일컬을 만큼, 내포하는 기능과 필드가 많습니다.
🤔 기기 및 애프리케이션에 대한 정보를 담고있는 Object 라고 기술되어 있는데, abstract class 이면 Object 가 아니지 않나요?
public class ContextWrapper extends Context
ContextWrapper 라는 클래스가 있으며, 해당 클래스가 Context 를 상속하는 형태를 이루고 있습니다. 즉, 우리가 직접 사용하는 Context 는 ContextWrapper 객체입니다.
2. Context 종류와 Memory Leak
자주 사용되는 Context 는 ApplicationContext, ActivityContext 가 있습니다. 우리는 이 둘을 적재적소에 사용해야만 합니다. 두 Context 의 영향 범위를 다음 그림으로 표현할 수 있습니다.
ApplicationContext 의 경우, 앱 전체의 생명 주기를 따릅니다. 앱이 실행되면 Application 클래스에서 Singleton 으로 초기화되고, 종료될 때 메모리에서 해제됩니다. GUI 와 관련된 작업에 사용되어서는 안 되며, 앱 전역에 걸쳐 존재하는 컴포넌트에 대해 사용하여야 합니다.
ActivityContext 의 경우, 각 Activity 내에서 유효한 Context 이며, 해당 액티비티의 생명 주기를 따릅니다.
Context 가 두 가지로 나뉘어져 있기 때문에, 종종 어떤 것을 어느 곳에 사용해야 하는지 헷갈릴 수 있습니다. 사실 꽤 간단한 문제인데, 특정 Activity 내에서 사용되고, 해당 Activity 가 소멸될 때 함께 소멸되어야 하는 곳에는 ActivityContext 를, 그렇지 않은 곳에는 ApplicationContext 를 사용하면 됩니다.
보통 자료의 저장과 관련된 작업 (Local Database, DataStore, SharedPreferences 등) 에는 ApplicationContext 를 사용하며, 그 외에는 대부분 ActivityContext 가 사용됩니다.
🤔 제 앱에서는 DB 에 접근하는 Activity 가 단 하나밖에 없는데, ActivityContext 써도 되나요?
이 경우는 사실 아키텍처 디자인이 잘못되었을 확률이 높습니다. Activity 소스 코드에서 DB 를 생성하는 작업을 하고 있을테니까요.
DB 를 생성하는 작업은 타 작업에 비해 굉장히 큰 작업이며, DB 는 앱 전반에 걸쳐 메모리에 적재되어 있어야 접근하기도 편하기 때문에, DB 는 가급적이면 ApplicationContext 로 생성해주시기를 바랍니다.
생명 주기가 긴 컴포넌트에 그보다 짧은 생명 주기를 갖는 컴포넌트의 Context 를 사용하지만 않으면 (또는 그 반대), Context 로 인한 메모리 누수를 걱정할 필요는 없어 보입니다.
3. Compose 의 LocalContext.current
이 포스트를 작성하게 된 가장 큰 이유이며, 가장 궁금했던 부분입니다.
XML 를 기반으로 하여 UI 를 구성했을 때에는 크게 신경쓰지 않았습니다. UI 는 XML 에서 따로 구현하고, Context 가 필요한 부분에 한해서 Activity 의 this, 또는 Fragment 에서의 getContext() 등을 통해 ActivityContext 를 전달해왔으니까요.
그러나 Compose 환경에서 UI 를 구성할 때에는 일반적인 방법으로 ActivityContext 를 전달하지 아니하고 (물론 가능은 합니다만, 코드가 많이 지저분해집니다.) LocalContext.current 를 통해 현재 ActivityContext 를 전달합니다.
여기서 생긴 궁금증은 다음과 같습니다.
'Compose 의 LocalContext.current 는 현재 Activity 의 context 를 어떻게 가져오는 걸까?'
/**
* Provides a [Context] that can be used by Android applications.
*/
val LocalContext = staticCompositionLocalOf<Context> {
noLocalProvidedFor("LocalContext")
}
LocalContext 의 명세는 위와 같습니다. staticCompositionLocalOf<Context> 를 통해 Context 객체에 접근하는 것 같습니다. 해당 함수는 CompositionLocalProvider 를 사용하는데, 이를 파악하기 위해서는 CompositionLocal 과 StaticProvidableCompositionLocal 에 대해 먼저 간단히 알아보아야 할 것 같습니다.
CompositionLocal
Jetpack Compose 에서 사용할 수 있는 CompositionLocal 은 Compose 에서 값의 로컬 인스턴스를 제공하기 위한 메커니즘입니다. 이는 Compose 트리의 특정 범위 내에서 값을 공유하는 데 사용됩니다. 이를 사용하면 상위 계층에서 값을 정의하고, 이를 하위 계층에서 사용할 수 있습니다. CompositionLocalProvider 를 통해 값을 정의하면, Provider 블럭 내에서 정의한 값을 사용하는 구조입니다. 이는 쉽게 변경될 수 있는 부분에 한하여 사용됩니다.
StaticProvidableCompositionLocal
CompositionLocal 과 유사하나, 쉽게 변경되지 않는 값에 사용합니다.
어쨌든, 상위 계층에서 선언된 값을 하위 계층에서 사용하는 것이니, 우리는 우리가 LocalContext 를 사용하는 범위보다 상위 계층을 살펴보아야만 합니다. 이는 ComponentActivity 의 확장함수인 setContent() 함수를 살펴보아야 하는 것을 의미합니다.
public fun ComponentActivity.setContent(
parent: CompositionContext? = null,
content: @Composable () -> Unit
) {
val existingComposeView = window.decorView
.findViewById<ViewGroup>(android.R.id.content)
.getChildAt(0) as? ComposeView
if (existingComposeView != null) with(existingComposeView) {
setParentCompositionContext(parent)
setContent(content)
} else ComposeView(this).apply {
// Set content and parent **before** setContentView
// to have ComposeView create the composition on attach
setParentCompositionContext(parent)
setContent(content)
// Set the view tree owners before setting the content view so that the inflation process
// and attach listeners will see them already present
setOwners()
setContentView(this, DefaultActivityContentLayoutParams)
}
}
해당 함수 내에서 ComposeView(context = this) 를 통해 Activity 의 Context 가 전달되고 있음을 알 수 있습니다.
간단하게 정리하자면 다음과 같습니다.
- LocalContext.current 는 CompositionLocal 을 통해 전달되는 context 값을 가져 옴.
- CompositionLocal 은 상위 계층에서 설정한 값을 하위 계층에서 사용하는 개념.
- LocalContext.current 는 Composable 내에서 사용할 수 있음.
- 그러므로, LocalContext.current 를 선언할 수 있는 곳들 중 최상위는 setContent() 블럭 내부가 됨.
- setContent() 는 ComponentActivity 의 확장 함수이며, 선언 시 this 키워드를 통해 ComponentActivityContext 를 전달함.
사실 Context 에 대해서는 긴 말이 필요하지 않습니다. 명세가 굉장히 길고 기능이 많은 것이 특이하긴 하지만, 이는 사실 위 서술하였듯, God Object 임이 유명한 사실이고, 사용에도 별다른 어려움은 없으니까요. 다만, LocalContext.current 를 통해 ActivityContext 를 전달하는 과정이 꽤 궁금했는데, 이를 알아보다보니 CompositionLocal 에 대해서도 조금 더 이해 할 수 있게 되어 좋았습니다.
'Android > Tech' 카테고리의 다른 글
우리는 어떻게 Modularization 해야 하는가? (0) | 2023.08.13 |
---|---|
HLS, DASH, 그리고 오디오 포맷 (feat.속도 비교) (0) | 2023.07.07 |
StateMachine 과 Stackless Coroutine (0) | 2023.05.31 |
[Dagger-Hilt] Dagger-Hilt 의 대표적인 Annotations (0) | 2023.04.03 |
나의 MVI 적용기 (0) | 2023.03.25 |