Primary/Android

[Android Fundamentals] Overview (1)

해스끼 2024. 7. 4. 20:58

Android Fundamentals 시리즈

안드로이드 개발자로서 Compose, ViewModel 등 여러 기술을 공부했지만, 구현에 집중한 나머지 기초 개념을 탄탄히 하지 못했다는 아쉬움이 있었다. 4대 컴포넌트가 왜 4대 컴포넌트인지 물어보면 대답을 못 할 정도. ㅠㅠ

 

더 이상 미루면 안드로이드 개발자로서 생존이 어려울 것 같아 공부를 시작한다. Android Fundamentals 시리즈에서는 공식 문서와 기술 도서 등 여러 자료를 참고하여 공부한 내용을 정리한다. 4대 컴포넌트부터 시작하여 BFS 방식으로 훑되, 중요한 내용은 DFS로 deep dive할 예정이다.

 

급할수록 돌아가라.

Android Fundamentals 시리즈를 언제 끝낼 지는 모르겠지만, 시리즈가 끝났을 땐 더 나은 개발자가 되어 있을 것이라 믿는다. 천천히, 그러나 조금씩 발전할 수 있길 바란다.

 

화이팅!


Overview

안드로이드 앱은 Kotlin, Java, C++ 언어로 작성할 수 있다. 작성한 코드와 리소스, 데이터 파일을 Android SDK가 APK 또는 AAB로 컴파일한다.

 

흔히 APK로 불리는 Android package는 런타임에 필요한 앱의 코드/리소스 등을 담고 있으며, 안드로이드 기기에 앱을 설치할 때 사용된다.

 

Android App Bundle(AAB)는 런타임에 필요하지 않은 메타데이터를 포함한 앱의 콘텐츠를 담고 있다. AAB는 앱을 출시할 때 사용되며, 앱을 설치할 때 직접 사용되지는 않지만 AAB의 정보를 이용하여 APK를 만들 수 있다. Google Play를 통해 앱을 출시한다면, Google Play 서버가 필요한 코드와 리소스만을 취합하여 기기별로 최적화된 APK 파일을 생성한다. 

 

안드로이드 앱은 자신만의 security sandbox에서 실행된다. 안드로이드 역시 리눅스 기반 운영체제이기 때문에 리눅스와 시스템이 유사하다.

  • 안드로이드 OS는 멀티 유저 리눅스 시스템이다. 여기서 ``유저``란 기기 사용자가 아닌 앱을 의미한다.
  • 기본적으로 각 앱에는 unique한 리눅스 user ID가 할당된다. 이 ID는 시스템만 확인할 수 있으며, 앱은 ID의 존재를 알지 못한다. 시스템은 각 앱에 할당된 user ID만이 그 앱의 파일에 접근할 수 있도록 접근 권한을 설정한다. 따라서 각 앱은 기본적으로 자신의 파일에만 접근할 수 있으며, 다른 앱의 파일에는 접근할 수 없다.
  • 각 프로세스는 자신만의 virtual machine에서 실행되므로, 다른 앱과 독립적으로 실행된다.
  • 기본적으로 모든 앱은 개별 리눅스 프로세스에서 실행된다. 안드로이드 시스템은 앱의 컴포넌트가 실행되어야 할 때 프로세스를 할당하며, 프로세스가 더 이상 필요없거나 메모리가 부족한 경우에 프로세스를 회수한다.

안드로이드 시스템은 ``권한 최소화`` 원칙을 따른다. 즉 앱은 작업에 필요한 컴포넌트에만 접근할 수 있으며, 그 이상으로는 접근할 수 없다. 따라서 권한이 주어지지 않은 컴포넌트에는 접근할 수 없는 안전한 환경을 만들 수 있다.

 

하지만 다른 앱의 데이터에 접근하거나 시스템 기능을 사용할 수도 있다. 예를 들어 다음과 같은 방법이 있다.

  • 여러 앱이 동일한 리눅스 user ID를 공유할 수 있다. 아이디를 공유하는 모든 앱이 서로 파일을 공유하는 것이다.
    • 리소스 절약을 위해 ID가 같은 앱은 같은 리눅스 프로세스와 VM에서 실행된다. 
    • ID가 같은 앱은 동일한 인증서로 서명되어야 한다.
  • 필요에 따라 GPS, 카메라, 블루투스 연결 등의 권한을 얻을 수 있다. 사용자가 명시적으로 권한을 허용해야 한다. (더 보기: Permissions on Android)

이어서 앱을 구성하는 핵심 컴포넌트, 컴포넌트와 기능을 정의하는 manifest 파일, 코드와는 분리되어 있지만 기기별 최적화를 돕는 리소스에 대해 알아보자.

앱 컴포넌트

앱 컴포넌트는 안드로이드 앱을 구성하는 핵심 요소이다. 각 컴포넌트는 시스템이나 사용자가 앱에 진입할 수 있는 진입점 역할을 한다. 국내에서는 흔히 4대 컴포넌트라고도 불린다.

  • Activity
  • Service
  • Broadcast receiver
  • Content provider

각 컴포넌트는 서로 다른 목적과 서로 다른 lifecycle을 갖는다.

Activity

Activity는 사용자가 앱과 상호작용하는 진입점 역할을 하며, UI를 보여주는 화면이라고 생각하면 된다. 예를 들어 쿠링 앱에는 공지 리스트 activity, 공지 웹뷰 activity, 설정 activity 등이 존재한다. 유기적인 사용자 경험을 만들기 위해 여러 activity를 사용하긴 하지만, 기본적으로 activity는 서로 독립되어 있다. 

 

쿠링 앱이 허용한다면 다른 앱에서 쿠링의 activity를 실행할 수도 있다. 예를 들어 공지 링크를 클릭했을 때 쿠링의 공지 웹뷰 activity가 실행될 수 있다. (이건 언제 개발하냐...)

 

다음과 같은 시스템과 앱 사이의 중요한 상호작용이 activity를 통해 이루어진다.

  • 사용자가 현재 관심있는 것(현재 화면에 보이는 것)을 추적하여, 시스템이 해당 activity를 보여주는 프로세스를 계속 실행할 수 있도록 돕는다.
  • 이전에 사용된 프로세스 중 사용자가 다시 탐색할 가능성이 있는 activity의 프로세스를 기억하고, 그 프로세스들이 종료되지 않도록 우선순위를 부여한다.
  • 앱의 프로세스가 종료되는 과정을 커스텀하여, 사용자가 activity로 돌아올 때 이전 상태가 복구될 수 있도록 돕는다. (스크롤 위치 등)
  • 여러 앱 간 user flow를 구현하는 방법을 제공하며, 시스템이 user flow를 감독(coordiante)할 수 있다. 대표적으로 데이터를 다른 앱으로 공유하는 경우가 있다. Activity  |  Android Developers
 

Activity  |  Android Developers

 

developer.android.com

Service

Service는 앱이 백그라운드에서 작동할 수 있도록 하는 general한 진입점 역할을 한다. 오랫동안 수행되는 작업이나 원격 작업을 백그라운드에서 수행하는 컴포넌트이다. Service는 UI를 보여주지 않는다.

 

예를 들어 음악 재생 앱이 백그라운드에서 계속 음악을 재생하려면 service를 사용해야 한다. 또는 사용자의 작업을 방해하지 않으면서 서버의 데이터를 가져올 수도 있다. Activity 같은 다른 컴포넌트에서 서비스를 실행하고 방임하거나(run), 서비스를 자신의 감독 하에 bind할 수 있다.

 

서비스에는 started service와 bound service가 있다.

 

Started service는 작업이 실행되는 한 종료되지 않는다. 예를 들어 백그라운드에서 데이터를 동기화하거나, 앱이 종료된 후에도 백그라운드에서 음악을 계속 재생할 수 있다. 그런데 방금 예시로 든 두 개의 작업은 목적이 미묘하게 다르고, 시스템이 취급하는 방식도 다르다.

  • 음악을 재생하는 작업은 사용자도 인식할 수 있다. 따라서 음악 재생 서비스는 foreground service로 실행되며, 서비스가 실행되고 있다는 알림이 표시된다. (재생 UI 등)
  • 일반적인 background service는 사용자가 인식하기 어렵기 때문에, 시스템에서 좀 더 자유롭게 처리한다. 중간에 종료될 수도 있고, 메모리가 부족하면 일단 종료됐다가 나중에 restart될 수도 있다.

Bound service는 다른 앱이나 시스템에서 서비스를 호출할 때 실행된다. 즉 bound service는 다른 프로세스에게 일종의 API를 제공하며, 시스템이 두 프로세스 간의 의존성을 인식할 수 있다.

 

예를 들어 프로세스 A가 프로세스 B의 서비스에 bound되었다면, 시스템은 A를 실행하기 위해 프로세스 B와 B의 서비스를 계속 실행해야 한다고 인식하게 된다. 만약 프로세스 A가 사용자와 상호작용하고 있다면, 시스템은 프로세스 B도 사용자와 상호작용하고 있는 것처럼 간주한다.

 

즉 started service는 자기가 실행한 자기 서비스이고, bound service는 다른 프로세스에서 실행할 수 있는 서비스라고 보면 된다.

 

서비스는 특유의 유연함 덕분에 여러 기능을 구현하는 데 사용된다. Wallpaper, 알림 리스너, 접근성 서비스, 그 외의 여러 핵심 시스템 기능이 모두 started/bound service로 구현되어 있다.

Android 5.0(API 21) 이상 버전을 타겟하는 앱에서는 ``JobScheduler``를 사용하는 것이 좋다. ``JobScheduler``는 작업 스케줄링을 최적화하고 Doze API를 사용하여 배터리 소모를 최소화한다. (링크 참고)

Broadcast receiver

Broadcast receiver를 사용하면 시스템이 일반적인 user flow 외부에서 이벤트를 전달할 수 있고, 전달된 이벤트를 앱이 처리할 수 있다. 시스템은 실행되고 있지 않은 앱에도 broadcast를 전달할 수 있다.

 

예를 들어 일정을 알려주기 위해 broadcast receiver로 alarm을 보낸다고 가정하자. Broadcast receiver로 보내는 메시지는 앱이 실행되고 있지 않을 때에도 받을 수 있다. 따라서 alarm이 올 때까지 앱이 실행 상태로 기다리지 않아도 된다.

 

대부분의 broadcast는 시스템이 보낸다. 시스템은 화면이 꺼졌다던가, 배터리가 부족하다던가, 화면이 캡쳐됐다는 등 여러 broadcast를 보낸다. 물론 앱에서도 broadcast를 보낼 수 있다. 파일 다운로드가 완료됐음을 다른 앱에 broadcast로 공유할 수도 있다.

 

Broadcast receiver 자체는 UI를 보여주지 않지만, broadcast 이벤트가 발생했을 때 상단 바에 알림을 보여줄 수 있다. 하지만 broadcast receiver는 일반적으로 통로(gateway) 역할만 수행하며, 최소한의 작업만 수행하도록 설계되어 있다.

 

예를 들어, broadcast를 수신했을 때 broadcast receiver가 직접 어떤 작업을 수행하는 대신 ``JobScheduler``를 통해 ``JobService``할 수 있다. 실질적인 작업은 ``JobService``에 맡기는 것.

 

Broadcast receiver는 보통 서로 다른 앱 간의 상호작용을 처리하기 때문에, 외부로부터 공격받는 등 보안 이슈가 발생할 수도 있다.

Content provider

Content provider는 파일 시스템에 저장된 데이터, SQLite DB, 그 밖에 접근할 수 있는 모든 저장소를 관리한다. Content provider를 통해 다른 앱의 데이터에 접근하거나 수정할 수 있다. 물론 content provider가 허락해야 한다.

 

예를 들어 안드로이드 시스템은 연락처 정보를 관리하는 content provider를 제공하고 있다. 연락처 권한을 받은 앱에서는 ``ContactsContract.Data`` 등의 쿼리를 통해 연락처를 read/write할 수 있다.

 

이런 특성 때문에 content provider를 추상화된 DB처럼 생각할 수도 있는데, content provider와 DB는 전혀 다른 목적으로 설계되었다.

 

Content provider는 앱이 named data를 publish하는 진입점의 역할을 한다. 각 데이터는 URI로 identify된다. 따라서 각 앱마다 서로 다른 형태로 URI를 정의할 수 있고, 정의한 URI를 데이터와 mapping하는 과정 역시 앱마다 다를 수 있다.

  • 따라서 URI를 할당하기 위해 앱이 실행될 필요는 없고, 앱이 종료된 후에도 URI는 계속 존재할 수 있다. URI를 통해 데이터를 얻을 때에만 앱을 실행하면 된다.
  • URI를 통해 보안 모델을 구현할 수도 있다. 앱 A가 클립보드에 이미지를 올린 후 URI를 부여했지만, 그 이미지를 다른 앱에서 접근할 수 없도록 하기 위해 content provider를 잠갔다고 해 보자. 다른 앱 B에서 해당 URI에 접근하려 한다면, 시스템은 앱 B에게 ``URI 접근 권한``을 임시로 부여한다. 임시 권한을 부여받은 B는 URI가 의미하는 이미지에는 접근할 수 있지만, 그 외의 어떠한 데이터에도 접근할 수 없다.

종합

안드로이드의 독특한 점은, 다른 앱의 컴포넌트를 자유롭게 실행할 수 있다는 점이다. 예를 들어 앱에서 사진을 촬영하기 위해 촬영 기능을 직접 구현할 수도 있지만, 다른 카메라 앱을 호출하여 사진을 얻을 수도 있다. 그냥 카메라 앱의 activity를 호출한 후 이미지를 받기만 하면 된다. 사용자에게는 카메라 기능이 내장되어 있는 것처럼 보일 것이다.

 

시스템에서 컴포넌트를 시작할 때에는, (앱이 실행되고 있지 않았다면) 앱을 실행하고, 해당 컴포넌트 클래스를 인스턴스화한다. 내 앱에서 카메라 앱의 촬영 activity를 시작한다면, 카메라 앱을 실행하는 별도의 프로세스가 시작된다. 내 앱의 프로세스에서 실행되지 않는 이유는, 서로 다른 앱은 서로 다른 프로세스에서 실행되기 때문이다.

 

지금까지의 얘기를 종합하면 안드로이드 앱에 ``main()``이 없는 이유를 알 수 있다. 안드로이드 앱은 컴포넌트를 통해 여러 경로로 시작될 수 있기 때문에, ``main`` 같은 유일한 진입점이 존재하지 않는다.

 

앱에서 다른 앱의 컴포넌트에 직접 접근할 수는 없다. 파일 권한에 의해 엄격하게 분리되기 때문. 대신 시스템에게 ``Intent``를 전달하면 시스템이 해당 컴포넌트를 실행해준다.

컴포넌트 실행하기

안드로이드의 시스템 메시지는 ``Intent``를 통해 비동기적으로 처리된다. ``Intent``를 사용하면 activity, service, broadcast receiver를 실행할 수 있다. 내 앱의 컴포넌트와 다른 앱의 컴포넌트 모두 ``Intent``로 호출할 수 있다.

 

``Intent``에는 컴포넌트의 이름을 명시하는 explicit intent와, 컴포넌트의 타입만을 지정하는 implicit intent가 있다.

 

Activity와 service를 실행하려면, 우선 view나 send 같은 작업의 종류를 지정하고, 필요하다면 작업에 사용할 데이터의 URI를 명시할 수도 있다. 

 

Activity에서 수행한 작업의 결과를 알고 싶다면, activity가 반환하는 결과 ``Intent``를 받으면 된다. 예를 들어 연락처 선택 activity를 실행했다면, activity에서 선택한 연락처를 ``Intent``에 URI로 담아 반환한다.

 

Broadcast receiver를 실행하려면 ``Intent``에 broadcast 종류를 정의해야 한다. 예를 들어 배터리가 부족하다는 broadcast에는 해당 broadcast를 의미하는 문자열 데이터만 포함되어 있다.

 

Content provider는 ``Intent``가 아닌 ``ContentResolver``를 통해 실행할 수 있다. 다른 컴포넌트에서 content provider와 직접 상호작용할 수는 없고, content resolver가 모든 content 작업을 중개한다. 다른 앱의 데이터에 직접 접근할 수 없도록 제한하기 위해 중개자를 두는 것이다.

 

각 컴포넌트를 실행하는 구체적인 방법은 다음과 같다.

  • Activity: ``startActivity()`` 또는 ``startActivityForResult()``를 통해 ``Intent``를 전달하여 실행한다.
  • Service: Android 5.0(API 21) 이상에서는 ``JobScheduler``를 통해 서비스를 실행한다. Bound service는 ``bindService()``에 ``Intent``를 전달하여 실행한다.
  • Broadcast: ``sendBroadcast()``나 ``sendOrderedBroadcast()``를 통해 메시지를 보낼 수 있다.
  • Content provider: ``ContenrResolver.query()``를 실행하여 content를 얻을 수 있다.

참고자료

Application fundamentals  |  Android Developers