Android 개발 핵심 정리

Android 개발은 Android 플랫폼 자체의 구조를 이해하는 것이 중요하다.

핵심 개념만 연결해보면 Activity, Lifecycle, View, Compose, Intent, Service, ViewModel, Coroutine, Repository, Room, Gradle 등이 하나의 구조 안에서 이어진다.

Android 앱은 단순히 화면을 만드는 프로그램이 아니다. Android OS가 앱의 생명주기와 프로세스를 관리하고, 앱은 그 위에서 UI와 상태, 데이터 처리를 수행한다.

Android 앱의 전체 구조

가장 먼저 앱의 일반적인 데이터 흐름을 잡으면 다음과 같다.

사용자 입력 -> Activity / Compose -> ViewModel -> Repository -> Data Source -> Network / Database

Android 플랫폼과의 관계까지 확장하면 다음과 같다.

Android App -> Android Framework -> Android OS -> Linux Kernel

Android 앱은 Android OS 위에서 실행되고, Android Framework가 Activity, Service, Intent, Permission 등의 플랫폼 기능을 제공한다.


Activity

Activity는 Android 앱의 대표적인 UI 진입점이다.

과거에는 Activity 하나 = 화면 하나라고 설명하는 경우가 많았지만, 이 방식으로 이해하면 부족하다.

Activity는 Android 시스템과 앱을 연결하면서 UI를 담는 컴포넌트에 가깝다.

대표적인 Lifecycle은 다음과 같다.

onCreate() -> onStart() -> onResume() -> [사용 중] -> onPause() -> onStop() -> onDestroy()

예를 들어 다른 화면이나 앱이 위에 올라오면 onPause()가 호출될 수 있고, 화면에서 완전히 벗어나면 onStop()까지 진행될 수 있다.

Activity는 시스템에 의해 다시 생성될 수도 있다. 화면 회전이나 프로세스 재생성 등을 고려해야 하기 때문에 중요한 상태를 Activity에 전부 저장하는 방식은 적절하지 않다.

이 문제를 해결하기 위해 ViewModel, SavedStateHandle, 영구 저장소 등을 함께 사용한다.


View와 Jetpack Compose

전통적인 Android UI는 View 시스템을 기반으로 한다.

Activity -> View / TextView / Button / RecyclerView

XML로 UI를 선언하고 Kotlin 코드에서 View를 제어하는 방식이 대표적이다.

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Hello" />

최근 Android에서는 Jetpack Compose를 사용하는 방식이 일반화되고 있다.

Compose에서는 UI를 Kotlin 코드로 선언한다.

@Composable
fun Greeting(name: String) {
    Text(text = "Hello $name")
}

전통적인 View 시스템은 UI 객체를 직접 조작하는 방식에 가깝고, Compose는 상태에 따라 UI를 선언하는 방식에 가깝다.

State -> Composable -> UI

상태가 변경되면 관련 Composable이 다시 실행되고 UI가 갱신된다.

Flutter를 알고 있다면 Compose의 선언적 UI와 상태 기반 렌더링 방식은 비교적 쉽게 이해할 수 있다.


Activity Lifecycle

Image

Lifecycle은 단순히 메서드 이름을 암기하는 개념이 아니다.

Android 시스템이 Activity를 생성하고, 화면에 표시하고, 백그라운드로 보내고, 제거하는 과정을 이해하기 위한 구조다.

Activity가 실행되면 일반적으로 다음과 같이 진행된다.

Activity 실행 -> onCreate() -> onStart() -> onResume()

다른 앱으로 이동하면 상황에 따라 다음과 같이 진행될 수 있다.

onResume() -> onPause() -> onStop()

다시 돌아오면 다음과 같이 진행된다.

onStop() -> onStart() -> onResume()

따라서 UI 리소스, 이벤트 수집, 비동기 작업 등의 생명주기를 적절하게 관리해야 한다.

현대 Android에서는 Activity에 모든 로직을 넣기보다는 ViewModel, Lifecycle, Coroutine 등을 이용해 책임을 분리하는 것이 일반적이다.


Intent

Android 컴포넌트 간 통신에서 중요한 개념이 Intent다.

다른 Activity를 실행할 때 사용할 수 있다.

val intent = Intent(this, DetailActivity::class.java)
startActivity(intent)

데이터를 전달하는 것도 가능하다.

intent.putExtra("userId", userId)

Intent는 앱 내부 컴포넌트 이동뿐만 아니라 Android 시스템을 통해 다른 앱의 기능을 요청하는 데에도 사용된다.

App -> Intent -> Android System -> 다른 Activity / 앱

예를 들어 브라우저 실행, 공유 기능 호출 등의 작업도 Intent를 통해 처리할 수 있다.

이 때문에 Intent는 Android가 단순한 UI 프레임워크가 아니라 OS와 강하게 연결된 플랫폼이라는 것을 보여주는 대표적인 개념이다.


Android 주요 컴포넌트

Android에는 대표적으로 네 가지 앱 컴포넌트가 있다.

Activity / Service / BroadcastReceiver / ContentProvider

Activity

사용자와 상호작용하는 UI 진입점이다.

Service

UI가 없는 상태에서도 특정 작업을 수행할 수 있는 컴포넌트다.

다만 현대 Android는 백그라운드 실행에 여러 제한을 두고 있다. 장시간 사용자에게 표시되는 작업은 Foreground Service가 필요할 수 있고, 예약된 백그라운드 작업은 WorkManager가 적합한 경우가 많다.

BroadcastReceiver

Android 시스템이나 다른 앱에서 발생하는 Broadcast를 수신한다.

특정 시스템 이벤트에 반응해야 하는 경우 사용할 수 있다.

ContentProvider

앱 간 데이터 공유를 위한 표준 인터페이스다.

일반적인 앱에서 직접 구현할 일은 많지 않지만 Android 플랫폼 구조를 이해하기 위해 알아둘 필요가 있다.


ViewModel

Activity나 Fragment에 화면 상태를 직접 저장하면 Lifecycle 문제를 처리하기 어려워진다.

그래서 ViewModel을 사용한다.

Activity / Compose -> ViewModel -> Repository -> API / Database

ViewModel은 UI에 필요한 상태와 UI 로직을 관리하는 계층이다.

Image
class UserViewModel(
    private val repository: UserRepository
) : ViewModel() {

    fun loadUser() {
        viewModelScope.launch {
            val user = repository.getUser()
        }
    }
}

Configuration Change로 Activity가 다시 생성되는 경우에도 ViewModel은 일반적으로 유지된다.

다만 ViewModel이 영구 저장소는 아니다.

프로세스가 완전히 종료되었다가 다시 생성되는 상황까지 고려한다면 SavedStateHandle, Room, DataStore 등의 별도 저장 수단이 필요하다.


Coroutine

Kotlin 기반 Android 개발에서 비동기 처리는 Coroutine을 많이 사용한다.

viewModelScope.launch {
    val user = repository.getUser()
}

Coroutine을 사용하면 비동기 작업을 순차적인 코드에 가까운 형태로 작성할 수 있다.

Android에서는 특히 Lifecycle과 연결된 Coroutine Scope가 중요하다.

ViewModel -> viewModelScope -> Coroutine -> Network / Database

ViewModel이 제거되면 해당 viewModelScope에서 실행 중인 Coroutine도 함께 취소된다.

따라서 화면의 생명주기와 비동기 작업의 생명주기를 자연스럽게 연결할 수 있다.


Repository

ViewModel에서 API나 Database를 직접 다루기 시작하면 UI 계층에 데이터 접근 로직이 섞인다.

그래서 Repository를 중간 계층으로 둔다.

UI -> ViewModel -> Repository -> Network / Database

ViewModel은 데이터가 실제로 어디에서 오는지 알 필요가 없다.

class UserViewModel(
    private val repository: UserRepository
) : ViewModel() {

    fun loadUser() {
        viewModelScope.launch {
            val user = repository.getUser()
        }
    }
}

Repository가 API와 로컬 Database를 조합하거나 적절한 데이터 소스를 선택한다.

ViewModel -> Repository -> API / Database

이렇게 하면 UI 계층과 데이터 계층의 결합을 줄일 수 있다.


Room

Android에서 로컬 Database를 사용할 때 대표적으로 사용하는 것이 Room이다.

Room은 SQLite를 직접 다루는 대신 Kotlin 코드와 SQL을 연결해주는 추상화 계층이다.

기본적인 구조는 다음과 같다.

Entity -> DAO -> Room Database -> SQLite

Entity를 정의한다.

@Entity
data class User(
    @PrimaryKey val id: Int,
    val name: String
)

DAO에서 데이터 접근을 정의한다.

@Dao
interface UserDao {

    @Query("SELECT * FROM User")
    suspend fun getUsers(): List<User>
}

실제 애플리케이션에서는 Repository가 Room과 Network API를 함께 사용하는 경우가 많다.

ViewModel -> Repository -> API / Room

상태 관리

Compose에서는 상태 관리가 핵심이다.

간단한 로컬 상태라면 Composable 내부에서 관리할 수 있다.

var count by remember { mutableStateOf(0) }

하지만 화면을 넘어 관리해야 하는 상태라면 ViewModel로 올리는 것이 일반적이다.

Composable -> ViewModel -> UiState

예를 들어 UI 상태를 하나의 객체로 관리할 수 있다.

data class UserUiState(
    val isLoading: Boolean = false,
    val user: User? = null,
    val error: String? = null
)

그러면 UI는 UiState를 기반으로 화면을 결정한다.

UiState -> Loading / Success / Error

이 방식은 UI와 상태를 분리하고 화면의 현재 상태를 명확하게 표현할 수 있다는 장점이 있다.


Navigation

Android에서는 Navigation 컴포넌트를 이용해 화면 이동을 관리할 수 있다.

Compose에서는 Navigation Compose를 사용해 route를 정의하는 방식이 일반적이다.

Navigation -> Home / Profile / Settings / Detail

각 Composable에서 화면 이동을 무작정 처리하기보다는 Navigation 계층에서 경로와 이동 구조를 관리하면 화면 구조를 파악하기 쉽다.


Android Manifest

AndroidManifest.xml은 Android 앱의 구성 정보를 선언하는 파일이다.

앱의 컴포넌트, 권한, 앱 설정 등을 Android OS에 알려준다.

인터넷 권한을 선언할 수 있다.

<uses-permission
    android:name="android.permission.INTERNET" />

Activity도 선언한다.

<activity
    android:name=".MainActivity" />

구조적으로 보면 다음과 같다.

Android App -> AndroidManifest.xml -> Android OS

Manifest는 Android 시스템이 앱을 어떻게 인식하고 실행할지 결정하는 중요한 설정 파일이다.


Gradle

Android 개발에서 Gradle은 빌드 시스템이다.

소스 코드 컴파일뿐만 아니라 의존성 관리, 리소스 처리, 빌드 변형 관리, APK/AAB 생성 등의 작업을 담당한다.

Source Code -> Compile -> Dependency Resolution -> Resource Processing -> APK / AAB

의존성도 Gradle을 통해 관리한다.

dependencies {
    implementation("androidx.room:room-runtime:...")
}

Android에서 Gradle 설정이 복잡해지는 이유도 단순 컴파일만 수행하는 것이 아니라 Android SDK, Plugin, Dependency, Build Variant 등의 여러 요소가 함께 연결되기 때문이다.


APK와 AAB

Android 애플리케이션의 배포 형식도 구분할 필요가 있다.

APK는 Android 기기에 설치할 수 있는 패키지 형식이다.

AAB(Android App Bundle)는 앱 배포를 위한 번들 형식이며, Google Play가 사용자 기기 환경에 맞는 APK를 생성하는 데 사용한다.

Android Project -> Gradle Build -> AAB -> Google Play -> Device-specific APK

개발 과정에서는 APK를 직접 설치해 테스트하는 경우가 많고, Google Play 배포에서는 일반적으로 AAB를 사용한다.


Android 개발의 전체 구조

앞에서 설명한 내용을 하나로 합치면 Android 앱의 구조는 다음과 같이 볼 수 있다.

Android OS -> Activity / Lifecycle / Intent / Permission -> UI -> ViewModel -> Repository -> Network / Room

UI 계층에서는 전통적인 View 시스템이나 Compose를 사용할 수 있다.

UI -> View / Compose

비동기 작업은 Coroutine이 연결한다.

ViewModel -> Coroutine -> Repository -> Network / Database

로컬 데이터는 Room을 사용할 수 있다.

Repository -> Room -> SQLite

프로젝트 자체는 Gradle이 빌드한다.

Source Code -> Gradle -> APK / AAB

결국 Android를 공부할 때 핵심은 API를 하나씩 외우는 것이 아니다.

먼저 Android OS가 앱을 어떻게 실행하고 관리하는지 이해해야 한다.

그 위에서 Activity와 Lifecycle이 어떻게 UI의 생명주기를 관리하는지, ViewModel이 상태를 어떻게 분리하는지, Repository가 데이터 접근을 어떻게 추상화하는지를 잡으면 된다.

전체 구조를 압축하면 다음과 같다.

Android OS -> Activity / Lifecycle -> UI -> ViewModel -> Repository -> Network / Database

그리고 각 계층의 역할을 확장하면 다음과 같다.

Android OS -> Activity / Compose -> ViewModel -> Repository -> API / Room

이 구조가 머릿속에 있으면 새로운 Android API를 만났을 때도 이것이 UI에 속하는지, 상태 관리에 속하는지, 데이터 계층에 속하는지, OS와 통신하는 기능인지 판단할 수 있다.