안녕하세요! 오랜만에 안드로이드 프로젝트로 복귀하신 것을 환영합니다.
3년 전이라면 아마 MVP 패턴이나 findViewById를 주로 사용하셨을 텐데요, 현재 안드로이드 개발은 MVVM (Model-View-ViewModel) 아키텍처를 중심으로 완전히 재편되었습니다.
ViewModel, DataBinding, LiveData... 처음엔 낯설지만, 한번 익숙해지면 예전 방식으로 돌아갈 수 없을 만큼 강력하고 편리합니다.
이 가이드에서는 복귀 개발자의 시각에서 가장 헷갈리는 부분만 쏙쏙 뽑아, "왜 쓰는지"부터 "어떻게 쓰는지"까지 상세하게 알려드립니다.

1. "대체 왜?" - MVVM, 왜 써야 하나요?
예전에는 Activity나 Fragment(View)가 API 통신, 데이터 계산, UI 변경 등 모든 것을 처리했습니다. (Massive Activity)
문제점:
- 생명주기(Lifecycle) 지옥: 화면 회전(Configuration Change) 시 Activity가 파괴되고 재생성되면서 모든 데이터가 날아갔습니다. 이를 살리기 위해 onSaveInstanceState 같은 복잡한 처리가 필요했습니다.
- 테스트 불가능: UI 코드와 비즈니스 로직이 떡칠되어 있어 유닛 테스트가 거의 불가능했습니다.
- findViewById의 번거로움: TextView, Button 하나 쓸 때마다 findViewById를 호출하고 null 체크를 해야 했습니다.
MVVM이 해결한 것: MVVM은 이 문제들을 '관심사의 분리(Separation of Concerns)'로 해결합니다.
- View (Activity/Fragment): 이제 정말 '멍청한' 껍데기가 됩니다. 오직 화면에 데이터를 보여주고 (Observe), 사용자 입력을 전달만 (Event) 합니다.
- ViewModel: View가 필요로 하는 데이터를 보관하고, 비즈니스 로직을 처리하는 '뇌'입니다. 가장 큰 특징은 View의 생명주기(화면 회전 등)보다 오래 살아남아 데이터를 보존한다는 것입니다.
- Model (Repository): Room DB, Retrofit API 등 실제 데이터 소스를 다룹니다.
[MVVM 아키텍처 다이어그램 이미지]
View (Activity) ➡️ ViewModel (로직 처리) ➡️ Model (데이터 요청)
View (Activity) ⬅️ ViewModel (데이터 반환) ⬅️ Model (데이터 획득)
2. 프로젝트 준비: build.gradle 설정 (필수)
가장 먼저 build.gradle.kts (또는 build.gradle) 파일에 ViewModel과 DataBinding을 활성화해야 합니다.
// build.gradle (Module 수준)
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("kotlin-kapt") // DataBinding에 필요할 수 있음
}
android {
// ...
// DataBinding 활성화
buildFeatures {
dataBinding = true
}
}
dependencies {
// ViewModel (KTX - 코틀린 확장 기능)
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.3") // 최신 버전 확인
// LiveData (KTX) - ViewModel과 짝꿍
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.8.3")
// Activity에서 ViewModel을 쉽게 가져오기 위한 KTX
implementation("androidx.activity:activity-ktx:1.9.0")
}
3. 핵심 1: ViewModel - 화면 회전에도 살아남는 데이터 저장소
ViewModel은 View(Activity)가 화면 회전으로 파괴되어도, 메모리에 살아남아 데이터를 보존합니다.
3-1. ViewModel 생성하기
CounterViewModel.kt 라는 클래스를 만들어 보겠습니다.
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class CounterViewModel : ViewModel() {
// 1. [외부 변경 불가 / 내부 변경 가능] LiveData 생성 (Backing Property)
// _count는 ViewModel 내부에서만 값을 변경 (private)
private val _count = MutableLiveData<Int>()
// 2. [외부 공개용 / 변경 불가] LiveData (Read-Only)
// View(Activity)는 이 count를 '관찰'만 할 수 있음 (public)
val count: LiveData<Int> = _count
// 3. 초기값 설정
init {
_count.value = 0
}
// 4. 비즈니스 로직 (View가 호출할 함수)
fun increment() {
// LiveData의 값을 변경.
// 이 값을 '관찰'하는 모든 View는 자동으로 UI가 업데이트됨.
_count.value = (_count.value ?: 0) + 1
}
// ViewModel이 파괴될 때 호출됨 (Activity가 완전히 종료될 때)
override fun onCleared() {
super.onCleared()
// 리소스 해제 등
}
}
복귀 개발자 Check!
- LiveData vs MutableLiveData: LiveData는 값 변경이 안 되는 '읽기 전용'입니다. MutableLiveData는 value 프로퍼티로 값 변경이 가능합니다.
- Backing Property (뒷받침 속성): _count (private, Mutable)와 count (public, Read-Only)로 나누는 것은 MVVM의 표준 패턴입니다. View가 ViewModel의 데이터를 직접 수정하는 것을 막기 위함입니다.
3-2. Activity에서 ViewModel 사용하기
MainActivity.kt에서 ViewModel을 가져옵니다.
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.activity.viewModels // ⭐️ KTX 라이브러리 임포트
import com.example.app.databinding.ActivityMainBinding // ⭐️ DataBinding이 자동 생성한 클래스
class MainActivity : AppCompatActivity() {
// 1. ⭐️ DataBinding 객체 선언 (null 체크 필요 없음!)
private lateinit var binding: ActivityMainBinding
// 2. ⭐️ 'by viewModels()' KTX를 사용해 ViewModel 인스턴스화
// 이러면 화면 회전 시에도 같은 ViewModel 인스턴스를 재사용합니다.
private val viewModel: CounterViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 3. DataBinding으로 View 인플레이트 (예전 setContentView(R.layout...))
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root) // ⭐️ binding.root를 content view로 설정
// ...
}
}
복귀 개발자 Check!
- by viewModels(): 이게 마법입니다. 예전처럼 ViewModelProvider(this).get(..) 안 써도 됩니다. 이 KTX 델리게이트가 알아서 ViewModel의 생명주기를 관리해 줍니다.
- ActivityMainBinding: activity_main.xml 레이아웃 파일에 DataBinding을 적용하면, 카멜 케이스로 변환된 Binding 클래스가 자동으로 생성됩니다. 이 객체가 TextView, Button 등 모든 View의 참조를 갖고 있습니다. (No findViewById!)
4. 핵심 2: DataBinding - View와 ViewModel을 연결하는 '풀'
DataBinding은 XML 레이아웃에서 ViewModel의 데이터를 직접 사용하거나, View의 이벤트를 ViewModel의 함수에 바로 연결하는 '풀' 역할을 합니다.
4-1. 레이아웃(XML) 수정
activity_main.xml을 수정합니다.
<!-- 1. ⭐️ 최상위 태그가 <layout>이어야 합니다. -->
<layout xmlns:android="[http://schemas.android.com/apk/res/android](http://schemas.android.com/apk/res/android)"
xmlns:app="[http://schemas.android.com/apk/res-auto](http://schemas.android.com/apk/res-auto)"
xmlns:tools="[http://schemas.android.com/tools](http://schemas.android.com/tools)">
<!-- 2. ⭐️ 이 레이아웃에서 사용할 변수(ViewModel)를 선언합니다. -->
<data>
<variable
name="vm"
type="com.example.app.CounterViewModel" />
<!-- 방금 만든 ViewModel 클래스 경로 -->
</data>
<!-- 기존 레이아웃 (예: ConstraintLayout) -->
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/textViewCounter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="40sp"
<!-- 3. ⭐️ 데이터 바인딩: vm.count(LiveData<Int>) 값을 text에 바인딩 -->
<!-- Int를 String으로 변환해야 함 (중요!) -->
android:text="@{String.valueOf(vm.count)}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="100" />
<Button
android:id="@+id/buttonIncrement"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="증가"
<!-- 4. ⭐️ 이벤트 바인딩: 클릭 이벤트를 vm의 onIncrement 함수에 바로 연결 -->
android:onClick="@{() -> vm.increment()}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textViewCounter" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
복귀 개발자 Check!
- @{...}: DataBinding의 표현식입니다.
- android:text="@{String.valueOf(vm.count)}": vm.count는 LiveData<Int>입니다. android:text는 String을 받습니다. String.valueOf()로 감싸거나 vm.count.toString()을 써야 합니다. (가장 많이 하는 실수!)
- android:onClick="@{() -> vm.increment()}": 람다식(Lambda)을 사용해 onClick 이벤트를 vm.increment() 메서드에 바로 연결합니다. Activity에 setOnClickListener가 필요 없어집니다.
4-2. Activity에서 완성하기
이제 MainActivity.kt로 돌아와 ViewModel과 LiveData를 연결하는 마지막 한 줄을 추가합니다.
// MainActivity.kt
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.activity.viewModels
import com.example.app.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val viewModel: CounterViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// 1. ⭐️ DataBinding에게 ViewModel 변수(vm)가 누구인지 알려줌
binding.vm = viewModel
// 2. ⭐️⭐️⭐️ [가장 중요] DataBinding이 LiveData를 관찰할 수 있도록
// 생명주기 소유자(LifecycleOwner)를 설정합니다.
// 이 코드가 없으면 LiveData가 변경되어도 XML이 업데이트되지 않습니다!
binding.lifecycleOwner = this
/*
* 끝입니다!
* * 이제 버튼을 누르면 XML(@onClick) -> ViewModel(increment) -> LiveData(_count.value)
* -> XML(@{vm.count})이 자동으로 업데이트됩니다.
* * Activity는 아무 코드도 작성하지 않았습니다.
*/
}
}
복귀 개발자 Check!
- binding.lifecycleOwner = this: 이 코드를 빠뜨려서 "데이터가 왜 안 바뀌죠?"라고 질문하는 경우가 90%입니다. LiveData가 View의 생명주기(Active/Inactive)를 알아야만 자동으로 UI를 갱신할 수 있기 때문에 필수입니다.
5. 결론: 왜 이게 더 좋은가?
- 생명주기 완전 정복: 화면을 100번 회전해도 ViewModel의 count 값은 보존됩니다.
- findViewById 제거: binding 객체가 모든 View를 소유하며, 타입도 안전합니다.
- setOnClickListener 제거: 이벤트가 XML을 통해 ViewModel로 바로 전달됩니다.
- 관심사 완벽 분리:
- MainActivity는 binding.vm = viewModel, binding.lifecycleOwner = this 단 두 줄의 '연결' 코드 외에 아무것도 하지 않습니다. (멍청한 View)
- CounterViewModel은 '증가' 로직만 담당합니다. (똑똑한 ViewModel)
- 테스트 용이: CounterViewModel은 안드로이드 프레임워크 의존성이 거의 없는 순수 Kotlin 클래스입니다. JVM에서 바로 유닛 테스트를 돌릴 수 있습니다.
이제 API 통신이 필요하다면 ViewModel 안에서 Retrofit을 호출하고 그 결과를 MutableLiveData에 담기만 하면 됩니다. View는 알아서 갱신될 것입니다.
이 기본기만 탄탄히 잡으시면, Flow, StateFlow, Hilt 등 다음 스텝으로 나아가기 훨씬 수월하실 겁니다. 복귀를 응원합니다!
다음 가이드 바로가기
'개발 이야기 > Android (안드로이드)' 카테고리의 다른 글
| GitHub Copilot 이 말아주는 MVVM 시작 가이드 (0) | 2025.12.08 |
|---|---|
| ChatGpt가 말아주는 MVVM 시작 가이드 (0) | 2025.12.08 |
| ViewModel, DataBinding 으로 ViewPager2/RecyclerView 제대로 쓰는 방법 정리 (1) | 2025.11.19 |
| [Android] MVVM 다음 스텝: Flow, StateFlow, Hilt 초보자 완벽 가이드 (1) | 2025.11.18 |
| Jetpack Compose 초보자 가이드 (0) | 2024.12.23 |
| Android DataBinding과 ViewModel 적용하기 (6) | 2024.12.12 |
| Fragment 효율적인 코딩 방법에 대한 정리 (0) | 2024.12.12 |
| Java8 Stream API 찍먹하기 (2) | 2024.12.11 |