
[MVVM + Room + Kotlin] 일기장 앱 개발 가이드
Phase 0: 개념 잡기 (Java 개발자를 위한 브리핑)
1. MVVM 패턴이란?
과거에는 Activity가 UI도 그리고, DB도 찌르고, 로직도 처리했죠? (God Activity). MVVM은 이를 철저히 분리합니다.
- Model (Room DB): 데이터 그 자체입니다.
- View (XML, Activity): 껍데기입니다. "화면에 무엇을 보여라"라는 명령을 받기만 하고, 사용자의 클릭을 ViewModel에 전달만 합니다. 절대 로직을 갖지 않습니다.
- ViewModel: 화면을 위한 데이터를 들고 있는 상태 저장소입니다. 화면 회전(Rotation)이 일어나도 죽지 않습니다. View가 "나 뭐 보여줘?" 하고 관찰(Observe)하고 있는 대상입니다.
2. 코틀린 문법 리프레시 (3년 만의 복귀)
- Null Safety (
?,!!): 코틀린은null이 될 수 있는 변수(String?)와 없는 변수(String)를 엄격히 구분합니다. valvsvar:final변수는val, 변경 가능하면var.- Data Class: 자바의 POJO(Getter/Setter/toString/Equals)를 한 줄로 끝냅니다.
- Coroutines (
suspend): 자바의 AsyncTask나 Thread를 대체합니다. 비동기 코드를 동기 코드처럼 짭니다.
Phase 1: 프로젝트 설정 (Gradle)
최신 안드로이드 개발은 의존성 설정이 반입니다. build.gradle (Module: app)에 다음을 추가합니다.
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("kotlin-kapt") // 데이터 바인딩 및 Room 처리를 위한 어노테이션 프로세서
}
android {
// ... 설정 유지
// 데이터 바인딩 활성화
buildFeatures {
dataBinding = true
viewBinding = true
}
}
dependencies {
// ViewModel & LiveData
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.2")
implementation("androidx.activity:activity-ktx:1.8.0") // by viewModels() 사용 위함
// Room DB
val roomVersion = "2.6.1"
implementation("androidx.room:room-runtime:$roomVersion")
implementation("androidx.room:room-ktx:$roomVersion") // 코루틴 지원
kapt("androidx.room:room-compiler:$roomVersion")
}
Phase 2: Model 설계 (Room DB)
DB 테이블과 쿼리를 만드는 단계입니다.
1. Entity (테이블)
자바의 Bean과 같습니다. data class를 사용합니다.
// Diary.kt
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "diary_table")
data class Diary(
@PrimaryKey(autoGenerate = true)
val id: Long = 0, // 기본값 0, DB 저장 시 자동 증가
var title: String,
var content: String,
var date: Long = System.currentTimeMillis()
)
Kotlin Tip: 생성자 안에
val/var를 선언하면 멤버 변수 선언과 초기화가 동시에 됩니다.
2. DAO (Data Access Object)
실제 쿼리를 담당합니다. suspend 키워드에 주목하세요. (비동기 처리)
// DiaryDao.kt
import androidx.room.*
import kotlinx.coroutines.flow.Flow
@Dao
interface DiaryDao {
// Flow: 데이터가 변경되면 실시간으로 데이터를 계속 뱉어주는 파이프라인 (RxJava와 비슷)
@Query("SELECT * FROM diary_table ORDER BY date DESC")
fun getAllDiaries(): Flow<List<Diary>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(diary: Diary)
@Delete
suspend fun delete(diary: Diary)
@Update
suspend fun update(diary: Diary)
}
3. Database
싱글톤 패턴으로 DB 인스턴스를 생성합니다.
// AppDatabase.kt
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import android.content.Context
@Database(entities = [Diary::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun diaryDao(): DiaryDao
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getDatabase(context: Context): AppDatabase {
// 엘비스 연산자 (?:) - 왼쪽이 null이면 오른쪽을 실행
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"diary_database"
).build()
INSTANCE = instance
instance
}
}
}
}
Phase 3: ViewModel (핵심 로직)
UI(Activity)와 데이터(DB) 사이의 중계자입니다.
요구사항: 목록 관리, 선택된 일기 관리(뷰어용), 수정/삭제 기능.
// DiaryViewModel.kt
import android.app.Application
import androidx.lifecycle.*
import kotlinx.coroutines.launch
// AndroidViewModel은 Application Context가 필요할 때 사용합니다 (DB 초기화 등)
class DiaryViewModel(application: Application) : AndroidViewModel(application) {
private val dao = AppDatabase.getDatabase(application).diaryDao()
// 1. 일기 목록 (DB에서 실시간으로 관찰)
// asLiveData(): Flow를 UI에서 관찰하기 쉬운 LiveData로 변환
val allDiaries: LiveData<List<Diary>> = dao.getAllDiaries().asLiveData()
// 2. 현재 선택된 일기 (뷰어에 표시될 데이터)
private val _selectedDiary = MutableLiveData<Diary?>()
val selectedDiary: LiveData<Diary?> get() = _selectedDiary
// 일기 선택 이벤트 처리
fun selectDiary(diary: Diary) {
_selectedDiary.value = diary
}
// 3. 기능 구현 (코루틴 사용)
// viewModelScope: ViewModel이 죽으면 코루틴도 같이 취소됨 (메모리 누수 방지)
fun insert(title: String, content: String) = viewModelScope.launch {
val newDiary = Diary(title = title, content = content)
dao.insert(newDiary)
}
fun update(diary: Diary) = viewModelScope.launch {
dao.update(diary)
}
fun delete(diary: Diary) = viewModelScope.launch {
dao.delete(diary)
_selectedDiary.value = null // 삭제 후 뷰어 비우기
}
}
Phase 4: UI & DataBinding (화면 구현)
요청하신 좌측 목록, 우측 뷰어 레이아웃을 XML에 작성합니다.ConstraintLayout이나 LinearLayout의 weight를 사용합니다.
1. activity_main.xml
DataBinding의 핵심: 최상위 태그가 <layout>이어야 합니다. <data> 태그 안에 ViewModel을 선언하여 XML이 직접 데이터를 바라보게 합니다.
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="viewModel"
type="com.example.diary.DiaryViewModel" />
<variable
name="currentDiary"
type="com.example.diary.Diary" />
<import type="android.view.View"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:weightSum="2">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="#EFEFEF"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
<ScrollView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<EditText
android:id="@+id/etTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="제목"
android:text="@{viewModel.selectedDiary.title}" />
<EditText
android:id="@+id/etContent"
android:layout_width="match_parent"
android:layout_height="200dp"
android:hint="내용"
android:gravity="top"
android:text="@{viewModel.selectedDiary.content}" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:text="저장/수정"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{() -> viewModel.selectedDiary != null ? viewModel.update(viewModel.selectedDiary) : viewModel.insert(etTitle.getText().toString(), etContent.getText().toString())}" />
<Button
android:text="삭제"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{viewModel.selectedDiary != null ? View.VISIBLE : View.GONE}"
android:onClick="@{() -> viewModel.delete(viewModel.selectedDiary)}" />
<Button
android:text="새 글 쓰기"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{() -> viewModel.selectDiary(null)}" />
</LinearLayout>
</LinearLayout>
</ScrollView>
</LinearLayout>
</layout>
2. RecyclerView Adapter (목록 아이템)
DataBinding을 사용하면 findViewById가 사라집니다.
// DiaryAdapter.kt
class DiaryAdapter(private val onClick: (Diary) -> Unit) :
androidx.recyclerview.widget.ListAdapter<Diary, DiaryAdapter.ViewHolder>(DiffCallback) {
// 뷰홀더 정의
class ViewHolder(private val binding: ItemDiaryBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(diary: Diary, onClick: (Diary) -> Unit) {
binding.diary = diary // XML 변수에 데이터 주입
binding.root.setOnClickListener { onClick(diary) }
binding.executePendingBindings() // 즉시 갱신 강제
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
// DataBindingUtil을 사용해 inflate
val binding = ItemDiaryBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(getItem(position), onClick)
}
// ListAdapter 필수 구현: 데이터 변경 감지 로직
object DiffCallback : DiffUtil.ItemCallback<Diary>() {
override fun areItemsTheSame(oldItem: Diary, newItem: Diary): Boolean = oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Diary, newItem: Diary): Boolean = oldItem == newItem
}
}
(참고: item_diary.xml도 <layout> 태그로 감싸고 <variable name="diary" type="...Diary"/>를 선언해야 합니다.)
Phase 5: Activity (연결)
이제 Activity 코드는 매우 짧아집니다. UI 로직만 남기 때문입니다.
// MainActivity.kt
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.activity.viewModels
import androidx.databinding.DataBindingUtil
import com.example.diary.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
// by viewModels() 대리자(delegate)를 사용하면 ViewModel 생성을 라이브러리가 알아서 해줍니다.
private val viewModel: DiaryViewModel by viewModels()
private lateinit var binding: ActivityMainBinding
private lateinit var adapter: DiaryAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 1. DataBinding 설정
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
// 2. 생명주기 소유자 설정 (이게 있어야 LiveData 변경 시 XML이 자동 갱신됨) -> 매우 중요!
binding.lifecycleOwner = this
// 3. XML 변수에 ViewModel 연결
binding.viewModel = viewModel
// 4. 리사이클러뷰 설정
setupRecyclerView()
// 5. 관찰(Observe): ViewModel의 데이터가 변하면 UI 업데이트
observeViewModel()
}
private fun setupRecyclerView() {
adapter = DiaryAdapter { selectedDiary ->
// 목록 클릭 시 ViewModel에 "이거 선택됐어"라고 알림
viewModel.selectDiary(selectedDiary)
}
binding.recyclerView.adapter = adapter
}
private fun observeViewModel() {
// DB 목록이 바뀌면 어댑터에 새 리스트 제출
viewModel.allDiaries.observe(this) { diaries ->
adapter.submitList(diaries)
}
// 선택된 일기가 바뀌면 제목/내용 EditText 갱신 (양방향 바인딩을 안 썼을 경우 수동 갱신 필요)
viewModel.selectedDiary.observe(this) { diary ->
if (diary != null) {
binding.etTitle.setText(diary.title)
binding.etContent.setText(diary.content)
} else {
// "새 글 쓰기" 모드
binding.etTitle.setText("")
binding.etContent.setText("")
}
}
}
}
핵심 요약 및 동작 원리

- 앱 실행:
MainActivity가 켜지고DiaryViewModel이 로드됩니다. - 데이터 로드:
ViewModel은Room을 통해 DB를 구독(Flow)하고,allDiariesLiveData를 업데이트합니다. - 목록 표시:
MainActivity는allDiaries를 관찰하다가 변경되면Adapter에 데이터를 넘겨줍니다. (좌측 화면 갱신) - 아이템 클릭: 사용자가 목록 아이템을 누르면
viewModel.selectDiary(diary)가 호출됩니다. - 뷰어 표시:
_selectedDiary값이 변합니다. 이를 관찰하던MainActivity나 XML 바인딩이 우측 에디트텍스트의 내용을 바꿉니다. - 수정/삭제: 버튼을 누르면
viewModel.update()또는viewModel.delete()가 호출되고, DB가 바뀌면 다시 2번 과정이 자동으로 수행되어 목록이 갱신됩니다.
이 구조의 장점은 "데이터를 갱신하고 화면을 다시 그려라" 라는 코드를 짤 필요가 없다는 것입니다. 데이터만 바꾸면 화면은 알아서 따라옵니다.
'개발 이야기 > Android (안드로이드)' 카테고리의 다른 글
| [Android] 확 바뀐 안드로이드 스튜디오 프로파일러로 메모리 릭 & CPU 점유율 잡는 법 (0) | 2026.01.22 |
|---|---|
| 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 |
| [Android] 3년 만에 복귀한 개발자를 위한 MVVM, ViewModel, DataBinding 완벽 정복 가이드 (0) | 2025.11.18 |
| Jetpack Compose 초보자 가이드 (0) | 2024.12.23 |
| Android DataBinding과 ViewModel 적용하기 (6) | 2024.12.12 |