[안드로이드] Context 총정리

SHY(code poet)·2024년 4월 28일
1

1. 어디서 봤더라?

우리는 종종 코드를 짜다 'context'라는 키워드를 접하게 된다.
흔히, intent를 쓸 때 많이 봐왔을 것이고,

상단의 import 부분을 주의 깊게 봤다면, context라는 글자를 보게 된다.

이뿐만 아니라, Toasat, Adatpers, Inflaters, SharedPreferenes, SystemServices 등을 다룰 때, Context를 인자로 넣어야 한다.

그런데, 지금까지는 그냥 채워넣으라고 해서 채워넣었지만, 한번도 깊게 알아볼 생각은 못했다.
그래서, 이번 기회에 context에 대해 총정리 해보고자 한다.

2. Context의 정의

"Context는 어플리케이션 환경에 관한 전체 정보를 받을 수 있는 추상 클래스(인터페이스)입니다. 이를 통해 애플리케이션별 리소스 및 클래스에 대한 액세스는 물론 액티비티 호출, 브로드캐스팅 및 인텐트 수신 등과 같은 애플리케이션 수준 작업에 대한 상향 호출이 가능합니다."

무슨 말인가 싶다. 하나하나 뜯어보자.

1. 전체정보

Context를 사전적 정의로 "맥락"이라는 뜻을 지닌다.
즉, 'app의 현재 상태' 내지는 '앱이 흘러가는 맥락'정도로 이해하면 충분하다.

2. 추상 클래스(인터페이스)


보다시피 Context는 Object를 상속받는 abstract class이다.
말그대로, '추상' 클래스이기 때문에 이를 사용하기 위해서는 구현체가 있어야 한다. 그 구현체가 바로 아래의 도식에 나와있다.

도식에서 볼 수 있듯, Object아래에 ContentProvider, Context, BroadReceiver가 있다. 우리가 안드로이드 4대 컴포넌트를 얘기할 때 쓰는 그것들이 맞다. 여기서 알 수 있듯, contentProvider와 BroadcastReciever는 Context의 구현체가 아니다. 즉, 자기 자신만의 context 맥락을 가지고 있지 않다는 것이다. 그러나, 실제적으로 사용할 때는 onReceive() 함수를 통해 다른 context를 인자로 전달 받아서 사용하며, 전달받은 Context의 생명주기를 따르게 된다.

이제 본격적으로 Context에 대해 알아보자.
Context는 추상 클래스이기 때문에 기본적인 구현체가 있어야 한다고 말했는데, 여기서 기본 구현체는 'ContextImpl'이다.
그러나 이 구현체는 사용자에게 직접적으로 노출되지는 않고,
'ContextWrapper'로 감싸져 있다.

보다시피, Context를 상속받는 ContestWrapper가 있다.
아래에 mBase라는 맴버변수가 선언되어 있는데, mBase는 원본 Context의 타입의 변수로, 원본 Context 객체를 참조하는 역할을 한다.
아래의 attachBaseContext함수는 이 mBase를 설정한 후, Context 구현체를 재정의하거나 확장하는데 사용하며,
Activity, Service, Applicaition 컴포넌트는 각각 ContextImpl을 생성하고, getBaseContext(), getApplicationContext()메서드를 통해 mBase를 return하여 Context를 가져온다.
(이는 아래에서 다시 설명할 것이다.)

참고로, ContextImpl은 앱에서 직접 사용할 수 있는 클래스가 아니어서 소스코드로만 볼 수 있다.

아래는, 이 내용들을 도식화한 것이다.

Activirt, Service, Application은 ContextWrapper을 상속하고, Context의 기능을 사용할 수 있게 되는 것이다.
그러면 이제 이들 각각이 어떻게, context를 구현하고 있는지 알아보자.

잠깐! 바로 ContextImpl로 구현하지, 왜 번거롭게 Wrapper로 감싸주나?
👉 보호 PROXY패턴

  • proxy는 '대리(인)','대신'이라는 뜻으로, 무언가를 대신한다는 의미를 지닌다.(proxy server할 때, 그 proxy다.)
  • '상속보다는 구성을 사용하라'는 객체 지향의 원칙을 준수하기 위함이다. 즉, 컴포넌트(Activity,Service,Application)들이 ContextImpl을 직접 상속해서 Context 메소드를 사용하는게 아니라, 중간에 '대리인'으로 ContextWrapper을 두어 메소드를 사용하게 하는 것이다.
  • 이렇게 함으로써, ContextImpl은 Context 클래스가 구현한 API메서드만을 구현하는 데 집중하고, ContextWrapper는 원본 Context를 변화시키지 않으면서도 이를 다양한 용도로 사용함으로써 역할을 분리할 수 있다.

3. Appliciation Context

  • Application Context는 싱글톤으로 프로세스에서 1개만 존재한다.
  • application 객체가 생성될 때, 함께 이 application conext 객체가 생성되기 때문에 application contextsms 동일한 앱 안에서 항상 동일한 인스턴스를 반환한다.
  • 애플리케이션의 생명주기를 따르기 때문에, 앱의 시작부터 종료까지 살아있다.
  • 액티비티의 Context가 종료되고 나서도, 오랫동안 지속되거나 앱 전역에서 사용할 Context가 필요할 때 활용하는 것이 적합하다.

4. Activity Context

  • 액티비티가 생성될 때마다 각자의 Context가 생성된다.
    (Service도 마찬가지이다.)
  • Activity 자체가 Context를 상속하고 있기 때문에, Activity 인스턴스 자체가 Context역할을 한다.
    ⇒ 따라서, Activity Context는 액티비티의 생명주기에 종속되기에, activity가 소멸되면 해당 context도 함께 소멸된다.

🚫주의
application이 activity보다 더 큰 범위인 것은 맞으나, context의 관점에서 이야기할 때는, 포함관계라기 보다는 서로 다른 역할을 하는 여집합 관계라고 보는 것이 옳다.
Application Context는 Activity Context가 지원하는 모든 것을 지원하지 않는다. 따라서, GUI 관련 동작들에 있어서는 Application Context에서 관리하면 안된다.

5. Context 가져오기(Context 참조)

context를 가져온다는 게 무슨 의미일까?앞서 얘기했듯, 해당 흐름을 가져오는 것이다.
즉, Activity Context를 가져오면, 해당 activity의 현재 상황에 관련된 모든 맥락과 정보들을 가져오는 것이고, Application Context을 가져오면 해당 app에 관한 모든 것들을 가져온다는 것이다.

🚫주의!
Context를 가져와 직접 접근할 수 있으려면, 해당 코드가 activity 혹은 fragment와 연관되어 있어야 한다.
예를들어, adapter class를 하나만들었다고 했을 때, 이 adapter는 activity 혹은 fragment의 생명주기와 직접 연결되어 있지 않기 때문에 참조할 수 없다.(실제로 참조 코드를 쳐도 에러가 난다.)
굳이 context를 참조하고 싶다면 context를 adapter의 생성자에 전달하여 activity 혹은 fragment의 context와 연결시키는 방법이 있다.
그러나 이경우, activity가 destroy되어도 context를 참조하고 있을 수 있으므로, 아래와 같이 참조를 해제해주어야 한다.

override fun onDestroy() {
        super.onDestroy()
        // Fragment가 소멸될 때 Adapter에서 Context 참조 해제 
        adapter.clearContextReference()
    }
}
fun clearContextReference() {
        context = null
    }

1. View에서 Activity Context 가져오기

this@ActivityName(Java의 경우, AcitivityName.this)

  • 우리가 intent할 때 많이 보아오던 그것이다.

getActivity() / requireActivity()

  • 이 함수는 fragment에서 fragment에 연결된 acitivity context를 가져오고 싶을 때 사용한다.
  • mbase가 아닌 mHost를 리턴하는 것을 볼 수 있는데, 여기서 필요한 것은 원본 context가 아니라, Host activity(혹은 fragment)의 context를 필요로 하기 때문이다.
  • requireActivity()의 경우, null값 반환이 아닌, 예외처리를 한번 해주고 있기 때문에, Context가 반드시 존재하는 경우에 사용해주면 된다.

getContext() / requireContext()

  • 이 함수의 경우, 현재 View가 속한 컨텍스트(Context)를 반환한다.
  • getActivity()가 fragment에서만 사용되는 반면 이 함수들은 더 광범위하게 쓰일 수 있다.

2. activity에서 Application Context 가져오기

getApplicationContext()

  • Application Context를 반환한다.
  • Activity로 케스팅하면 ClassCaseException이 발생한다.

getApplication()

  • Application 인스턴스를 반환한다. Application도 Context의 자식 클래스이므로 Context처럼 사용할 수 있다.
  • 하지만 Application과 Application Context가 항상 동일한 인스턴스라는 보장이 없고, Application은 Activity나 Service 내부에서만 참조가 가능하다는 차이점도 있다.

3. 원본 Context 가져오기

getBaseContext()

  • mBase 즉, ContextImpl 인스턴스를 반환한다.
  • 상위단계의 인스턴스이기 때문에, 여기서 가져온 것을 Activity로 캐스팅하면 ClassCaseException이 발생한다.

✅참고: Context 유스케이스

  • 핵심: 보다시피, dialog, activity, inflation 등, view UI적인 요소들언 전부 Acitivity Context에서 관리하고 있다.
    만약 다른 context에서 관리한다면? 아래에서 살펴보도록 하자.

6. Context와 메모리 누수

위의 주의사항에서 얘기했듯, 이러한 Context 참조는 메모리 누수와 매우 밀접하게 관련되어 있기 때문에, 함부로 context를 참조하면 안되고, 역할에 따라 잘 구분해서 사용해야 한다.

메모리 누수란, 더이상 필요하지 않은 리소스가 RAM에 해제되지 않고 계속 남아있는 것을 말한다. 메모리가 누수되면, 에플리케이션에 할당된 메모리가 초과되어 크래쉬가 발생하게 된다.

ⓐ Activity와 분리된 작업에 Acitivity Context를 사용해서는 안된다. (activity context의 옳지 못한 참조의 예)

  • 어플리케이션 내에 전역으로 사용할 싱글톤 객체를 만드려고 하는데 이 객체가 Context를 필요로 할 때, Application Context 를 사용하면 된다. 만약 이런 상황에 Activity Context를 넘겨주게 되면, Activity가 Destroy되어 사용되지 않을 때에도, 싱글톤 객체가 Activity에 대한 참조를 메모리에 남겨두며 GC(Garbage Collected)되지 않은 채 메모리 릭이 발생할 것이다.

ⓑ Activity에 종속된 작업에 Application Context를 사용해서는 안된다. (appliciation context의 옳지 못한 참조의 예)

  • 특히나, Application Context의 사용은 주의에 주의를 기울여야 한다. Activity는 Garbage Colletion이 가능하지만, Application은 앱 프로세스가 살아있는 한 계속하여 남아있기 때문에, 만약 Appliciation Context를 활용한 객체를 메모리에서 할당 해제하지 않고 있으면, 메모리 릭이 발생할 수 있다.
    이는 해당 앱이 악성앱으로 탐지되게 하는 요인이다.

이 외에도, 메모리 누수의 원인은 다음과 같다.
static 변수

  • static변수는 그 자체로 클래스 자체에 속하기 때문에 activity context와 관련이 없으나, 만약 static 변수에 Activity의 Context를 저장하면 Activity가 onDestroy()되더라도 Context가 메모리에 유지될 수 있으므로, 메모리 누수 문제가 발생할 수 있다.
    따라서, onDestroy()를 오버라이드하여, static 변수를 null로 만들어주자.

Singleton

  • 싱글톤 또한 위의 경우와 마찬가지로, Activity Context를 참조하게 될 경우, 메모리 누수가 발생할 수 있다.
  • 싱글톤의 경우 전역적으로 사용하기 위해 쓰인 것이기 때문에, Application Context를 사용하는 것이 맞다.
  • 따라서, MVC, MVVM, MVP등의 적절한 아키텍쳐로 View와 분리를 하여 Activity Context에 대한 접근이 필요없게 해야한다.

inner class

  • 기본적으로 inner class는 상위 class에 대한 참조를 유지하고 있고, 상위 class의 인스턴스가 해제되면, inner class의 참조도 해제되어 있어야 하지만, inner class가 다른 오브젝트나 컨텍스트에 의해 참조되어 있을 경우, 상위 class는 가비지 컬렉션을 통해 해제될 수 없으며, 이는 메모리 누수의 요인인 된다.
    (가비지 컬렉션은 참조되지 않는 객체를 해제하는 역할을 하나, inner class가 계속 참조하고 있기 때문에, 상위 클래스를 계속 해제하지 못하고 있는 것이다.)
    Thread, Background, EventHandling
  • 비동기 작업으로 activity가 종료된 뒤에도 백그라운드에서 계속 실행되는 모든 작업들은 activity에 참조되어 있을 경우, onDestroy 단계 때, 중단시키는 것이 핵심이다.


TIP. 메모리 누수 탐지도구

  • Memory Profiler: 안드로이드 스튜디오에 있으며 가비지 컬렉션과 메모리 소비에 대한 정보를 표시하는 가장 간편한 도구이다.

  • Leak Canary: 이 라이브러리를 앱에 설치하면, 메모리 누수를 발생시키는 것들을 추적할 수 있다.

❤️이 글의 결론❤️
액티비티에서 사용된다면 Activity Context
에플리케이션 전역에서 사용된다면 Application Context를 사용하자.
(백그라운드 작업 또는 데이터 엑세스와 같은, Activity cycle에 종속되지 않고 유지되어야 하는 작업)

📜Reference
Context, 너 대체 뭐야?
Context 넌 대체 무엇이더냐
Context 제대로 알고 사용하자
안드로이드의 Context와 메모리누수
Why use ContextImpl to implement Context rather than ContextWrapper in Android?

profile
그것을 이해하고자 하기 때문에 결국은 그것을 견디어내게 된다.

0개의 댓글