안드로이드

[Android] MVVM architecture example

IT꿈나무 2023. 7. 13. 19:53
반응형

1. 안드로이드의 기초

1.1 안드로이드의 4대 컴포넌트[1]

컴포넌트란? 안드로이드 시스템이 생명 주기를 관리하는 프로그램 덩어리(Class). 컴포넌트의 물리적인 모습은 클래스이며, 안드로이드에서 클래스는 컴포넌트와 일반 클래스로 나뉜다. 이 둘의 차이는 생명주기를 누가 관리하는지에 있고 안드로이드의 컴포넌트는 안드로이드 시스템이 생명주기를 관리하며, 일반 클래스는 개발자가 관리한다는 차이가 있다. [1]

l  안드로이드 컴포넌트는 4가지 종류가 있다.

액티비티(Activity): UI를 구성하기 위한 컴포넌트
서비스(Service): UI 없이 백그라운드에서 장시간 수행되는 컴포넌트
콘텐츠 프로바이더(ContentProvider): 애플리케이션 간 데이터를 공유하기 위한 컴포넌트
브로드캐스트 리시버(BroadcastReceiver): 이벤트를 받기 위한 컴포넌트

1.2 디렉토리와 파일 구조.

 안드로이드 프로젝트의 파일 구조는 app 영역과 gradle scripts 영역으로 나뉘며, gradle scripts 영역은 프로젝트의 빌드 정보와 라이브러리 설정 정보가 담겨 있다. app 영역은 app에 대한 내용들에 해당하는 정보를 담고 있으며, 애플리케이션의 권한 정보 컴포넌트 정보등의 정보를 관리하는 ‘manifests’와 앱의 프로그램 코드인 소스코드UI 정보 이미지 정보등을 관리하는 리소스등으로 구성된다.

1.3 안드로이드 앱 개발의 기초적인 버튼을 만들고, 간단한 메시지를 띄워 보는 프로젝트로 앱을 만들어 본다. (기본적인 안드로이드 개발 방법을 습득한다)

 웹개발에서는 UI를 개발할 때 HTML(Hyper Text Markup Language)라는 마크업 랭귀지를 이용하지만 안드로이드에서는 레이아웃이라는 사용자 인터페이스의 구조를 정의하는 것을 XML(eXtensible Markup Language)이라는 마크업 랭귀지를 이용하여 UI 구조를 정의하게 된다.

 Layout의 정의는 Layout Editor를 사용하여 드래그 앤 드롭 인터페이스로 XML을 정의할 수 있다. [2] Layout EditorXML 레이아웃 파일을 열면 나타난다. [3]

 Layout Editor을 이용하여 Button UI를 정의할 수 있고, 뷰 객체를 고유하게 식별할 수 있는 정수 ID를 연결한다. ID는 정수로 참조되지만, xml 파일 내에서는 id 속성에서 문자열로 할당되며 구문은 android:id="@+id/button"와 같다. [4]

1. 레이아웃 xml에 뷰를 정의한다.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout>
    <!--Button을 정의 하며 button을 식별 할 수 있는 고유 id를 할당한다.-->
    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:text="Button"
        app:layout_constraintEnd_toEndOf="@+id/textView"
        app:layout_constraintStart_toStartOf="@+id/textView"
        app:layout_constraintTop_toBottomOf="@+id/textView"/>

</androidx.constraintlayout.widget.ConstraintLayout>

 

 

2. 레이아웃 xml code를 연결하고, Toast를 이용하여 “Hello World”를 출력한다.

package com.jky.myhelloworld;

import …

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //프로젝트 내에서 layou.xml을 참조한다.
        setContentView(R.layout.activity_main);

        //button 객체를 생성하고, layout의 button 뷰 객체와 연결한다.
        Button btn = findViewById(R.id.button);
        
        //버튼에 클릭 리스너를 구현한다.
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                
                //Toast를 이용하여 "hello world"를 출력한다.
                Toast.makeText(getBaseContext(),
				"Hello world", Toast.LENGTH_LONG).show();
            }
        });
    }
}

[1] 깡쌤의 안드로이드 개발

[2] Android 개발 문서 레이아웃’, https://developer.android.com/guide/topics/ui/declaring-layout?hl=ko#layout

[3] Android 개발 문서 “layout Editor 소개”, https://developer.android.com/studio/write/layout-editor?hl=ko#intro

[4] Android 개발 문서 “ID”, https://developer.android.com/guide/topics/ui/declaring-layout?hl=ko#id

 

2. MVVM 아키텍처

앱 개발은 기능구현 뿐만 아니라, 안정성과 유지보수에 능한 구조로 설계되어야 한다.

이를 위해 MVC(Model, View, Controller), MVP(Model, View, Presenter) Pattern 등 다양한 아키텍처 패턴이 연구되어왔다. 최근의 연구에서는 MVVM(Model, View, ViewModel) 패턴이 안드로이드 어플리케이션의 구조 설계로 활용되고 있다. 교육의 부족으로 바르지 않은 방향으로 아키텍처가 설계되고 개발되어, 오히려 더 비효율적인 아키텍처 구조로 사용되는 현실이 있다.[1] 보다 나은 아키텍처의 설계를 위해 MVVM에 대해 알아보고 적용해 보도록 한다.

 

2.1 아키텍처 패턴[2]

2.1.1 MVC (Mode-View-Controller)

 프로그램을 각각의 역할에 따라 Model, View, Controller로 나누어 설계한 아키텍처 패턴이다. ViewUi 영역의 layout XML (ActivitySetContentView (R.layout.activity_main))을 나타내고, ControllerActivityUI와 관련된 코드 예로 Click listener등을 나타내며 modelDB를 나타낸다.

 Controller modelview를 연결해준다. 사용자가 View에서 이벤트를 발생시키면 액션이 Controller에 들어오게 된다. Controller는 액션을 처리는 과정에서 model을 업데이트 하고, model의 결과를 나타내 줄 View를 선택한다. View는 업데이트 된 model을 이용해서 화면을 나타낸다.[2]

 View를 업데이트하기 위해서 Model-View 사이에 의존성이 존재하게 되며, 안드로이드는 Activitycontroller view 모두 처리하기 때문에 한 클래스에서 model-view-controller 모두 처리하게 되는 문제점이 발생한다.

2.1.2 MVP(Model-View-presenter)

 View Model 간 의존성을 줄이기 위해서 presenter를 사용한다. Presenterview model의 데이터 전송자 역할을 담당한다. 사용자 이벤트가 발생하면 View Presenter에 처리 요청을 하고, Presenter Model에 처리 요청 결과를 받아 ViewUI 업데이트 처리를 위임한다. 이로써 View는 수동적으로 UI 관련 로직을 처리하고 Presenter는 데이터 전송자 역할을 수행한다.[4]

 PresenterModel-View 사이에서 관리를 해주기 때문에, MVC 패턴과는 달리 Model-View 사이의 의존성이 없다. 하지만 앱이 커지거나 복잡해질수록 View-Presenter간 의존성이 강해지는 문제점이 발생한다.[3]

2.1.3 MVVM (Model-View-ViewModel)

이미지 출처[4]설명[5]

 

MVVM 아키텍처 패턴은 Model, View, ViewModel의 줄임 말이다. 하나의 소프트웨어를 최대한 기능적으로 작은 단위로 나누어 테스트가 쉽고 큰 프로젝트도 상대적으로 관리하기가 좋은 구조이다.

 MVVM 패턴을 이루는 핵심 기술 요소인 ViewModel, DataBinding, LiveData을 먼저 이해할 필요가 있다.
DataBinding
은 간단하게 layout.xml 안의 뷰 Data들을 class로 연결(binding)해서 사용할 수 있게 도와주는 Android jetpack 라이브러리의 하나의 기능이다. 즉 데이터바인딩을 통해서 findViewById를 사용하지 않아도 되며, ViewModel liveDatalayout.xml에 연결(binding)할 수도 있다.[6] ViewModel 클래스에서는 액티비티에서 사용되는 UI 관련 데이터를 보관하고, 관리하기 위해 디자인 되었다. ViewModel 클래스를 사용하면 화면 회전과 같이 구성을 변경할 때도 데이터를 유지할 수 있다.[7,8]
LiveData
Data의 변경을 관찰할 수 있는 Data Holder 클래스이다. LiveData는 안드로이드의 생명주기(LifeCycle)를 알고 있다. LiveData 객체는 Observer 객체와 함께 사용되어 데이터의 변화가 일어날 경우 등록된 Observer 객체에 변화를 알려주고 ObserveronChanged() 메소드가 실행된다.[6]

 MVVM 패턴은 View가 필요로 하는 데이터를 ViewModel이 가지고 있기 때문에, ViewViewModel이 갖고 있는 데이터를 관찰(Observing)한다. 때문에 MVC 패턴과 다르게, View DB에 직접 접근하는 것이 아닌 UI 업데이트에만 집중한다. 또한 관찰하고 있는 만큼 데이터 변화에 더욱 능동적으로 움직이게 된다.[5]

 

2.2 MVVM의 장점 [9]

1. 뷰가 데이터를 실시간으로 관찰!

 LiveData, Observable 패턴을 이용하기 때문에 데이터베이스를 관찰하고 자동으로 UI를 갱신한다. 직접 뷰를 바꾸어 주는 번거로움도 없으며 데이터와 불일치할 확률이 줄어든다.

2. 생명주기로부터 안전! 메모리 릭 방지!

 뷰 모델을 통해 데이터를 참조하기 때문에 액티비티/프래그먼트의 생명주기를 따르지 않는다. 화면전환과 같이 액티비티가 파괴된 후 재구성되어도 뷰 모델이 데이터를 홀드 하고 있기 때문에 영향을 받지 않는다. 또한 뷰가 활성화 되어있을 경우에만 작동하기 때문에 불필요한 메모리 사용을 줄일 수 있다.

3. 역할 분리! 모듈화!

 UI, 비즈니스 로직, 데이터베이스가 기능별로 모듈화 되어있어서 역할 별로 정리가 깔 끔 유닛 테스트가 한결 용이해질 것이다. (물론 MVVM도 잘 짜야함.)

 모듈화가 잘 되어 있다면 다음과 같은 상황에서 편하게 코딩이 가능할 것 같다.

  - 내장 DB를 통째로 바꾸고 싶다고 할 때, 뷰나 다른 코드에 깊게 종속 돼있지 않기 때문에 DB만 쓱 교체해주면 된다.

  - 뷰모델과 뷰가 1:n 연결이 가능하기 때문에, 뷰모델에 하나의 메소드를 구현해 놓으면 A 액티비티든 B 액티비티든 여러 뷰에서 호출해 재사용하기 편리하다.

 

2.3 Android MVVM의 구현

2.3.1 데이터 바인딩

데이터 바인딩을 사용 설정하려면 아래와 같이 모듈의 build.gradle 파일에서 dataBinding 빌드 옵션을 true로 설정

    //데이터 바인딩 설정.
    buildFeatures {
        dataBinding true
    }

데이터 바인딩은 레이아웃 바인딩을 xml파일 내에서 직접 수행한다.

그러기 위해서 xml 최 상단 루트를 layout으로 감싸주고, 사용할 data, variable 태그를 추가하고 name에는 변수명을, type에는 데이터 바인딩을 통한 이벤트를 세팅할 (내 패키지명 + 액티비티 명 또는 프래그먼트 명 + 모델 명)을 적어주면 됩니다.

<?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"
    xmlns:tools="http://schemas.android.com/tools">
    <data>
        <variable
            name="viewModel"
            type="com.jhs.mymvvm.MainViewModel" />
    </data>


    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <TextView
            android:id="@+id/label"
            android:layout_width="250dp"
            android:layout_height="wrap_content"
            android:text="학점계산기!"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <EditText
            android:id="@+id/etScore"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:ems="10"
            android:text="@={viewModel.myScore}"
            android:hint="점수를 입력하세요."
            app:layout_constraintTop_toBottomOf="@id/label"
            app:layout_constraintStart_toStartOf="@id/label"
            app:layout_constraintEnd_toEndOf="@id/label"/>

        <Button
            android:id="@+id/btn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Button"
            android:onClick="@{() -> viewModel.onClick().invoke()}"
            app:layout_constraintTop_toBottomOf="@id/etScore"
            app:layout_constraintStart_toStartOf="@id/label"
            app:layout_constraintEnd_toEndOf="@id/label" />

        <TextView
            android:id="@+id/tvGrade"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text=""
            app:layout_constraintTop_toBottomOf="@id/btn"
            app:layout_constraintStart_toStartOf="@id/label"
            app:layout_constraintEnd_toEndOf="@id/label"  />


    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

위젯 내에 @{} 구문을 이용하여 데이터 연결.

 

DataBindingUtil.setContentView를 이용하여 Binding 연결.

package com.jhs.mymvvm

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import com.jhs.mymvvm.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    private lateinit var mainViewModel: MainViewModel

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

        binding = DataBindingUtil.setContentView(this@MainActivity, R.layout.activity_main) //데이터바인딩

        mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)//뷰모델
        binding.viewModel =mainViewModel//데이터 바인딩
        observer()


    }

    fun observer() {
        mainViewModel.myGrade.observe(this, Observer {
            binding.tvGrade.text = it.toString()
        })
    }

}

2.3.2 뷰모델 및 라이브 데이터

뷰 모델을 사용하려면 빌드 종속성을 아래와 같이 추가하여야 한다.

// ViewModel - 라이프 사이클

implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0"

// LiveData - 데이터의 변경 사항을 알 수 있음

implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.0"

뷰모델에는 데이터를 유지 보관하기 위한 로직을 아래와 같이 처리한다. 이때 라이브 데이터를 이용한다.

package com.jhs.mymvvm

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

class MainViewModel : ViewModel() {

    val todayLocalSalesAmount = MutableLiveData<String>()
    val todayLocalSalesCount = MutableLiveData<String>() //score //grade

    val myGrade = MutableLiveData<String>()
    val myScore = MutableLiveData<String>("0")
    fun onClick(): () -> Unit {
        return { myGrade.value = GradeModel().getGrade(score = myScore.value.toString().toInt()) }
    }

}

뷰의 구현체인 엑티비티에서는 라이브데이터의 변화를 관찰(observe)하여 ui를 변경한다.

    fun observer() {
        mainViewModel.myGrade.observe(this, Observer {
            binding.tvGrade.text = it.toString()
        })
    }

---용어 정리

관심사의 분리(Separation of Concerns, SoC): 컴퓨터 과학(Computer Science)에서 관심사의 분리란 각 부문이 각자의 관심사를 갖도록 컴퓨터 프로그램을 여러 부문으로 나누는 설계 원칙

onSaveInstanceState(): 화면의 life cycle의 변화에 따라 임시로 데이터를 저장하는 메서드

---참고자료

[1] 신윤권, 최영근.(2020).안드로이드 애플리케이션의 바람직한 MVVM 패턴 설계.한국정보기술학회 종합학술발표논문집,(),184-185.

[2] [Design Pattern] MVC / MVP / MVVM 비교, https://velog.io/@kerri/Design-Pattern-MVC-MVP-MVVM-비교

[3] MVC, MVP, MVVM, MVI https://brunch.co.kr/@oemilk/113

[4] https://www.youtube.com/watch?v=BofWWZE1wts

[5] [Android] 깔쌈하게 MVVM 패턴과 AAC 알아보기, https://velog.io/@haero_kim/Android-깔쌈하게-MVVM-패턴과-AAC-알아보기

[6] DataBinding & LiveData, https://velog.io/@eoqkrskfk94/DataBinding-LiveData

[7] 안드로이드 아키텍처 컴포넌트, ViewModel 이해하기,

https://medium.com/@jungil.han/아키텍처-컴포넌트-viewmodel-이해하기-2e4d136d28d2

[8] ViewModel 개요, https://developer.android.com/topic/libraries/architecture/viewmodel

[9] [Android] MVVM & 안드로이드 아키텍쳐 컴포넌트 시작하기,
 https://blog.yena.io/studynote/2019/03/16/Android-MVVM-AAC-1.html

 

지현승 책임-안드로이드 MVVM-20230713.docx
0.48MB
mywebview-20230105.zip
0.11MB

반응형