필자는 Jetpack Compose 프레임워크를 사용하여 안드로이드 앱을 개발 중인데, 본인 뿐만 아니라 정말 많은 사람들이 안드로이드 앱이 정확히 어떤 구조로 구성되어, 각 요소가 어떤 역할을 하는지 모른채 프로젝트를 진행하는 것 같다. 앱의 구성 요소를 잘 활용하지 못하고 무분별하게 사용한다면 자원의 낭비를 초래하고 이는 성능에 큰 악영향을 미치게 될 것이다. 따라서, 안드로이드 앱의 주요 구성 요소인 Activity, Fragment, Service, Content Provider, Broadcast Receiver에 대해 알아보고자 한다.
Activity는 안드로이드 시스템의 생명주기에 의해 관리되는데, 이는 안드로이드 시스템이 Activity를 생성하는 것부터 소멸시키는 것까지의 과정을 의미한다. 생성부터 소멸까지의 과정 내에서 Activity는 다음 3가지 역할을 담당한다.
Activity는 AndroidManifest.xml 파일의 activity 태그에 name 속성으로 등록하고, intent-filter 태그 안에 MAIN 액션과 LAUNCHER 카테고리를 설정하여 앱의 시작점으로 지정할 수 있다.
Fragment는 재사용 가능한 UI의 일부를 나타내며, 전체 화면을 구성하는 UI(status bar, navigation bar)나 일시적으로 나타나는 UI(dialog, toast, snackbar)는 포함되지 않는다. Fragment는 Activity의 컨테이너 뷰 안에서 호스팅 되며, Activity와 함께 동작하면서 화면의 일부를 구성한다. Fragment의 특징은 다음과 같다.
Service는 UI 없이 백그라운드에서 장시간 실행되는 작업을 수행하는 앱의 구성요소이다. Service는 Activity 내부에서 실행되지만, 실행된 후에는 Activity의 생명주기와 독립적으로 작동한다. 따라서, Activity가 종료되더라도 Service는 유지된다. 이를 바탕으로 Service를 통해 다른 앱과 데이터를 공유하거나 상호작용 할 수 있다.
Service는 기본적으로 메인 스레드 내에서 실행되는데 Service의 작업이 무겁다면 UI를 업데이트하는 메인 스레드에 악영향을 끼칠 수 있다. 따라서 Service의 종류와 작업의 특성에 따라 가벼운 작업은 메인 스레드에서, 무거운 작업은 별도의 스레드에서 실행해야한다.
포그라운드 서비스는 사용자가 인지할 수 있는 작업을 실행하는 서비스이다. 서비스를 인지하기 위해 상태 표시 줄에 알림을 표시하여 사용자에게 서비스가 실행 중임을 알려야 한다. 알림을 통해 사용자는 서비스의 작동 상태를 지속적으로 확인할 수 있다.
예를 들어, 음악 앱 같은 경우 화면에 보이지 않아도 실행 시켜 놓을 수 있으며, 상태 표시 줄에서 음악이 재생 중인지 일시 정지 중인지 등의 정보를 확인할 수 있다. 음악 앱의 곡 정보 혹은 재생/일시정지 버튼처럼 UI에 대한 가벼운 업데이트는 메인 스레드에서 처리한다. 그러나 음악 파일을 디코딩하고 재상하는 등의 무거운 작업은 별도의 스레드에서 처리하여 앱의 반응성을 유지한다.
백그라운드 서비스는 포그라운드 서비스와는 다르게 사용자가 인지하지 못하는 작업을 실행하는 서비스이다. 즉, 사용자에게서 앱이 보이지 않는 순간에도 어딘가에서 백그라운드 작업이 처리되고 있는 것이다.
예를 들어, 날씨 앱에서 서버로부터 날씨 정보를 주기적으로 받아와야 하는 경우에 대해 생각해볼 수 있다. 앱을 실행할 때 날씨 정보를 서버로부터 받아오면 앱의 실행 속도가 저하될 수 있다. 이때 백그라운드 서비스를 사용하여 날씨 정보를 미리 받아오도록 하면 앱의 성능과 사용자 경험을 향상 시킬 수 있다. 또, 서버에서 데이터를 받아오는 작업은 네트워크 통신이 주된 작업이므로 무거운 작업에 속한다. 따라서, 해당 작업을 별도의 스레드에서 실행시키면 바람직할 것이다.
바인드 서비스는 다른 앱에서 어떤 앱의 서비스의 기능에 접근하고 데이터를 주고받을 수 있도록 하는 서비스이다. 이는 클라이언트-서버 모델과 유사하게 동작하는데, 서비스는 서버 역할을 맡게 되고, 다른 앱은 클라이언트 역할을 맡게 된다. 서비스에 바인딩 된 클라이언트가 연결을 전부 해제하여 서비스와 연결된 클라이언트가 전부 없어지면 서비스는 자동으로 종료된다.
예를 들어, 운전할 때 사용하는 네비게이션 앱과 지도 앱을 생각해보자. 이동할 때마다 변하는 위치를바인드 서비스를 통해 네비게이션 앱이 위치 정보를 지도 앱의 서비스에 요청하고 네비게이션 앱은 이 위치 정보를 통해 다음 길을 안내한다. 목적지에 도착하게 되면 네비게이션 앱은 바인딩을 해제하고 지도 앱의 바인드 서비스 또한 연결될 클라이언트가 없어 종료된다.
이러한 바인드 서비스는 실시간으로 위치 정보를 계산하고 통신하는 과정에서 상당한 처리 능력을 요구할 수 있다. 따라서 작업이 무거워질 경우, 별도의 스레드에서 서비스를 실행하면 바람직할 것이다.
![]() | 왼쪽 사진을 보면 포/백그라운 서비스(Unbounded service)와 바인드 서비스(Bounded service)에 대해서 어떻게 동작하는지 볼 수 있다. 현재 프로젝트에서 필요한 기능을 고려하여 서비스의 종류를 선택하고 해당 서비스의 생명주기에 맞추어 앱의 기능을 설계하면, 유지보수성과 확장성을 고려하여 앱을 올바르게 설계할 수 있을 것이다. |
|---|
Content Provider는 앱 간의 데이터 공유를 위한 앱의 구성 요소이다. 각 앱은 하나의 프로세스로 구성되며 자신의 프로세스에서 사용하는 데이터는 자신만 접근할 수 있도록 되어있다. 하지만, 우리가 카메라 앱에서 찍은 사진은 갤러리 앱에 자동으로 저장되는 것을 보면 데이터가 공유되는 것을 알 수 있다. 이때 데이터를 공유하기 위해 사용되는 통로가 Content Provider 이다.
![]() | 왼쪽 그림처럼 Content Provider를 통해 외부 앱에서 자신의 앱에 데이터를 전달 받아 저장할 수 도 있고, 자신의 데이터를 외부 앱에 전달 할 수 있다. 이런 방식으로 Content Provider를 통해 데이터가 공유된다. |
|---|
Content Provider가 제공하는 데이터에 접근하기 위해서는 데이터에 대한 접근을 요청할 수 있는 객체가 필요한데, 이때 사용하는 것이 Content Resolver이다. Content Provider와 Content Resolver는 서버-클라이언트 구조로 통신을 주고받는다. 즉, Content Resolver 객체는 Content Provider에 데이터를 요청하고 Content Provider는 요청된 작업에 대한 결과를 반환받을 용도로 사용된다.
Content Resolver가 요청할 데이터의 위치를 알아야 하고, Content Provider는 제공할 데이터의 위치를 알아야 하는데, 이때 사용되는 것이 Uri이다. Uri는 데이터에 접근하기 위해 사용되는 고유한 식별자로, 데이터를 가리키는데 사용된다. Uri는 다음과 같은 구조로 작성된다.
Uri는 content://authority/path/id 이러한 구조로 이루어진다. 각 요소의 역할은 다음과 같다.
Cursor는 Content Provider에서의 쿼리 결과를 저장하는 객체이다. Cursor는 쿼리 결과로 나온 테이블 형태의 데이터 집합을 읽고 탐색하는데만 사용된다. Cursor를 이동시키는 moveToFirst(), moveToNext() 와 같은 메서드들을 통해서 원하는 데이터로 이동하고, getString(), getInt()와 같은 메서드를 통해 데이터를 읽을 수 있다.
![]() | 왼쪽 그림은 Content Provider를 통해 데이터를 공유하는 자세한 과정으로 다음과 같은 순서로 이루어진다. 1. Activity 내에서 Uri를 지정하여 데이터 요청 2. 쿼리 결과를 담을 CursorLoader생성 3. Content Resolver를 통해 원하는 작업을 Content Provider에 요청 4. DB 에서 해당 데이터에 대한 결과를 Content Resolver에 Cursor 형태로 반환하여 Cursor Loader에 전달 5. Activity 내에서 Cursor 자유롭게 사용하여 데이터 사용 |
|---|
Broadcast Receiver가 뭔지 알려면 일단 Broadcast가 무엇인지부터 알아야한다. Broadcast는 시스템이나 앱에서 특정 이벤트가 발생했을 때, 해당 이벤트를 시스템의 컴포넌트나 다른 앱들에게 알리는 메시지이다. Broadcast는 시스템에서 발생하는 Broadcast가 있고 앱 자체적으로 생성하는 사용자 정의 Broadcast가 있다. Broadcast라는 메시지가 발생하면, Broadcast Receiver의 Intent 필터를 통해 원하는 Broadcast만 수신할 수 있다.
예를 들어, 네트워크 연결 상태가 변화하는 경우에 대해서 생각해보자. 와이파이를 잘 사용하고 있다가 와이파이를 꺼버리면 네트워크가 끊겼다는 Broadcast가 발생한다. 카카오톡처럼 네트워크 연결이 필요한 앱은 네트워크 연결 상태 변화 Broadcast를 수신하도록 Broadcast Receiver를 등록할 수 있다. 등록된 Broadcaste Receiver를 통해 카카오톡은 Broadcast를 수신하여 네트워크와 관련된 기능을 전부 차단하는 등의 작업을 수행할 수 있게 된다.
정적 Broadcast Receiver는 AndroidManifest.xml 파일에 등록하여 사용하는 Broadcast Receiver를 말한다. AndroidManifest.xml에 Broadcast Receiver를 선언하면 시스템에 등록되어 앱의 실행 여부와 관계 없이 앱을 설치하면 Broadcast를 수신할 수 있다. 따라서 앱이 실행되지 않은 상태에서도 시스템 또는 다른 앱에서 발생하는 이벤트를 수신하여 처리해야할 때 유용하다.
예를 들어, 위와 같이 AndroidManifest.xml 파일에 등록한 Broadcast Receiver를 보자. 태그를 사용하여 Broadcast Receiver를 등록할 수 있고, 에서 어떤 Broadcast를 수신할 지 를 통해 정할 수 있다. android.intent.action.BOOT_COMPLETED 에 의해 시스템 부팅이 완료되면 Broadcast를 수신함으로써 MyBroadcastReceiver가 실행된다.
동적 Broadcast Receiver는 앱이 실행 중일 때 registerReceiver() 메서드를 통해 등록하고 unregisterReceiver() 메서드를 통해 해제하여 사용하는 Broadcast Receiver 이다. 동적 Broadcast Receiver는 Activity나 Service와 같은 컴포넌트의 생명주기와 연결되어 작동하고, 해당 컴포넌트가 활성화된 동안에만 Broadcast를 수신한다. 따라서, 컴포넌트에 등록한 Broadcast Receiver를 더 이상 사용하지 않거나, 컴포넌트가 종료될 때 메모리 누수를 방지하기 위해 Broadcast Receiver를 해제해야 한다.
https://developer.android.com/topic/architecture?hl=ko
https://developer.android.com/guide/components/activities/intro-activities?hl=ko
https://developer.android.com/guide/fragments?hl=ko
https://developer.android.com/develop/background-work/services?hl=ko
https://developer.android.com/guide/topics/providers/content-providers?hl=ko
https://developer.android.com/develop/background-work/background-tasks/broadcasts?hl=ko