오늘은 안드로이드를 이루는 기본 요소들에 대해 알아볼 것이다.
안드로이드의 기본 컴포넌트는 크게 4가지로 나누어진다.
Activity, Service, Broadcast Receiver, Content Provider로 이루어지는데 이에 대해 하나씩 자세히 알아볼 것이다.
추가로 이들과 함께 사용되면서 안드로이드의 핵심을 이루는 Manifest와 Intent도 함께 공부해 보자.
액티비티 (Activity)
액티비티는 사용자와 상호작용하는 UI 화면을 담당하는 컴포넌트이다.
모든 앱에 반드시 1개 이상 존재하며 사용자가 앱을 실행하면 가장 먼저 마주하게 된다. 즉, 앱과 상호작용하기 위한 진입점이라 할 수 있으며 앱을 실행할 때 앱 전체를 호출하는 게 아니라 앱의 액티비티를 호출하게 된다.
액티비티 생명주기 (Lifecycle)
액티비티는 상태에 따라 호출되는 여러 콜백 메서드를 가지고 있으며 이를 생명주기(Lifecycle)라고 부른다. 개발자들은 이 생명주기의 상황에 맞추어 코드를 작성해 앱의 안정성을 높일 수 있다.
만약, 이를 제대로 알지 못한 채로 개발을 하게 되면 다른 앱으로 전환 시, 비정상 종료되는 문제나 시스템 리소스 누수, 화면 전환 시 상태 저장 오류등을 일으킬 수 있다.
- onCreate(): 액티비티가 처음 생성될 때 필수적으로 호출된다. 앱이 작동할 때 한 번만 동작해야 하는 초기화나 화면 레이아웃 설정 같은 작업이 수행된다.
- onStart(): 액티비티가 사용자에게 보이기 직전에 호출된다. 해당 액티비티를 포그라운드로 보내 상호작용할 수 있도록 준비한다.
- onResume(): 액티비티가 사용자와 상호작용하기 시작할 때 호출된다. 액티비티가 포그라운드에 표시되며 앱에서 포커싱이 사라질 때까지 이 상태를 유지한다.
- onPause(): 액티비티가 다른 액티비티에 가려지거나 사용자의 포커스가 사라졌을 때 호출된다. 즉, 사용자가 떠날 때 발생하는 첫 번째 신호이다. 매우 짧은 상태이며 실행 중이지 않을 때 필요 없는 리소스들을 해제할 수 있다. 때문에 오래 걸리는 I/O작업이나 통신 작업은 지양해야 한다.
- onStop(): 액티비티가 완전히 가려지거나 사용자에게 더 이상 보이지 않을 때 호출된다. 프로세스가 소멸될 수 있으므로 그전에 마무리 작업을 하는 편이 좋다.
- onDestory(): 액티비티가 완전히 소멸되기 전에 호출된다. 주로 finish가 호출되거나 기기회전 같은 configurationChange가 발생할 경우 호출될 수 있다.
만약, 예기치 못하게 앱이 종료되었을 때 복구 과정도 존재한다.
보통 액티비티가 onStop 상태로 전환되기 직전, 시스템은 예기치 못하게 액티비티가 파괴될 가능성을 고려하여 onSaveInstanceState 메서드를 호출하며 현재 앱의 UI 상태를 저장한다. 주로 화면 회전, 메모리 부족, 언어 변경이나 구성을 변경할 경우 발생할 수 있다.
일반적으로 복구 과정은 onCreate에서 수행된다. onCreate(Bundle savedInstanceState) 메서드의 파라미터를 확인하는 방식이다. 추가로, onStart 이후에 호출되는 onRestoreInstanceState 메서드에서도 복구가 가능하다. 이 메서드는 복구할 상태가 있을 때만 호출되기 때문에 코드의 의도를 더 명확하게 표현 가능하다.
생명주기를 관찰하기 가장 좋은 방법은 테스트 앱을 만들어서 로그를 찍어보는 것이다.
다음 코드를 실행해보면 생명주기를 확실하게 알 수 있다. 홈 버튼을 한 번만 누를 시, Pause 후 Stop이 실행되며 홈 버튼을 길게 누르면 갤럭시의 경우 AI 어시스턴트가 호출된다. 이 때 메인 액티비티가 "일부" 가려지기 때문에 Pause까지만 호출되는걸 볼 수 있었다. [액티비티 생명주기 테스트 코드]
예시를 하나 들어보자.
만약, 유튜브 영상 시청 중 전화가 왔다고 가정해 보자. 전화를 받고 유튜브 앱으로 돌아왔을 때 영상은 끊겼던 시점에 멈춰서 자연스럽게 다시 재생될 것이다. 이 과정에서 일어나는 상태변화를 살펴보자.
1. 전화가 걸려왔을 때 (유튜브 앱이 화면에서 가려질 때)
- onPause가 호출된 후 onStop이 호출된다.
- 전화에 의해 유튜브 앱이 완전히 가려지게 되면서 Activity는 파괴되지 않고, 메모리상에 '중지(Stopped)' 상태로 남아있게 된다. 안드로이드 OS는 효율성을 추구하기 때문에 바로 프로세스를 소멸시키지 않고 사용자가 빨리 돌아올 수 있으니 일단 대기하는 것이다.
2. 통화 종료 후 다시 유튜브 앱으로 돌아왔을 때
- Activity가 메모리에 살아있기 때문에 onRestart, onStart, onResume의 순서로 호출이 되면서 다시 화면이 보인다.
여기서 궁금해지는 것은 요즘 유튜브를 보다 전화가 걸려오면 아주 작은 팝업 아이콘으로 표시가 되며 전화를 받으면서 동시에 유튜브를 볼 수도 있다. 이때 생명주기는 어떻게 될까?
위의 1번과 생명주기 내용을 다시 보면 "전화에 의해 유튜브 앱이 완전히 가려지게 되면서"라고 적혀있고 생명주기 또한 완전히 가려졌을 때 onStop이 호출된다. 하지만 전화 팝업으로 뜰 경우 유튜브 앱이 완전히 가려진 것은 아니기 때문에 onPause까지만 호출되고 그곳에서 머무르게 된다. 즉, 이 경우에 전화를 끊고 유튜브로 돌아오면 onPause 상태에서 곧바로 onResume으로 복귀할 수 있게 된다.
그렇다면 완전히 Activity가 파괴되는 onDestory가 호출되는 경우도 있을까?
결론적으로 그렇다. 만약 사용자가 뒤로 가기를 눌러 명시적으로 유튜브 Activity를 종료할 경우에는 onDestroy가 호출된다.
또한, 전화를 받는 도중 시스템 메모리가 부족해지면 OS가 자원 확보를 위해 백그라운드의 유튜브 프로세스를 강제로 종료시켜도 onDestroy가 호출된다.
서비스 (Service)
서비스는 백그라운드에서 오래 실행되어야 하는 작업을 위한 컴포넌트를 말한다. UI가 없으며 사용자가 다른 앱으로 전환하거나 종료되어도 계속 동작이 필요한 경우에 사용된다. 가령, 음악 재생이나 파일 다운로드, 데이터 동기화 같은 작업에 사용된다.
서비스의 종류
서비스의 종류는 크게 세 가지 유형으로 나뉠 수 있다.
- 포그라운드 서비스 (Foreground Service): 사용자에게 보이는 작업으로 서비스가 실행 중임을 명확하게 인지할 수 있는 서비스이다. 반드시 상태 표시줄에 알림(Notification)으로 사용자에게 알려야 하며, 시스템에 의해 강제로 종료될 확률이 낮다. 음악 플레이어는 음악이 재생되는 동안 알림의 형태로 상태바에 나타나고 있는 대표적 예시이다.
- 백그라운드 서비스 (Background Service): 사용자에게 직접적으로 보이지 않는 작업을 수행한다. 안드로이드 시스템의 리소스가 부족해지면 강제로 종료될 수 있다. API 26 이상부터는 배터리 소모를 줄이기 위해 앱이 백그라운드에 있을 때 실행에 제약을 두었다. 이 때문에 오랫동안 백그라운드에서 실행되어야 하는 작업은 사용자가 인지할 수 있게 포그라운드 서비스로 전환하여 사용하는 것이 권장된다.
- 바인드 서비스 (Bound Service): 다른 앱 구성 요소와 클라이언트-서버와 같은 형태로 상호작용할 수 있는 서비스이다. bindService()를 통해 이루어지며 연결된 다른 컴포넌트(Activity)가 해제되면 서비스도 함께 소멸한다. 여러 서비스가 한 번에 바인딩 가능하다.
서비스를 직접 코드로 구현해봤다.
포그라운드 서비스의 경우, 서비스 실행 직후 알람이 뜨지 않았다. 이는 안드로이드에서는 UX를 위해 포그라운드 서비스 알림 유예 기간이라는 개념때문이었다.
사용자가 앱을 빠르게 전환하거나 매우 짧은 백그라운드 작업에도 알람이 반응해서 사용자에게 노출될 경우 사용자는 이를 "불편하다"고 느낄 수 있기 때문에 알람이 너무 자주 뜨지 않도록하여 UX를 해치지 않게끔 한 것이다.
바인드 서비스의 경우, 액티비티에 묶어서 사용했을 때 액티비티의 생명주기를 따라가는 것을 확인했다. 특히 액티비티가 파괴될 때 로그를 확인해봤는데 먼저, 액티비티의 OnDestory가 호출되고 이후 바인드 서비스가 UnBound 되었다. 최종적으로 바인드 서비스가 소멸되면서 앱이 종료되는 모습을 볼 수 있었다.
[포그라운드, 백그라운드, 바인드 서비스 테스트 코드]
브로드캐스트 리시버 (Broadcast Receiver)
브로드캐스트 리시버는 안드로이드 시스템이나 다른 앱에서 발생하는 다양한 이벤트를 수신하고 그에 맞는 동작을 수행하는 컴포넌트이다. 예를 들어, 배터리 부족, 이어폰 연결 해제, 네트워크 연결 상태 변경, 메시지 수신, 화면 캡처와 같은 시스템 이벤트를 감지하여 특정 작업을 처리할 수 있다. 하지만, 모든 브로드캐스트가 개방되어 있지는 않기에 권한이나 제약에 의해 사용 불가능한 이벤트도 있으니 참고하자.
리시버 등록 방법
- 정적 등록 (Manifest-delcared): AndroidManifest.xml 파일에 리시버를 등록하는 방식이다. 앱이 실행되고 있지 않아도 브로드캐스트를 수신할 수 있지만 앞서 말한 것처럼 수신할 수 있는 종류에는 한계가 있다.
- 동적 등록 (Context-registered): 코드 내에서 registerReceiver 메서드를 호출하여 리시버를 등록하는 방법이다. 앱이 실행 중일 때만 브로드캐스트를 수신할 수 있고 unregisterReceiver 메서드로 등록을 해제해야만 한다.
리시버를 '동적 등록'할 때, 최신 안드로이드에서는 필수 표기 사항이 존재했다.
registerReceiver(broadcastReceiver, filter, Context.RECEIVER_EXPORTED)
리시버를 등록할 때 해당 리시버가 "앱 내부"에서만 사용될지, "앱 외부"에서도 사용될지를 결정해야한다. 'Context.RECEIVER_NOT_EXPORTED'는 내 앱 내부에서만 사용하겠다는 의미이며 위 코드는 외부에서도 쓰겠다는 의미인데 보안을 잘.. 지켜보자..
등록된 리시버를 사용할 때도 해당 옵션에 따라서 구현이 조금씩 달라졌다.
val intent = Intent(MyBroadcastReceiver.CUSTOM_ACTION).apply {
//setPackage(context.packageName)
putExtra("message", "커스텀 브로드캐스트 메시지!")
}
context.sendBroadcast(intent)
위 코드는 앱 외부에서도 쓸 수 있는데 만약 내부에서만 쓰기로 결정했다면 주석 부분을 해제하고 패키지를 명확하게 지정해줘야한다.
콘텐츠 프로바이더 (Content Provider)
콘텐츠 프로바이더는 앱 간의 데이터 공유를 위한 컴포넌트이다.
파일 시스템, SQLite 데이터베이스, 웹이나 앱이 액세스 할 수 있는 다른 모든 영구 저장위치에 저장 가능한 자신의 데이터를 다른 앱에게 제공하고 싶을 때 사용한다. 데이터에 대한 접근을 체계적으로 관리하고 보안을 유지할 수 있으며 다른 앱들은 ContetnResolver 객체를 통해 고유한 URI(Uniform Resource Identifier) 주소로 식별된 데이터에 접근을 요청하고, 콘텐츠 프로바이더는 이 요청을 받아 데이터를 조회, 삽입, 수정, 삭제할 수 있다.
주소록 앱의 연락처 정보나 갤러리 앱의 사진, 동영상 데이터가 다른 앱에 쉽게 전달될 수 있는 것이 대표적인 예시이다.
브로드캐스트 리시버와 콘텐츠 프로바이더까지 학습하자마자 생각났던 건 본인인증을 할 때 문자메시지의 인증번호를 앱이 알아서 척척 입력해 주던 것이 생각났다.
본인인증을 할 때, 문자메시지로 인증번호를 받고 사용자가 이를 입력하는 형태인데 요즘 앱들은 메시지가 오자마자 알아서 인증번호를 입력해 준다. 이는 브로드캐스트 리시버를 통해 메시지가 수신된 것을 확인하고, 콘텐츠 프로바이더를 이용해 메시지의 인증번호를 가져와 입력하는 것이 아닌가 생각했다. 하지만 이렇게 개인 메시지에 접근하는 건 너무나 치명적인 보안 문제를 일으킬 거란 생각에 동작 원리가 궁금해서 조금 찾아봤다.
실제로 과거에는 내가 추측한 방식과 유사한 형태로 구현했지만, 문자 메시지를 읽는 권한을 앱이 가져가게 되면 사용자의 메시지를 마음대로 언제든지 열어볼 수 있게 된다. 개인 대화나 금융 정보, 다른 서비스의 인증번호 등 민감 정보가 유출될 수 있는 매우 심각한 보안 문제를 가지고 있었던 것이다.
그래서 현재에는 Google Play Services를 이용해서 이를 구현한다. 구글은 민감한 권한 없이 필요한 기능만 안전하 게 구현할 수 있도록 'SMS Retriever API'를 만들었다. 이 방식을 통해 민감한 권한 자체를 앱이 직접 부여받을 이유가 사라져 버린 것이다.
더 자세한 동작 원리는 공식 문서를 참고하자.
매니페스트 (Manifest)
매니페스트는 안드로이드 앱의 필수적인 정보를 담고 있는 설정 파일이다. 안드로이드 OS가 앱을 실행하고 이해하는 데 필요한 모든 정보가 담겨 있다. 앱 실행 전 OS는 반드시 이 파일을 먼저 읽어 앱의 구조를 파악하기에 모든 앱은 반드시 이 파일을 가지고 있다.
매니페스트의 주요 역할
- 앱의 패키지 이름 정의: com.example.app과 같이 앱을 시스템 전체에서 고유하게 식별하는 이름을 지정한다.
- 4대 컴포넌트 선언 (Component Declaration): 앱에 포함된 모든 액티비티, 서비스, 브로드캐스트 리시버, 콘텐츠 프로바이더를 <activity>, <service> 같은 XML 태그로 등록한다. 만약 여기서 등록하지 않는다면 OS는 해당 컴포넌트를 인지하지 못해 앱에서 실행시킬 수 없다.
- 앱 권한 요청 (Permissions Declaration): 앱이 사용해야 하는 시스템 권한을 선언한다. 인터넷이라던지 카메라, 내부 데이터 저장소 접근 등을 <uses-permission android::name="android.permission.INTERNET"/> 의 형태로 명시해야 한다. 이 정보를 바탕으로 OS는 앱 설치 또는 실행 중 사용자에게 권한을 요청한다.
- 하드웨어 및 소프트웨어 요구사항 명시: 앱이 정상 작동하기 위해 필요한 최소 API 레벨, 카메라나 블루투스 같은 하드웨어 기능의 필요 여부를 선언해 해당 기능이 없는 기기에는 앱이 설치되지 않도록 제한할 수 있다.
- 인텐트 필터 설정 (Intent Filter): 특정 컴포넌트가 어떤 종류의 인텐트를 처리할 수 있는지 OS에게 알려준다. 가장 중요한 용도는 앱의 시작점(Entry Point)을 지정하는 것이다. 사용자가 홈 화면에서 앱 아이콘을 눌렀을 때 가장 먼저 실행될 액티비티를 지정하는 작업을 할 수 있는데 코드로는 다음과 같다.
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
인텐트 (Intent)
인텐트는 컴포넌트 간에 작업 수행을 위한 정보를 전달하는 '메시지 객체'이다. 앱 내의 액티비티 간 통신이나 다른 앱의 컴포넌트를 호출하는 등 안드로이드 앱의 경우 모두 객체로 구성되어 있기에 상호 간에 작용을 위해서는 이 메시지 객체를 이용해야만 한다. 인텐트는 '무엇을(Action)', '어떤 데이터로(Data)' 실행할지 담아서 전달하는 요청서의 역할을 한다.
인텐트의 종류와 사용법
인텐트는 대상을 명시하는지 여부에 따라 크게 두 가지로 나뉜다.
- 명시적 인텐트 (Explicit Intent)
- 호출할 대상 컴포넌트를 명확하게 지정하여 실행하는 방식이다. 주로 하나의 앱 내부에서 다른 액티비티를 시작하거나 서비스를 실행하는 등 호출할 대상이 확실할 때 사용된다. 보안상 안전하고 직관적이며 아래와 같이 작성한다.
Intent intent = new Intent(this, ProfileActivity.class);
intent.putExtra("userId", "001");
startActivity(intent);
- 암시적 인텐트 (Implicit Intent)
- 호출할 대상을 명확하게 지정하지 않고, 수행할 '행동(Action)'과 '데이터(Data)'만 정의하여 보내는 방식이다. 안드로이드 시스템은 해당 인텐트를 보고, 각 앱의 매니페스트에 등록된 인텐트 필터를 확인하여 이 요청을 처리할 수 있는 가장 적절한 앱의 컴포넌트를 찾아 연결해 준다. 주로 서로 다른 앱 간의 기능을 연동할 때 사용된다.
- 아래 코드는 웹 브라우저로 특정 URL을 여는 코드이다. 웹 페이지를 본다(ACTION_VIEW)는 행동과 URL데이터를 담아서 요청을 보내면 OS는 이 인텐트를 처리할 수 있는 웹 브라우저 앱을 찾아 실행한다.
Uri webpage = Uri.parse("https://ritbul-develop.tistory.com/");
Intent intent = new Intent(Intent.ACTION_VIEW, webpage);
startActivity(intent);
브로드캐스트 리시버, 콘텐츠 프로바이더, 인텐트는 모두 학습한 내용에서 다른 내용을 찾을 수 없었기에 한 번에 테스트 코드를 작성하고 실험해봤다. 자세한 내용은 코드를 참고하길 바란다. [브로드캐스트 리시버, 콘텐츠 프로바이더, 인텐트 테스트 코드]
'공부 > 안드로이드' 카테고리의 다른 글
| 안드로이드 앱이 만들어지는 과정 (0) | 2025.08.21 |
|---|---|
| 안드로이드의 언어 (5) | 2025.08.20 |
| 안드로이드 개발 공부 (0) | 2025.08.19 |