
안드로이드 앱은 어떻게 돌아갈까를 처음 공부할 때 가장 어려운 것은 용어가 아니라 구조입니다. Activity, 생명주기, ViewModel, 코루틴을 따로따로 아는 것과 그것들이 하나의 앱 안에서 어떻게 연결되는지 이해하는 것은 전혀 다른 문제입니다. 이 글은 그 큰 그림을 잡기 위한 시리즈 1편입니다.
안드로이드 앱은 기능 모음이 아니라 구조로 봐야 한다
초보자는 보통 버튼 클릭, 화면 전환, 데이터 불러오기 같은 기능부터 봅니다. 물론 그것도 필요합니다. 하지만 안드로이드에서는 기능보다 먼저 앱이 어떤 구조 안에서 돌아가는지를 이해해야 합니다. 같은 버튼 클릭이라도 화면이 언제 만들어졌는지, 지금 화면이 어느 상태인지, 이 작업이 잠깐 멈췄다가 다시 이어질 수 있는지에 따라 코드를 두는 위치가 달라지기 때문입니다.
즉, 안드로이드는 단순히 화면을 그리는 도구가 아니라 화면, 상태, 생명주기, 비동기 작업이 동시에 얽혀 있는 환경입니다.
안드로이드 앱 구조를 볼 때 가장 먼저 잡아야 하는 축
- 화면 진입점
- 생명주기
- 화면 상태
- 오래 걸리는 작업
이 네 가지가 함께 보이면 앱 구조가 갑자기 단순해집니다. 반대로 이걸 따로따로 배우면 각 개념은 아는데 실제 코드에서는 계속 섞여 보입니다. 예를 들어 네트워크 요청 하나만 생각해도 이 작업은 어디서 시작해야 할까, 화면이 사라지면 어떻게 해야 할까, 결과는 어디에 보관해야 할까 같은 질문이 바로 생깁니다.
Activity는 왜 첫 번째 기준점이 될까
안드로이드 앱 구조를 설명할 때 가장 먼저 나오는 핵심 단위는 Activity입니다. 아주 단순하게 말하면 Activity는 사용자가 앱과 만나는 화면 단위의 시작점입니다. 실제 앱에서는 Fragment, Navigation, Compose, ViewModel이 함께 등장하지만 구조를 처음 이해할 때는 Activity를 중심축으로 놓는 편이 가장 덜 헷갈립니다.
이유는 명확합니다. 사용자가 앱을 열고, 화면이 나타나고, 입력이 들어오고, 다른 화면으로 이동하고, 앱이 잠깐 가려졌다가 다시 돌아오는 흐름을 가장 먼저 읽게 해주는 단위가 Activity이기 때문입니다. Activity를 그냥 XML 붙이는 곳 정도로 이해하면 그 순간부터 구조가 무너지기 시작합니다.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}겉으로 보면 아주 단순한 코드지만, 실제로는 이 Activity가 언제 처음 만들어졌는지, 다시 만들어진 것인지, 어떤 상태를 복구해야 하는지, 화면과 데이터를 어디서 연결해야 하는지의 출발점이 됩니다.
안드로이드 앱은 화면이 계속 살아 있는 환경이 아니다
안드로이드 화면은 다른 앱에 가려질 수 있고, 회전으로 다시 만들어질 수 있고, 시스템 상황에 따라 사라질 수도 있습니다. 이 말은 곧 지금 보이는 화면이 영원히 유지된다고 가정하면 거의 반드시 문제가 생긴다는 뜻입니다.
그래서 안드로이드에서는 화면을 그리는 코드만 보는 것이 아니라 그 화면이 언제 만들어지고, 언제 보이고, 언제 멈추고, 언제 사라질 수 있는지를 함께 봐야 합니다. 생명주기는 콜백 암기 과목이 아니라 코드를 어디에 둬야 하는지 판단하는 기준입니다.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d("MainActivity", "onCreate")
}
override fun onStart() {
super.onStart()
Log.d("MainActivity", "onStart")
}
override fun onResume() {
super.onResume()
Log.d("MainActivity", "onResume")
}
override fun onPause() {
super.onPause()
Log.d("MainActivity", "onPause")
}
override fun onStop() {
super.onStop()
Log.d("MainActivity", "onStop")
}
override fun onDestroy() {
super.onDestroy()
Log.d("MainActivity", "onDestroy")
}
}이 코드를 실행하고 앱을 열고, 홈으로 나가고, 다시 돌아오고, 회전해보면 화면이 계속 같은 상태로 유지된다고 생각하면 안 되는 이유가 훨씬 쉽게 보입니다.

생명주기를 모르면 왜 설계가 꼬일까
생명주기를 잘 모르면 초보자는 거의 비슷한 실수를 합니다. 화면이 열릴 때마다 같은 작업을 중복 실행하고, 이미 사라진 화면에 결과를 반영하려 하고, 회전 후 상태가 초기화되고, UI 코드 안에 너무 많은 책임을 밀어 넣습니다. 이건 문법 실수라기보다 구조 실수에 가깝습니다.
예를 들어 데이터 로드를 항상 onCreate()에만 넣는다고 해결되지 않습니다. 어떤 데이터는 화면이 다시 그려져도 유지되어야 하고, 어떤 작업은 화면이 보일 때만 살아 있어야 하고, 어떤 상태는 별도 저장 위치가 필요합니다. 기존에 발행한 안드로이드 액티비티 생명주기 기초 정리 글도 여기서 바로 이어집니다.
화면과 상태를 분리해서 봐야 하는 이유
안드로이드 구조에서 초보자가 가장 늦게 깨닫는 것 중 하나가 화면과 상태는 같지 않다는 점입니다. 화면은 그려졌다가 사라질 수 있지만 상태는 화면이 다시 만들어져도 유지되어야 할 때가 많습니다.
예를 들어 로딩 중인지, 어떤 목록이 내려왔는지, 현재 선택된 항목이 무엇인지, 에러가 발생했는지 같은 정보는 그냥 화면 안에만 두면 금방 꼬입니다. 그래서 안드로이드 구조를 볼 때는 늘 지금 보이는 UI는 무엇인가, 이 UI를 만들기 위한 상태는 어디에 있어야 하는가를 나눠서 생각해야 합니다.
class MainViewModel : ViewModel() {
private val _uiState = MutableStateFlow<MainUiState>(MainUiState.Loading)
val uiState: StateFlow<MainUiState> = _uiState
fun onDataLoaded(items: List<String>) {
_uiState.value = MainUiState.Success(items)
}
fun onError(message: String) {
_uiState.value = MainUiState.Error(message)
}
}이 코드는 단순하지만 중요한 관점을 보여줍니다. 화면을 그리는 코드와 화면을 만들기 위한 상태를 분리하기 시작하면 구조가 훨씬 덜 흔들립니다.
비동기 작업은 기술 문법이 아니라 구조 문제다
안드로이드에서는 네트워크 요청, DB 조회, 파일 처리 같은 작업이 거의 항상 들어가고, 이 작업들이 화면과 따로 놀기 시작하면 구조가 곧바로 흔들립니다. 이미 사라진 화면에 응답을 그리려고 하거나, 회전 후 이전 작업 결과가 어색하게 들어오거나, 화면 책임과 작업 책임이 한곳에 몰릴 수 있습니다.
즉, 비동기 작업은 단순히 백그라운드에서 돌리는 기술이 아니라 어디서 시작하고 어디서 멈추고 어디에 결과를 반영할지까지 포함한 구조 문제입니다. 이 감각이 없으면 코루틴 문법을 알아도 앱 구조는 계속 어색하게 남습니다. 기존 발행 글인 안드로이드 코루틴 기초 정리와 연결해서 보면 이 부분이 더 분명해집니다.
class MainViewModel(
private val repository: ItemRepository
) : ViewModel() {
private val _uiState = MutableStateFlow<MainUiState>(MainUiState.Loading)
val uiState: StateFlow<MainUiState> = _uiState
fun loadItems() {
viewModelScope.launch {
_uiState.value = MainUiState.Loading
runCatching {
repository.loadItems()
}.onSuccess { items ->
_uiState.value = MainUiState.Success(items)
}.onFailure { throwable ->
_uiState.value = MainUiState.Error(
throwable.message ?: "알 수 없는 오류가 발생했습니다."
)
}
}
}
}여기서 중요한 것은 코루틴 문법 자체보다 이 작업이 화면 코드 안이 아니라 화면 상태와 연결된 구조 안에서 움직이고 있다는 점입니다.
초보자가 앱 구조를 볼 때 자주 놓치는 질문
- 지금 이 화면의 진입점은 무엇인가
- 이 화면은 언제 다시 만들어질 수 있는가
- 이 데이터는 화면 안에 두면 안 되는가
- 이 작업은 화면이 사라져도 계속 살아야 하는가
- 지금 복잡한 이유가 기능 때문인지 책임이 섞였기 때문인지
이 질문으로 구조를 보기 시작하면 안드로이드 앱이 API들의 집합이 아니라 하나의 설계 문제처럼 보이기 시작합니다.
안드로이드 앱 구조를 한 문장으로 정리하면
안드로이드 앱은 화면 하나를 예쁘게 만드는 문제가 아니라 계속 변하는 화면 환경에서 상태와 작업을 어떻게 안전하게 연결할 것인가를 다루는 구조 문제입니다. 이 관점이 생기면 왜 Activity가 중요한지, 왜 생명주기를 알아야 하는지, 왜 상태와 ViewModel 이야기가 나오는지, 왜 코루틴도 구조와 연결해서 봐야 하는지가 같이 보이기 시작합니다.
이번 편의 핵심 정리
- 안드로이드 앱은 기능 목록이 아니라 구조로 봐야 한다
- Activity는 그 구조를 이해하는 첫 기준점이다
- 화면은 계속 살아 있지 않기 때문에 생명주기를 함께 봐야 한다
- 화면과 상태는 다를 수 있다
- 비동기 작업도 구조와 분리해서 볼 수 없다
이 다섯 가지가 잡히면 다음 편인 Activity와 Fragment는 왜 나뉘어 있을까도 훨씬 쉽게 읽힙니다.
공식 참고 자료는 Android app fundamentals, Introduction to activities, Activity lifecycle입니다.