본문 바로가기

Android/Tech

Android Context Details (feat.LocalContext)

Unsplash, Gulfer ergin.

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 가 전달되고 있음을 알 수 있습니다. 

 

간단하게 정리하자면 다음과 같습니다.

 

  1. LocalContext.currentCompositionLocal 을 통해 전달되는 context 값을 가져 옴.
  2. CompositionLocal 은 상위 계층에서 설정한 값을 하위 계층에서 사용하는 개념.
  3. LocalContext.current 는 Composable 내에서 사용할 수 있음.
  4. 그러므로, LocalContext.current 를 선언할 수 있는 곳들 중 최상위는 setContent() 블럭 내부가 됨.
  5. setContent()ComponentActivity 의 확장 함수이며, 선언 시 this 키워드를 통해 ComponentActivityContext 를 전달함.

 


사실 Context 에 대해서는 긴 말이 필요하지 않습니다. 명세가 굉장히 길고 기능이 많은 것이 특이하긴 하지만, 이는 사실 위 서술하였듯, God Object 임이 유명한 사실이고, 사용에도 별다른 어려움은 없으니까요. 다만, LocalContext.current 를 통해 ActivityContext 를 전달하는 과정이 꽤 궁금했는데, 이를 알아보다보니 CompositionLocal 에 대해서도 조금 더 이해 할 수 있게 되어 좋았습니다.