๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
๐Ÿš€ Development/Android

[Android] MVVM (Model-View-ViewModel) ํŒจํ„ด์ด๋ž€? ์˜ˆ์ œ ํฌํ•จ

by Jay Din 2025. 10. 12.
728x90
๋ฐ˜์‘ํ˜•

MVVM (Model-View-ViewModel) ์ด๋ž€?

MVVM์€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์ฑ…์ž„์„ ์„ธ ๋ถ€๋ถ„์œผ๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ ์ฝ”๋“œ๋ฅผ ๋” ์‰ฝ๊ฒŒ ํ…Œ์ŠคํŠธํ•˜๊ณ , ์œ ์ง€๋ณด์ˆ˜ํ•˜๊ณ , ํ™•์žฅํ•  ์ˆ˜ ์žˆ๋„๋ก ๋•๋Š” ๋””์ž์ธ ํŒจํ„ด์ž…๋‹ˆ๋‹ค.

  • Model (๋ชจ๋ธ):
    • ์—ญํ• : ๋ฐ์ดํ„ฐ์™€ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค, ๋„คํŠธ์›Œํฌ API, ๋ฐ์ดํ„ฐ ์กฐ์ž‘ ๋“ฑ ๋ฐ์ดํ„ฐ ๊ด€๋ จ๋œ ๋ชจ๋“ ๊ฒƒ์ด ์—ฌ๊ธฐ์— ํฌํ•จ๋ฉ๋‹ˆ๋‹ค.
    • ์•ˆ๋“œ๋กœ์ด๋“œ ๊ตฌํ˜„: Repository ํด๋ž˜์Šค์™€ Data Class (Kotlin)๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.
  • View (๋ทฐ): 
    • ์—ญํ• : ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ณด์ด๋Š” UI (ํ™”๋ฉด)๋ฅผ ๋‹ด๋‹นํ•˜์—ฌ, ์‚ฌ์šฉ์ž์˜ ์ž…๋ ฅ์„ ๋ฐ›๊ณ  ViewModel์— ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค. Model์ด๋‚˜ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์— ๋Œ€ํ•ด ์•Œ์ง€ ๋ชปํ•˜๊ณ , ๋‹จ์ˆœํžˆ ViewModel์ด ์ œ๊ณตํ•˜๋Š”๋ฐ์ดํ„ฐ๋ฅผ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค.
    • ์•ˆ๋“œ๋กœ์ด๋“œ ๊ตฌํ˜„: Activity ๋˜๋Š” Fragment (XML ๋ ˆ์ด์•„์›ƒ ํฌํ•จ) ์ž…๋‹ˆ๋‹ค. 
  • ViewModel (๋ทฐ๋ชจ๋ธ):
    • ์—ญํ• : View์™€ Model ์‚ฌ์ด์˜ ์ค‘๊ฐœ์ž(Mediator) ์—ญํ• ์„ํ•ฉ๋‹ˆ๋‹ค. View๊ฐ€ ํ‘œ์‹œํ•  ๋ฐ์ดํ„ฐ๋ฅผ ์ค€๋น„ํ•˜๊ณ , View์˜ ์ˆ˜๋ช… ์ฃผ๊ธฐ(Lifecycle)๋ฅผ ๊ณ ๋ คํ•˜์ง€ ์•Š๊ณ  ๋ฐ์ดํ„ฐ๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ๋ณด์กดํ•ฉ๋‹ˆ๋‹ค.
    • ์•ˆ๋“œ๋กœ์ด๋“œ ๊ตฌํ˜„: JetPack ViewModel ํด๋ž˜์Šค๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

 

MVVM์„ ์‚ฌ์šฉํ•˜๋Š” ์ด์œ ์™€ ์‹œ์ 

1. MVVM์„ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๋Š” ์ด์œ 

์žฅ์  ์„ค๋ช…
๊ด€์‹ฌ์‚ฌ์˜ ๋ถ„๋ฆฌ UI ์ฝ”๋“œ(`View`)์™€๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง/๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ(Model, ViewModel)๊ฐ€ ๋ช…ํ™•ํ•˜๊ฒŒ ๋ถ„๋ฆฌ๋˜์–ด ์ฝ”๋“œ์˜ ๊ฐ€๋…์„ฑ์ด ๋†’์•„์ง€๊ณ  ํŠน์ • ๋ถ€๋ถ„์„ ๋ณ€๊ฒฝํ•  ๋•Œ ๋‹ค๋ฅธ ๋ถ€๋ถ„์— ๋ฏธ์น˜๋Š” ์˜ํ–ฅ์ด ์ตœ์†Œํ™”๋ฉ๋‹ˆ๋‹ค. 
ํ…Œ์ŠคํŠธ ์šฉ์ด์„ฑ `ViewModel`์€ Android Framework์— ์˜์กดํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ, UI๋‚˜ `Activity`์—†์ด ์ˆœ์ˆ˜Kotlin/Java ์ฝ”๋“œ๋กœ ๋กœ์ง์„ ๋‹จ์œ„ ํ…Œ์ŠคํŠธํ•˜๊ธฐ๊ฐ€ ๋งค์šฐ ์‰ฝ์Šต๋‹ˆ๋‹ค 
์ˆ˜๋ช… ์ฃผ๊ธฐ ์•ˆ์ „์„ฑ ViewModel์€ `Activity`๊ฐ€ ํ™”๋ฉด ํšŒ์ „ ๋“ฑ์œผ๋กœ ์žฌ์ƒ๋˜๋”๋ผ๋„ ๋ฐ์ดํ„ฐ๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ์œ ์ง€ํ•˜๋ฉฐ, ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ์ €ํ•ดํ•˜๋Š” ๋ฐ์ดํ„ฐ ์†์‹ค์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.

 

2. MVVM์„ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๋Š” ์‹œ์ 

  • ์ค‘๊ทœ๋ชจ ์ด์ƒ์˜ ์•ฑ ๊ฐœ๋ฐœ ์‹œ: ๋‹จ์ˆœํ•œ 'Hello World' ์•ฑ์„ ๋„˜์–ด ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ, ๋„คํŠธ์›Œํฌ ํ†ต์‹ , ์‚ฌ์šฉ์ž ์ž…๋ ฅ ์ฒ˜๋ฆฌ ๋“ฑ ๋ณต์žกํ•œ ๋กœ์ง์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ, MVVM์€ ํ•„์ˆ˜์ ์ž…๋‹ˆ๋‹ค.
  • ํ˜‘์—… ํ”„๋กœ์ ํŠธ ์‹œ: ๋ช…ํ™•ํ•œ ์—ญํ•  ๋ถ„๋‹ด์œผ๋กœ ์—ฌ๋Ÿฌ ๊ฐœ๋ฐœ์ž๊ฐ€ ๋™์‹œ์— ์ž‘์—…ํ•˜๊ธฐ ์šฉ์ดํ•ฉ๋‹ˆ๋‹ค.
  • ์žฅ๊ธฐ์ ์œผ๋กœ ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ํ•„์š”ํ•œ ์•ฑ: ์•ฑ์˜ ๊ทœ๋ชจ๊ฐ€ ์ปค์ง€๊ฑฐ๋‚˜ ์š”๊ตฌ์‚ฌํ•ญ์ด ๊ณ„์† ๋ณ€๊ฒฝ๋  ๋•Œ, MVVM ๊ตฌ์กฐ๋Š” ํ™•์žฅ์— ์œ ๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

 

ํ•ต์‹ฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋ฐ ๊ตฌํ˜„

์•ˆ๋“œ๋กœ์ด๋“œ์—์„œ MVVM์„ ๊ตฌํ˜„ํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋Š” Jetpack ๋ผ์ด๋ธŒ๋Ÿฌ์ž…๋‹ˆ๋‹ค.

1. ViewModel (ํ•ต์‹ฌ)

  • ๊ธฐ๋Šฅ: `Activity`๋‚˜ `Fragment`์˜ ์ˆ˜๋ช… ์ฃผ๊ธฐ๋ฅผ ์ธ์‹ํ•˜์—ฌ, UI๊ฐ€ ํŒŒ๊ดด๋˜๊ณ  ๋‹ค์‹œ ์ƒ์„ฑ๋  ๋•Œ (์˜ˆ: ํ™”๋ฉด ํšŒ์ „) ๋ฐ์ดํ„ฐ๊ฐ€ ์†์‹ค๋˜์ง€ ์•Š๋„๋ก UI ๊ด€๋ จ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๊ณ  ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
  • ์˜์กด์„ฑ ์ถ”๊ฐ€ (build.gradle.kts - app ๋ชจ๋“ˆ):
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2") // ์ตœ์‹  ๋ฒ„์ „ ์‚ฌ์šฉ ๊ถŒ์žฅ

 

2. LiveData / SataeFlow (๋ฐ์ดํ„ฐ ๊ด€์ฐฐ)

  • ๊ธฐ๋Šฅ: `ViewModel`์˜ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ `View`์— ์ž๋™์œผ๋กœ ์•Œ๋ฆผ์„ ๋ณด๋‚ด UI๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋„๋ก ๋•๋Š” ๊ด€์ฐฐ ๊ฐ€๋Šฅํ•œ ๋ฐ์ดํ„ฐ ํ™€๋”์ž…๋‹ˆ๋‹ค. `View`์˜ ์ˆ˜๋ช… ์ฃผ๊ธฐ(Lifecycle)์„ ์ธ์‹ํ•˜์—ฌ ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜๋ฅผ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.
  • ์˜์กด์„ฑ ์ถ”๊ฐ€: ๋ณดํ†ต `ViewModel` ์˜์กด์„ฑ์— ํ•จ๊ป˜ ํฌํ•จ๋ฉ๋‹ˆ๋‹ค.

 

3. Hilt / Koin (์˜์กด์„ฑ ์ฃผ์ž… - DI)

  • ๊ธฐ๋Šฅ: Repository๋‚˜ ๋‹ค๋ฅธ ํด๋ž˜์Šค๋“ค์„ ViewModel์— ๊ฐ„ํŽธํ•˜๊ฒŒ ์ œ๊ณตํ•˜๊ณ  ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ๋Œ€๊ทœ๋ชจ ์•ฑ์—์„œ ๊ฐ์ฒด ์ƒ์„ฑ์„๊น”๋”ํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•˜๋Š”๋ฐ ํ•„์ˆ˜์ ์ž…๋‹ˆ๋‹ค. 

 

MVVM ์˜ˆ์ œ: ๊ฐ„๋‹จํ•œ ์นด์šดํ„ฐ ์•ฑ

์‚ฌ์šฉ์ž๊ฐ€ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅผ ๋•Œ ๋งˆ๋‹ค ์ˆซ์ž๊ฐ€ ์ฆ๊ฐ€ํ•˜๊ณ , ํ™”๋ฉด ํšŒ์ „ ์‹œ์—๋„ ์ˆซ์ž๊ฐ€ ์œ ์ง€๋˜๋Š” ์นด์šดํ„ฐ ์•ฑ์„ MVVM์œผ๋กœ ๊ตฌํ˜„ํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

 

1. Model (Data Class & Repository)

์ด ์˜ˆ์‹œ์—์„œ๋Š” ๊ฐ„๋‹จํ•œ ์ˆซ์ž ๋ฐ์ดํ„ฐ๋งŒ ๋‹ค๋ฃจ๋ฏ€๋กœ `Repository`๋Š” ์ƒ๋žตํ•˜๊ณ  `ViewModel`์—์„œ ์ง์ ‘ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

 

2. ViewModel (kotlin)

`CounterViewModel.kt`

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

class CounterViewModel : ViewModel() {
    // 1. MutableLiveData: ViewModel ๋‚ด๋ถ€์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ€๊ฒฝ(write)ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.
    private val _count = MutableLiveData<Int>()
    
    // 2. LiveData: View(Activity/Fragment)์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ด€์ฐฐ(read)๋งŒ ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋…ธ์ถœํ•ฉ๋‹ˆ๋‹ค.
    // ์ด๋Š” ์™ธ๋ถ€์—์„œ์˜ ์ž„์˜์ ์ธ ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ์„ ๋ง‰์•„์ค๋‹ˆ๋‹ค.
    val count: LiveData<Int>
        get() = _count

    init {
        // ViewModel์ด ์ฒ˜์Œ ์ƒ์„ฑ๋  ๋•Œ ์ดˆ๊ธฐ๊ฐ’์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.
        _count.value = 0 
    }

    // 3. ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง: ์ˆซ์ž๋ฅผ ์ฆ๊ฐ€์‹œํ‚ค๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
    fun incrementCount() {
        // value ์†์„ฑ์„ ํ†ตํ•ด LiveData์˜ ๊ฐ’์„ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค.
        _count.value = (_count.value ?: 0) + 1 
    }
}

 

3. View (Activity - Kotlin + XML)

`MainActivity.kt`

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.activity.viewModels // by viewModels() ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•œ ํ™•์žฅ ํ•จ์ˆ˜
import com.example.myapplication.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

    // View Binding ์„ค์ •
    private lateinit var binding: ActivityMainBinding

    // ViewModel ์ดˆ๊ธฐํ™”: Android Jetpack์˜ 'by viewModels()'๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ViewModel์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.
    // ํ™”๋ฉด ํšŒ์ „ ์‹œ์—๋„ ๋™์ผํ•œ ์ธ์Šคํ„ด์Šค๋ฅผ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค.
    private val viewModel: CounterViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // 1. LiveData ๊ด€์ฐฐ (Observing):
        // viewModel.count์˜ ๋ณ€ํ™”๋ฅผ ๊ด€์ฐฐํ•ฉ๋‹ˆ๋‹ค. ๋ณ€ํ™”๊ฐ€ ์ƒ๊ธธ ๋•Œ๋งˆ๋‹ค { it: Int -> ... } ์ฝ”๋“œ๊ฐ€ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.
        viewModel.count.observe(this) { newCount ->
            // UI ์—…๋ฐ์ดํŠธ ๋กœ์ง (View์˜ ์—ญํ• )
            binding.textViewCounter.text = "Count: $newCount"
        }

        // 2. ์‚ฌ์šฉ์ž ์ž…๋ ฅ ์ฒ˜๋ฆฌ (View์˜ ์—ญํ• )
        binding.buttonIncrement.setOnClickListener {
            // ์‚ฌ์šฉ์ž ์•ก์…˜์„ ViewModel์— ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค. (ViewModel์˜ ๋กœ์ง ํ˜ธ์ถœ)
            viewModel.incrementCount()
        }
    }
}

 

`activity_main.xml`

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center">

    <TextView
        android:id="@+id/textView_counter"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="32sp"
        android:text="Count: 0"/>

    <Button
        android:id="@+id/button_increment"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Increment"/>

</LinearLayout>

 

 

728x90
๋ฐ˜์‘ํ˜•