본문 바로가기
💻 Programming/Android Developer AtoZ

[Android] App Components (2) - Activity 완벽 가이드

by 기무정 2025. 3. 5.
728x90

 

 

 

Android의 4대 컴포넌트 중 하나인 Activity는 앱에서 UI를 담당하는 핵심 요소입니다.

앱의 화면을 구성하고, 사용자와 상호작용하며 다른 액티비티나 앱과 데이터를 주고받는 역할을 수행합니다.

 

이 가이드에서는 Activity의 역할, 생명주기, 태스트 관리, 데이터 전달 방식까지 자세히 설명하겠습니다.

 


 

Activity

사용자와 상호작용을 담당하는 인터페이스로 사용자에게 드러나는 화면을 의미합니다. 때문에 반드시 하나 이상의 Activity를 포함하고 있어야 합니다. 앱을 실행할 때는 앱을 전체적으로 호출하는 것이 아니라 앱의 Activity를 호출합니다.

Activity는 생명주기(Lifecycle) 관련 메서드를 재정의하여 원하는 기능을 구현할 수 있습니다.

 

정리하자면 다음과 같습니다.

  • Activity는 사용자가 Application과 상호작용하며 실제로 사용자에게 보이는 화면을 의미
  • Activity는 Intent를 통해 다른 Application의 Activity를 호출
  • 2개 이상의 Activity를 동시에 보여줄 수 없음
  • 1개 이상의 View 또는 ViewGroup을 포함
  • Application에는 반드시 하나 이상의 Activity를 포함
  • Activity 내에 Fragment를 추가하여 화면을 분할할 수 있음

 

기본 구성은 아래와 같습니다.

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

 

 

Activity Lifecycle

Activity는 실행되는 동안 여러 상태를 거칩니다. Lifecycle은 앱의 화면(Activity)가 생성되고, 실행, 종료되는 과정을 의미합니다.

Android 시스템은 사용자가 화면을 이동할 때 적절한 콜백을 호출하여 Activity의 상태를 관리하고, 개발자는 이에 맞춰 리소스를 최적화해야 합니다.

 

 

 

1) onCreate()

Activity가 처음 메모리에 로드될 때 한 번만 호출합니다.

ViewBinding, Data Load 등 레이아웃 설정 및 초기화 작업을 수행합니다.

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
}

 

✅ 이 단계에서는 아래와 같은 작업들을 수행합니다.

  • View 초기화 (findViewById(), ViewBinding, Jetpack Compose 설정)
  • RecyclerView, Viewpager, Adapter 설정
  • ViewModel, LiveData 바인딩
  • Intent를 통한 데이터 전달 처리
  • 네트워크 요청 실행

❌ 아래와 같은 작업은 수행하지 않는게 좋습니다.

  • UI 관련 애니메이션 실행 → onResume()에서 수행하는 것이 좋습니다.

 

2) onStart()

Activity가 화면에 나타나기 직전에 호출합니다.

UI를 그릴 준비를 완료하고 사용자에게 표시가 가능한 상태입니다. onStart() 호출 후에 바로 onResume()이 호출됩니다.

override fun onStart() {
    super.onStart()
}


✅ 이 단계에서는 아래와 같은 작업들을 수행합니다.

  • 앱이 백그라운드에서 돌아왔다면 UI를 갱신
  • onStop()에서 해제한 리소스를 다시 등록

❌ 아래와 같은 작업은 수행하지 않는게 좋습니다.

  • UI 관련 애니메이션 실행  → onResume()에서 수행하는 것이 좋습니다.

 

3) onResume()

Activity가 완전히 표시되고 포커스를 갖는 상태입니다.

사용자는 이 상태에서 UI와 상호작용이 가능합니다.

override fun onResume() {
    super.onResume()
}


✅ 이 단계에서는 아래와 같은 작업들을 수행합니다.

  • UI 관련 애니메이션 실행
  • 센서, 카메라, GPS 등과 같은 리소스 활성화

❌ 아래와 같은 작업은 수행하지 않는게 좋습니다.

  • 긴 작업 수행 (DB 연산, 네트워크 요청)  → onStart()에서 수행하는 것이 적절합니다.

 

4) onPause()

다른 Activity가 실행되거나 홈 버튼이 눌렸을 때 호출합니다.

화면이 보이진 않지만 완전히 사라지지는 않은 상태입니다.

override fun onPause() {
    super.onPause()
}


✅ 이 단계에서는 아래와 같은 작업들을 수행합니다.

  • 애니메이션, 센서, GPS, 카메라 등의 리소스 해제
  • 데이터 저장 (DataStore, DB 등)

❌ 아래와 같은 작업은 수행하지 않는게 좋습니다.

  • 네트워크 요청  → Activity가 강제 종료될 수 있습니다.

 

5) onStop()

Activity가 완전히 보이지 않게 되면 호출됩니다.

하지만 종료되지 않고, 앱이 백그라운드에서 실행 중인 상태입니다.

override fun onStop() {
    super.onStop()
}


✅ 이 단계에서는 아래와 같은 작업들을 수행합니다.

  • 애니메이션, 센서, GPS, 카메라 등의 리소스 해제
  • 데이터 저장 (DataStore, DB 등)

 

6) onDestroy()

Activity가 완전히 종료될 때 호출됩니다.

finish()를 호출하거나 시스템이 메모리를 해제할 때 호출됩니다.

override fun onDestroy() {
    super.onDestroy()
}


✅ 이 단계에서는 아래와 같은 작업들을 수행합니다.

  • ViewModel, Presenter, Coroutine Scope 등 해제
  • Disposable 객체 정리

 


기본적인 Lifecycle의 흐름은 아래와 같습니다.

  • 앱 실행 → onCreate() → onStart() → onResume()
  • 홈 버튼 클릭 → onPause() → onStop()
  • 다시 실행 → onRestart() → onStart() → onResume()
  • 앱 종료 → onPause() → onStop() → onDestroy()

 

이외에도 여러가지 시나리오가 발생할 수 있습니다.

  • 다른 Activity 실행 → onPause() → 다른 Activity 완전히 표시 → onStop()
  • 화면이 회전될 때 (Configuration Change)
    • 기존 Activity → onPause() → onStop() → onDestroy()
    • 새로운 Activity → onCreate() → onStart() → onResume()

 

State Changes

앞서 언급한 시나리오와 같이, 안드로이드 앱을 실행하는 동안 화면 회전, 멀티태스킹, 프로세스 종료 등의 이유로 Activity가 재생성될 수 있습니다. 이때, 사용자가 입력한 데이터나 UI 상태를 유지해야 사용자에게 더 좋은 환경을 제공할 수 있습니다.

 

이를 위해 안드로이드는 onSaveInstanceState()를 이용해 데이터를 저장하고, onRestoreInstanceState() 또는 onCreate()에서 데이터를 복원할 수 있도록 지원합니다.

 

onSaveInstanceState()

Activity가 강제 종료되거나, 화면 회전 등으로 인해 재생성될 때 호출됩니다.

Bundle 객체를 이용해 UI 상태, 사용자 입력 값, 기타 데이터를 저장할 수 있습니다.

onPause()와 onStop()보다 먼저 호출됩니다.

override fun onSaveInstanceState(outState: Bundle) {
    super.onSaveInstanceState(outState)
    
    // 사용자가 입력한 텍스트 저장
    outState.putString("USER_INPUT", editText.text.toString())

    // 현재 RecyclerView 스크롤 위치 저장
    outState.putInt("SCROLL_POSITION", recyclerView.layoutManager?.onSaveInstanceState())
}

 

이때 onSaveInstanceState()는 일시적인 UI 상태만 저장할 수 있습니다. 따라서 네트워크 데이터, DB 관련 데이터 등 장기적이거나 크기가 큰 데이터는 ViewModel 또는 Room, DataStore 등을 사용하는 것이 좋습니다.

또한, 사용자가 뒤로 가기를 눌러 앱을 종료하는 경우에는 호출되지 않습니다.

 

onRestoreInstanceState()

onCreate() 이후에 저장된 Bundl이 있으면 호출됩니다.

onCreate()에서도 savedInstanceState를 통해 복원이 가능하지만, onRestoreInstanceState()는 Activity가 완전히 생성된 후에 호출되므로 UI 초기화 후 데이터를 복원하는데 적합합니다.

override fun onRestoreInstanceState(savedInstanceState: Bundle) {
    super.onRestoreInstanceState(savedInstanceState)
    
    // 저장된 데이터 복원
    val userInput = savedInstanceState.getString("USER_INPUT", "")
    editText.setText(userInput)

    val scrollPosition = savedInstanceState.getParcelable<Parcelable>("SCROLL_POSITION")
    recyclerView.layoutManager?.onRestoreInstanceState(scrollPosition)
}

 

 

onCreate()에서 데이터를 복원하는 방법은 다음과 같습니다.

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    if (savedInstanceState != null) {
        val userInput = savedInstanceState.getString("USER_INPUT", "")
        editText.setText(userInput)
    }
}

 

728x90

 

 

Task and Back Stack

안드로이드의 Task와 Back Stack 개념은 Activity의 실행 흐름과 이동 방식을 결정하는 핵심 요소입니다.

 

Task

Task는 사용자가 수행하는 작업의 단위로, 하나의 독립된 앱 실행 흐름을 의미합니다.

하나 이상의 Activity가 쌓이는 구조이며, 홈 버튼을 누르면 백그라운드에서 유지될 수 있습니다.

기본적으로 Back Stack을 관리하며, LIFO(Last In First Out)방식으로 동작합니다.

 

Back Stack

Back Stack은 Task 내부의 Activity를 저장하는 스택(Stack)입니다.

Activity는 Intent를 통해 실행될 때 자동으로 Back Stack에 추가됩니다. 사용자가 뒤로 가기 버튼을 누른 경우, Back Stack의 최상단 Activity가 제거되며 이전 Activity가 보입니다. Back Stack이 비어있을 때는 Task가 종료됩니다.

 

 

Task의 주요 속성 및 제어 방법은 다음과 같습니다.

 

1) launchMode

Activity가 Task와 Back Stack에서 어떻게 동작할지 결정하는 중요한 속성입니다.

launchMode 설명 예시
standard (기본값) 매번 새로운 Activity 인스턴스 생성 리스트에서 아이템 클릭 시 새로운 DetailActivity
singleTop 동일한 Activity가 최상단에 있으면 재사용 알림(Notification) 클릭 시 동일 화면 유지
singleTask 새로운 Task를 생성하고, 기존에 있으면 재사용 웹 브라우저, 홈 화면 등 하나만 유지해야 하는 경우
singleInstance 오직 하나의 Task에서만 실행 가능 미디어 플레이어, 전화 앱 등 독립적인 앱

 

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

 

 

2) Intent Flags

Activity 실행 시 Intent에 추가하여 Task 및 Back Stack 동작을 제어할 수 있습니다.

Flag 설명 예시
FLAG_ACTIVITY_NEW_TASK 새 Task에서 Activity를 실행 브라우저 링크 클릭 시 새로운 Task에서 열기
FLAG_ACTIVITY_CLEAR_TOP 해당 Activity가 이미 백스택에 있으면, 그 위의 Activity를 모두 제거하고 해당 Activity 재사용 특정 화면으로 돌아가되, 중간 화면들은 제거
FLAG_ACTIVITY_SINGLE_TOP 현재 최상단의 Activity와 동일하면 재사용 알림(Notification) 클릭 시 중복 실행 방지
FLAG_ACTIVITY_CLEAR_TASK 현재 Task 내의 모든 Activity 제거 후 새로 시작 사용자 로그아웃 후 로그인 화면으로 이동

 

val intent = Intent(this, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
startActivity(intent)

 

 

3) TaskAffinity

Activity가 어떤 Task에 속할지를 결정하는 속성입니다.

안드로이드에서는 기본적으로 같은 애플리케이션의 모든 Activity가 동일한 Task에 속합니다. 그러나 특정 Activity가 새로운 Task에서 실행되도록 설정할 수 있습니다. 이때 사용되는 속성이 TaskAffinity입니다.

 

만약 MainActivity → DetailActivity → SettingActivity 로 이동할 경우 Back Stack의 구조는 다음과 같습니다. 

Task A
├── MainActivity
├── DetailActivity
└── SettingsActivity

 

 

이때 SettingActivity를 별도의 Task에서 실행하도록 변경해보겠습니다.

taskAffinity를 사용하여 새로운 Task를 정의한 경우, SettingActivity가 실행 됨과 동시에 해당 Task가 활성화 됩니다.

<activity android:name=".SettingActivity"
    android:taskAffinity="com.example.sample"
    android:launchMode="singleTask"/>

 

위처럼 taskAffinity 속성을 설정하면, 사용자가 해당 Activity로 이동한 경우 새로운 Task에서 실행됩니다.

다른 화면으로 이동해도 SettingActivity는 독립적인 Task에서 유지됩니다.

Task A
├── MainActivity
└── DetailActivity

Task B
└── SettingsActivity

 

 

기본적으로 Activity는 한 번 실행된 Task에서 유지됩니다.

이때 allowTaskReparenting를 사용하여 Activity를 다른 Task로 배치할 수 있습니다.

<activity android:name=".SettingActivity"
    android:taskAffinity="com.example.sample"
    android:allowTaskReparenting="true"/>

 

allowTaskReparenting 값을 true로 설정하면, 정의한 Task가 나타난 경우 해당 Task로 이동합니다.

이때 해당 taskAffinity에 정의한 Task가 활성화되기 전까지는 기본 Task에 위치합니다.

 

 

동작은 다음과 같습니다. 

먼저, 아래와 같이 Activity가 구성되어 있다고 가정해보겠습니다.

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

<activity android:name=".DetailActivity"
    android:taskAffinity="com.example.sample"
    android:launchMode="singleTask"
    android:allowTaskReparenting="true"/>

<activity android:name=".SettingActivity"
    android:taskAffinity="com.example.sample"
    android:launchMode="singleTask"/>

 

1️⃣ MainActivity 실행 시 Task A에서 시작됩니다.

Task A
└── MainActivity

 

2️⃣ DetailActivity 실행 시 Task A에서 시작됩니다.

Task A
├── MainActivity
└── DetailActivity

 

3️⃣ SettingActivity 실행 시 Task B를 활성화하며 해당 Task에서 시작됩니다.

Task A
├── MainActivity
└── DetailActivity

Task B
└── SettingActivity

 

4️⃣ Task B가 활성화되었으므로, DetailActivity는 Task B로 이동합니다.

Task A
└── MainActivity

Task B
└── SettingActivity
└── DetailActivity

 

 

Parcelables and Bundles

안드로이드에서 Parcelable과 Bundle는 Activity 간 데이터 전달과 관련이 깊습니다.

특히 성능 최적화, 프로세스 간 데이터 전송, 앱 내 데이터 저장 등에 중요한 역할을 수행합니다.

 

Parcelable

안드로이드의 IPC(Inter-Process Communication)을 위해 만들어진 인터페이스로, 객체를 직렬화(Serialization)하여 다른 컴포넌트로 전달하는 방식입니다.

이 방식은 Serializable보다 성능이 뛰어나기 때문에 안드로이드에서 기본적으로 권장하는 데이터 직렬화 방법입니다.

💡직렬화(Serialization)란?
객체를 Byte 형태로 변환하여 파일, 네트워크, 데이터베이스 등에 저장하거나 전송할 수 있도록 하는 과정을 말합니다.
반대로 직렬화된 데이터를 다시 원래 객체로 복원하는 과정을 역직렬화(Deserialization)라고 합니다.

 

 

Parcelable 사용 방법은 다음과 같습니다.

import android.os.Parcel
import android.os.Parcelable

data class User(val name: String, val age: Int) : Parcelable {

    // 1. Parcelable 객체를 Parcel로 변환하는 함수
    override fun writeToParcel(parcel: Parcel, flags: Int) {
        parcel.writeString(name)
        parcel.writeInt(age)
    }

    // 2. Parcel에서 데이터를 읽어 다시 객체로 변환하는 함수
    companion object CREATOR : Parcelable.Creator<User> {
        override fun createFromParcel(parcel: Parcel): User {
            val name = parcel.readString() ?: ""
            val age = parcel.readInt()
            return User(name, age)
        }

        override fun newArray(size: Int): Array<User?> {
            return arrayOfNulls(size)
        }
    }

    // 3. Parcelable 인터페이스에서 필요한 기본 메서드
    override fun describeContents(): Int {
        return 0
    }
}

 

 

복잡한 코드를 간소화하기 위해서는 @Parcelize 어노테이션을 사용합니다.

@Parcelize 어노테이션을 사용하기 위해서는 앱 수준의 build.gradle 파일 내 plugins에 아래 설정이 추가되어야 합니다.

id("kotlin-parcelize")

 

사용 방법은 매우 간단합니다.

@Parcelize 어노테이션을 추가함으로써, 앞선 복잡한 코드를 간소화할 수 있습니다.

import kotlinx.parcelize

@Parcelize
class User(
    val name: String,
    val age: Int
): Parcelable

 

 

 

이렇게 구현한 객체는 intent를 이용해 전달합니다.

val user = User("SAMPLE_NAME", 0)
val intent = Intent(this, DetailActivity::class.java)
intent.putExtra("user_data", user)
startActivity(intent)

 

아래의 방법으로 데이터에 접근합니다.

val user = intent.getParcelableExtra<User>("user_data")

 

 

Bundle

Key-Value 형태로 데이터를 저장하고 전달할 수 있는 컨테이너입니다.

안드로이드에서 Intent를 통해 데이터를 전달하거나, Fragment 간 데이터 공유 시 주로 사용합니다. 또한 onSaveInstanceState()를 활용해 Activity가 종료될 때 데이터를 보존할 수 있습니다.

 

기본적인 사용 방법은 다음과 같습니다.

먼저 Bundle 객체에 데이터를 담아 intent 시 함께 전달합니다.

val bundle = Bundle()
bundle.putString("name", "SAMPLE_NAME")
bundle.putInt("age", 0)

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

 

이동 후, DetailActivity에서 intent를 사용해 동일한 key 값으로 데이터에 접근합니다.

val name = intent.extras?.getString("name")
val age = intent.extras?.getInt("age")

 

 

Bundle는 Parcelable 객체도 저장할 수 있습니다.

(User 객체는 앞서 설명한 Parcelable 파트에서 확인할 수 있습니다.)

val user = User("SAMPLE_NAME", 0)
val bundle = Bundle()
bundle.putParcelable("user_data", user)

 

데이터에 접근할 때도 동일합니다.

val user: User? = bundle.getParcelable("user_data")

 


 

지금까지 Activity의 기초적인 개념에 대해 알아보았습니다.

실무에서도 적절한 생명주기, 직렬화 방식 등을 신중하게 고려하면 안정적인 Android 앱을 개발할 수 있습니다.

 

추가적으로 Jetpack Navigation, Lifecycle Observer 등의 기술도 학습하면 좋습니다.

이 내용은 로드맵을 따라 차차 정리하겠습니다.

 


 

안드로이드 개발자 로드맵을 따라 정리한 내용입니다.

 

 

 

728x90