안드로이드 앱 구성 요소와 기본 개념

StudyBug·2025년 3월 23일
post-thumbnail

필자는 Jetpack Compose 프레임워크를 사용하여 안드로이드 앱을 개발 중인데, 본인 뿐만 아니라 정말 많은 사람들이 안드로이드 앱이 정확히 어떤 구조로 구성되어, 각 요소가 어떤 역할을 하는지 모른채 프로젝트를 진행하는 것 같다. 앱의 구성 요소를 잘 활용하지 못하고 무분별하게 사용한다면 자원의 낭비를 초래하고 이는 성능에 큰 악영향을 미치게 될 것이다. 따라서, 안드로이드 앱의 주요 구성 요소인 Activity, Fragment, Service, Content Provider, Broadcast Receiver에 대해 알아보고자 한다.

Activity

Activity는 안드로이드 시스템의 생명주기에 의해 관리되는데, 이는 안드로이드 시스템이 Activity를 생성하는 것부터 소멸시키는 것까지의 과정을 의미한다. 생성부터 소멸까지의 과정 내에서 Activity는 다음 3가지 역할을 담당한다.

  • UI로 구성된 화면 제시: 배경 색상, 버튼, 이미지 등등으로 이루어져 사용자에게 보여지는 화면을 의미한다.
  • 앱의 로직 처리: 네트워크 통신, 클릭 이벤트 처리, 데이터 베이스 접근 등 앱 내에서 이루어지는 비즈니스 로직에 대한 수행을 의미한다. 즉, 핵심적인 동작을 제어하고 데이터를 관리한다는 의미이다.
  • 자신의 생명주기 상태에 따른 생명주기 메서드 호출: Activity의 생명주기 상태는 4가지로 분류 되는데, 상태 변화가 일어났을 때 각 상태에 대응되는 생명주기 메서드가 실행된다.
    • 생명주기 상태
      • Running: 사용자가 앱과 상호작용할 수 있는 상태
      • Paused: 전화가 걸려오거나 알림으로 인해 화면이 일부 가려져 Activity에 대한 상호작용이 잠시 정지된 상태
      • Stopped: 앱이 백그라운드로 전환되는 것처럼, 앱이 사용자에게 표시되지 않는 상태
      • Destroyed: 앱이 종료되어 Activity가 메모리에서 제거된 상태
    • 생명주기 메서드
      • onCreate(): Activity가 처음 생성될 때 호출되어 초기화 작업을 수행한다.
      • onStart(): Activity가 화면에 표시되기 직전에 호출되어 구성된 UI를 화면에 보여준다.
      • onResume(): 사용자가 화면을 통해 앱과 상호작용할 수 있도록 한다.
      • onPause(): Paused 상태가 되었을 때 호출되어 입력중이던 텍스트, 스크롤 위치와 같은 가벼운 정보를 저장한다.
      • onStop(): Stopped 상태일 때 호출되며, 메모리 부족 시 앱이 강제 종료될 수 있다.
      • onRestart(): 앱이 종료되지 않은 Stopped 상태 에서 다시 시작될 때 호출된다. 이때 뒤 따라서 onStart()가 호출되어 사용자에게 화면을 보여주고 이후 onResume()을 통해 사용자가 앱과 상호작용할 수 있도록 한다.
      • onDestroy(): Activity가 완전히 종료되기 직전에 호출되어 모든 리소스를 해제한다.

Activity는 AndroidManifest.xml 파일의 activity 태그에 name 속성으로 등록하고, intent-filter 태그 안에 MAIN 액션과 LAUNCHER 카테고리를 설정하여 앱의 시작점으로 지정할 수 있다.

Fragment

Fragment는 재사용 가능한 UI의 일부를 나타내며, 전체 화면을 구성하는 UI(status bar, navigation bar)나 일시적으로 나타나는 UI(dialog, toast, snackbar)는 포함되지 않는다. Fragment는 Activity의 컨테이너 뷰 안에서 호스팅 되며, Activity와 함께 동작하면서 화면의 일부를 구성한다. Fragment의 특징은 다음과 같다.

  • 독립성:
    • 자체적인 생명주기를 지니며, Activity의 생명주기와 독립적으로 동작할 수 있다. 예를 들어, 화면 전환 시에 생명주기의 생성 비용이 큰 Activity 대신 비용이 적은 Fragment만을 교체해 효율적인 화면 전환이 가능하다. 또한, Fragment는 Activity가 일시 중지되더라도 자체 생명주기를 유지하며, 필요에 따라 상태를 저장하고 복원할 수 있다.
  • 모듈성:
    • Fragment는 독립적인 UI 모듈로서, 다른 Fragment를 포함하거나 Activity에서 재사용될 수 있다. 이를 통해 코드의 재사용성을 높이고, 앱의 유지보수성과 확장성을 향상시킬 수 있다. 예를 들어, 동일한 버튼을 이름만 바꿔서 여러 곳에 사용해야 할 때 Fragment를 활용하면 중복 코드를 줄일 수 있다.
  • 부분 업데이트:
    • Fragment는 자체 레이아웃과 데이터를 관리하므로, invalidate()이라는 특정 함수를 통해 화면의 일부분만 업데이트할 수 있다. 예를 들어, Activity 단위로 UI를 갱신하면 변경되지 않은 요소까지 다시 렌더링해야 하지만, Fragment 단위의 갱신을 통해 변경된 부분만 업데이트할 수 있다. 이는 성능적인 이점을 제공하며, 특히 복잡한 UI에서 더욱 효과적이다.

Service

Service는 UI 없이 백그라운드에서 장시간 실행되는 작업을 수행하는 앱의 구성요소이다. Service는 Activity 내부에서 실행되지만, 실행된 후에는 Activity의 생명주기와 독립적으로 작동한다. 따라서, Activity가 종료되더라도 Service는 유지된다. 이를 바탕으로 Service를 통해 다른 앱과 데이터를 공유하거나 상호작용 할 수 있다.

Service는 기본적으로 메인 스레드 내에서 실행되는데 Service의 작업이 무겁다면 UI를 업데이트하는 메인 스레드에 악영향을 끼칠 수 있다. 따라서 Service의 종류와 작업의 특성에 따라 가벼운 작업은 메인 스레드에서, 무거운 작업은 별도의 스레드에서 실행해야한다.

Service의 종류

포그라운드 서비스

포그라운드 서비스는 사용자가 인지할 수 있는 작업을 실행하는 서비스이다. 서비스를 인지하기 위해 상태 표시 줄에 알림을 표시하여 사용자에게 서비스가 실행 중임을 알려야 한다. 알림을 통해 사용자는 서비스의 작동 상태를 지속적으로 확인할 수 있다.

예를 들어, 음악 앱 같은 경우 화면에 보이지 않아도 실행 시켜 놓을 수 있으며, 상태 표시 줄에서 음악이 재생 중인지 일시 정지 중인지 등의 정보를 확인할 수 있다. 음악 앱의 곡 정보 혹은 재생/일시정지 버튼처럼 UI에 대한 가벼운 업데이트는 메인 스레드에서 처리한다. 그러나 음악 파일을 디코딩하고 재상하는 등의 무거운 작업은 별도의 스레드에서 처리하여 앱의 반응성을 유지한다.

백그라운드 서비스

백그라운드 서비스는 포그라운드 서비스와는 다르게 사용자가 인지하지 못하는 작업을 실행하는 서비스이다. 즉, 사용자에게서 앱이 보이지 않는 순간에도 어딘가에서 백그라운드 작업이 처리되고 있는 것이다.

예를 들어, 날씨 앱에서 서버로부터 날씨 정보를 주기적으로 받아와야 하는 경우에 대해 생각해볼 수 있다. 앱을 실행할 때 날씨 정보를 서버로부터 받아오면 앱의 실행 속도가 저하될 수 있다. 이때 백그라운드 서비스를 사용하여 날씨 정보를 미리 받아오도록 하면 앱의 성능과 사용자 경험을 향상 시킬 수 있다. 또, 서버에서 데이터를 받아오는 작업은 네트워크 통신이 주된 작업이므로 무거운 작업에 속한다. 따라서, 해당 작업을 별도의 스레드에서 실행시키면 바람직할 것이다.

바인드 서비스

바인드 서비스는 다른 앱에서 어떤 앱의 서비스의 기능에 접근하고 데이터를 주고받을 수 있도록 하는 서비스이다. 이는 클라이언트-서버 모델과 유사하게 동작하는데, 서비스는 서버 역할을 맡게 되고, 다른 앱은 클라이언트 역할을 맡게 된다. 서비스에 바인딩 된 클라이언트가 연결을 전부 해제하여 서비스와 연결된 클라이언트가 전부 없어지면 서비스는 자동으로 종료된다.

예를 들어, 운전할 때 사용하는 네비게이션 앱과 지도 앱을 생각해보자. 이동할 때마다 변하는 위치를바인드 서비스를 통해 네비게이션 앱이 위치 정보를 지도 앱의 서비스에 요청하고 네비게이션 앱은 이 위치 정보를 통해 다음 길을 안내한다. 목적지에 도착하게 되면 네비게이션 앱은 바인딩을 해제하고 지도 앱의 바인드 서비스 또한 연결될 클라이언트가 없어 종료된다.

이러한 바인드 서비스는 실시간으로 위치 정보를 계산하고 통신하는 과정에서 상당한 처리 능력을 요구할 수 있다. 따라서 작업이 무거워질 경우, 별도의 스레드에서 서비스를 실행하면 바람직할 것이다.

Service의 생명주기

왼쪽 사진을 보면 포/백그라운 서비스(Unbounded service)와 바인드 서비스(Bounded service)에 대해서 어떻게 동작하는지 볼 수 있다.

현재 프로젝트에서 필요한 기능을 고려하여 서비스의 종류를 선택하고 해당 서비스의 생명주기에 맞추어 앱의 기능을 설계하면, 유지보수성과 확장성을 고려하여 앱을 올바르게 설계할 수 있을 것이다.

Content Provider

Content Provider는 앱 간의 데이터 공유를 위한 앱의 구성 요소이다. 각 앱은 하나의 프로세스로 구성되며 자신의 프로세스에서 사용하는 데이터는 자신만 접근할 수 있도록 되어있다. 하지만, 우리가 카메라 앱에서 찍은 사진은 갤러리 앱에 자동으로 저장되는 것을 보면 데이터가 공유되는 것을 알 수 있다. 이때 데이터를 공유하기 위해 사용되는 통로가 Content Provider 이다.

왼쪽 그림처럼 Content Provider를 통해 외부 앱에서 자신의 앱에 데이터를 전달 받아 저장할 수 도 있고, 자신의 데이터를 외부 앱에 전달 할 수 있다. 이런 방식으로 Content Provider를 통해 데이터가 공유된다.

Content Provider의 주요 구성 요소

Content Resolver

Content Provider가 제공하는 데이터에 접근하기 위해서는 데이터에 대한 접근을 요청할 수 있는 객체가 필요한데, 이때 사용하는 것이 Content Resolver이다. Content Provider와 Content Resolver는 서버-클라이언트 구조로 통신을 주고받는다. 즉, Content Resolver 객체는 Content Provider에 데이터를 요청하고 Content Provider는 요청된 작업에 대한 결과를 반환받을 용도로 사용된다.

Uri

Content Resolver가 요청할 데이터의 위치를 알아야 하고, Content Provider는 제공할 데이터의 위치를 알아야 하는데, 이때 사용되는 것이 Uri이다. Uri는 데이터에 접근하기 위해 사용되는 고유한 식별자로, 데이터를 가리키는데 사용된다. Uri는 다음과 같은 구조로 작성된다.

Uri는 content://authority/path/id 이러한 구조로 이루어진다. 각 요소의 역할은 다음과 같다.

  • content:// : 항상 content:// 로 시작되며 Uri가 Content Provider를 통해 관리되는 데이터임을 나타낸다.
  • authority : Content Provider를 식별하는 고유 이름으로, com.example.kkk 와 같은 형식으로 작성된다.
  • path : 테이블 또는 파일을 가리키는 이름이다.
  • id : 테이블 내의 특정 행을 지칭한다.

Cursor

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 Receiver가 뭔지 알려면 일단 Broadcast가 무엇인지부터 알아야한다. Broadcast는 시스템이나 앱에서 특정 이벤트가 발생했을 때, 해당 이벤트를 시스템의 컴포넌트나 다른 앱들에게 알리는 메시지이다. Broadcast는 시스템에서 발생하는 Broadcast가 있고 앱 자체적으로 생성하는 사용자 정의 Broadcast가 있다. Broadcast라는 메시지가 발생하면, Broadcast Receiver의 Intent 필터를 통해 원하는 Broadcast만 수신할 수 있다.

예를 들어, 네트워크 연결 상태가 변화하는 경우에 대해서 생각해보자. 와이파이를 잘 사용하고 있다가 와이파이를 꺼버리면 네트워크가 끊겼다는 Broadcast가 발생한다. 카카오톡처럼 네트워크 연결이 필요한 앱은 네트워크 연결 상태 변화 Broadcast를 수신하도록 Broadcast Receiver를 등록할 수 있다. 등록된 Broadcaste Receiver를 통해 카카오톡은 Broadcast를 수신하여 네트워크와 관련된 기능을 전부 차단하는 등의 작업을 수행할 수 있게 된다.

Broadcast Receiver의 종류

정적 Broadcast Receiver

정적 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

동적 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

profile
갈 길이 먼 개발자 꿈나무

0개의 댓글