ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Android] MVVM 패턴 적용해보며 배우기(1) - ACC, MVC와 MVVM비교, MVVM 장점
    Client/Android 2020. 4. 7. 01:25

    🔥목차🔥

    🍓 안드로이드 아키텍처 컴포넌트 (AAC)

    🍓 MVC 와 MVVM 차이점

    🍓 MVVM 패턴

    🍓 MVVM 장점

    🐥 실습 - MVVM 패턴, Repository, BataBinding 적용

     

    이어지는 시리즈

    👉 [Android] MVVM 패턴 적용해보며 배우기(2) - RxJava2

     

     

    안드로이드 아키텍쳐 컴포넌트

    • 안드로이드 아키텍쳐 컴포넌트(Android Architecture Components, AAC)

    • 라이브러리 모음

      • 앱 구조를 더 튼튼하도록

      • 테스트에 용이하도록

      • 유지 보수성이 뛰어나도록

    • 모듈화된 코딩을 돕기 위해 Databinding, LiveData, ViewModel 등의 유용한 라이브러리를 제공

      이러한 라이브러리의 모음은 MVVM 패턴의 구조의 설계에 최적화되어 있다.

    View - 뷰

    • UI Controller를 담당하는 Activity, Fragment이다.

    • 화면에 무엇을 그릴지 결정하고, 사용자와 상호작용한다.

    • 데이터의 변화를 감지하기 위한 옵저버를 가지고 있다.

    ViewModel - 뷰모델

    • 뷰모델은 UI를 위한 데이터를 가지고 있다.

    • 구성(configuration)이 변경되어도 살아남는다. (예를 들어 화면 회전이라던가, 언어 변경 등)

    • AsyncTask는 액티비티나 프래그먼트의 생명 주기에서 자유로울 수 없지만,

      뷰모델은 뷰와 분리되어 있기 때문에 액티비티가 Destroy 되었다가 다시 Create 되어도 종료되지 않고 데이터를 여전히 가지고 있다.

    LiveData - 라이브데이터

    • 관찰이 가능한(Observable) 데이터 홀더 클래스이다.
    • 뷰에서 뷰모델의 라이브데이터를 관찰하게 되면 데이터가 변경될 때 내부적으로 자동으로 알려주게 된다.
    • 액티비티나 프래그먼트의 생명 주기를 인지한다. 즉, 액티비티가 화면 위에 활성화되어 있을 때에만 UI변경 등의 기능을 동작하게 되고, Destroy 된 상태에서는 동작하기 않기 때문에 메모리 릭의 발생을 줄여준다.

    Repository - 리포지토리

    • 뷰모델과 상호작용하기 위해 잘 정리된(Clean) 데이터 API를 들고 있는 클래스이다.

    • 앱에 필요한 데이터 (내장DB or 외부DB) 를 가져온다.

      뷰모델은 DB나 서버에 직접 접근하지 않고, 리포지토리에 접근하는 것으로 앱의 데이터를 관리한다.

    Room - 룸

    • SQLite 데이터베이스를 편하게 사용하게 해주는 라이브러리이다.
    • SQLite의 코드를 직접 작성하는 경우, 직접 테이블을 Create 하거나 쿼리문을 일일이 변수를 통해 작성해주어야 했지만, Room을 쓰면 조금 더 직관적이고 편리하게 DB를 사용할 수 있다.

    아키텍처 적용 전의 문제점

    액티비티에 기능을 붙이다보면 액티비티가 무거워지거나 혹은 종속성이 너무 강해

    테스트가 힘들고 유지보수가 어려워진다.

    MVVM 패턴

    View - ViewModel - Model 을 이용해 각각의 역할을 분리하여

    가독성과 재사용성을 높인 디자인 패턴으로 종속성 및 유지보수의 어려움을 해소해준다.

    MVC와 MVVM 차이점

    img

    • MVC (Model - View - Controller)

      • Controller

        Activity 부분에서 View에게는 화면 업데이트, Model에게는 데이터 갱신을 알리며

        View와 Mdel을 연결해주며 비즈니스 로직을 처리하기 위해 많은 일을 해야한다.

      • View

        activity_main.xml 화면 부분으로 UI 역할 담당한다.

      • Model
        Model class로 비즈니스 로직에서의 알고리즘, 데이터 드의 기능 처리한다.

    • MVVM (View - ViewModel - Model)

      • View

        Activity 역할을 담당하고 UI를 갱신하는 역할에만 충실히 한다.
        ​ 뷰모델을 관찰(Observe) 한다.
        ​ 따라서, 데이터의 변화를 알아차리고 자동으로 화면을 갱신할 수 있다.

      • ViewModel

        Model에게 데이터 갱신 처리를 요청하고 잘 정리된 데이터를 참조한다.

      • Model

        Repository, DataBase 부분으로 데이터 처리 역할을 한다.

    MVVM 장점

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

    LiveData (=Observable 패턴) 을 이용해 DB를 관찰하고 자동으로 UI를 갱신한다.

    직접 뷰를 바꾸어주는 번거로움을 없애고 데이터와 불일치할 확률이 줄인다.

    2. 생명주기로부터 안전하여 메모리 릭을 방지할 수 있다

    뷰모델을 통해 데이터를 참조하기 때문에 액티비티/프래그먼트의 생명주기를 따르지 않는다.

    ​ => 화면전환과 같이 액티비티가 파괴된 후 재구성 되어도 뷰모델이 데이터를 홀드하고 있기 때문에

    ​ 영향을 받지 않는다.

    뷰가 활성화되어있을 경우에만 작동하기 때문에 불필요한 메모리 사용을 줄일 수 있다.

    *3. 기능별로 모듈화되어 있어 역할 분리를 할 수 있고 유닛 테스트에 한결 용이해진다. *

    • 내장 DB를 통째로 바꾸고 싶다고 할 때, 뷰나 다른 코드에 깊게 종속돼있지 않아 DB 교체가 쉽다.

    • 뷰모델과 뷰가 1:n 연결이 가능하다.

      따라서, 뷰모델에 하나의 메소드를 구현해 놓으면

      A 액티비티든 B 액티비티든 여러 뷰에서 호출해 재사용하기 편리하다.

    실습 - MVVM 패턴, Repository, DataBinding 적용

    얼리버디 프로젝트에 MVVM 아키텍처를 적용해 공부한 내용을 익혀보려 한다.

    SignupActivity.kt

    var vm: SignupViewModel = SignupViewModel()
    
    lateinit var binding : ActivitySignupBinding
    
    override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            //setContentView(R.layout.activity_signup)
    
            binding = DataBindingUtil.setContentView(this, R.layout.activity_signup)
            makeController()
    
            binding.signupActivity = this
    
            addObserverableData()
        }
    
    ...
    
    fun communication(id: String, pw: String) {
            var jsonObject = JSONObject()
            jsonObject.put("userId", id)
            jsonObject.put("userPw", pw)
    
    
            val body = JsonParser().parse(jsonObject.toString()) as JsonObject
            vm.viewCommunicate(body)
        }
    
         fun click() {
             communication(id, pw)
        }
    
        private fun View.showOrInvisible(show: Boolean) {
            visibility = if (show) {
                View.VISIBLE
            } else {
                View.INVISIBLE
            }
        }
    
    
    
        private fun idDuplicate(){
            act_signup_tv_id_ment.showOrInvisible(true)
    
        }
    
        private fun addObserverableData(){
            vm.isSuccessNetwork.observe(
                this, Observer {
                    if(it){
                        val intent = Intent(this@SignupActivity, SignupSuccessActivity::class.java)
                        startActivity(intent)
                    }else{
                        idDuplicate()
                    }
                    }
            )
    
            vm.wifiDisconnect.observe(
                this, Observer {
                    val intent = Intent(this@SignupActivity, SignupFailActivity::class.java)
                    startActivity(intent)
                }
            )
        }
    
    }
    
    
    

    SignupViewModel.kt

    class SignupViewModel : ViewModel() {
        val signupRepository = SignupRepository()
    
        internal val disposables = CompositeDisposable()
    
        val isSuccessNetwork = MutableLiveData<Boolean>()
        val wifiDisconnect = MutableLiveData<Unit>()
    
        fun viewCommunicate(jsonObject: JsonObject) {
    
            val body = JsonParser().parse(jsonObject.toString()) as JsonObject
            Log.d("test", "postUserData body : " + body)
    
            signupRepository.signUp(body = body,
                onResponse = {
                    //고차함수로 구현, it으로 response에 바로 접근 가능
                    if (it.isSuccessful) {
                        val intent = Intent(this, SignupSuccessActivity::class.java)
                        startActivity(intent)
                        isSuccessNetwork.value = true
    
                    } else { //아이디 중복
                        isSuccessNetwork.value = false
    
                        Log.d("test", "아이디중복: " + it.message())
                    }
    
                }, onFailure = {
                    //고차함수로 구현, it으로 t에 바로 접근 가능
                    Log.d("test", "통신 실패 error : " + it.toString())
                    wifiDisconnect.value = Unit
                    val intent = Intent(this@SignupActivity, SignupFailActivity::class.java)
                    startActivity(intent)
                }
            )
        }
    }

    SignupRepository.kt

    class SignupRepository{
        val retrofitRemoteDataSource: RemoteDataSource = RemoteDataSourceImpl() //인스턴스 생성
        fun signUp(
            jsonObject: JsonObject,
            onResponse: (Response<PostSignupData>) -> Unit,
            onFailure: (Throwable) -> Unit
        ){
            //생성한 인스턴스로 RempteDataSourceImpl에 구현해놓은 함수 실행
            retrofitRemoteDataSource.signUp(jsonObject, onResponse, onFailure)
        }
    }

    PostSignupData.kt

    data class PostSignupData (
        @SerializedName("status")
        val status: Int,
        @SerializedName("data")
        val idx: Int,
        @SerializedName("message")
        val message: String
    )

    AndBuddyService.kt

    interface AndBuddyService {
        @POST("/users/signup")
        fun postSignupUser(
            @Body() body: JsonObject
        ): Call<PostSignupData>
    }

    AndBuddyServiceImpl.kt

    object AndBuddyServiceImpl {
        private const val BASE_URL = "http://서버주소:3456/"
    
        private val okHttpClient: OkHttpClient =
            OkHttpClient.Builder().addInterceptor(CookiesIntercepter())
                .addNetworkInterceptor(CookiesIntercepter()).build()
    
        private val retrofit: Retrofit =
            Retrofit.Builder().baseUrl(BASE_URL).client(
                okHttpClient
            )
                .addConverterFactory(GsonConverterFactory.create())
                .build()
    
        val service: AndBuddyService = retrofit.create(
            AndBuddyService::class.java)
    }

    RemoteDataSource.kt

    interface RemoteDataSource { //필요한 함수들을 모아서 정의하는 인터페이스
        fun signUp(jsonObject: JsonObject, //post에 필요한 값을 보낼 body
                   onResponse: (Response<PostSignupData>) -> Unit, //통신 성공시 수행할 함수
                   onFailure: (Throwable) -> Unit) //통신 실패시 수행할 함수
    }

    RetmoteDataSourceImpl.kt

    class RemoteDataSourceImpl : RemoteDataSource { //RemoteDataSource를 상속받는 클래스
        override fun signUp(
            jsonObject: JsonObject,
            onResponse: (Response<PostSignupData>) -> Unit,
            onFailure: (Throwable) -> Unit
        ) {
            AndBuddyServiceImpl.service.postSignupUser(jsonObject).enqueue(object : //enqueue 함수 실행
                Callback<PostSignupData> {
                override fun onFailure(call: Call<PostSignupData>, t: Throwable) { //통신 실패시 실행되는 함수 구현
                    onFailure(t)
                }
    
                override fun onResponse( //통신 성공시 실행되는 함수 구현
                    call: Call<PostSignupData>,
                    response: Response<PostSignupData>
                ) {
                    onResponse(response)
                }
            })
        }
    }

    build.gradle (Module: app)

    android {
    
        ...
    
        dataBinding {
            enabled = true
        }
    
    }
    
    dependencies {
    
        ...
    
        //Retrofit 라이브러리 : https://github.com/square/retrofit
        implementation 'com.squareup.retrofit2:retrofit:2.6.2'
        //Retrofit 라이브러리 응답으로 가짜 객체를 만들기 위해
        implementation 'com.squareup.retrofit2:retrofit-mock:2.6.2'
    
        //객체 시리얼라이즈를 위한 Gson 라이브러리 : https://github.com/google/gson
        implementation 'com.google.code.gson:gson:2.8.6'
        //Retrofit 에서 Gson 을 사용하기 위한 라이브러리
        implementation 'com.squareup.retrofit2:converter-gson:2.6.2'
    
        //okHttp
        implementation 'com.squareup.okhttp3:logging-interceptor:3.8.1'
        implementation 'com.squareup.okhttp3:okhttp:3.8.1'
    
        //리사이클러뷰 라이브러리
        implementation 'androidx.recyclerview:recyclerview:1.1.0-alpha06'
      }

    통신을 위한 퍼미션을 주기 위해 2가지를 추가해줘야한다.

    1) AndroidManifest.xml

    해당 코드를 추가해줘야 한다.

     <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
     <uses-permission android:name="android.permission.INTERNET" />

    2) CookiesIntercepter.kt

    class CookiesIntercepter : Interceptor {
        override fun intercept(chain: Interceptor.Chain): Response {
            val request =
                chain.request().newBuilder().header("Content-Type", "application/json")
                    .build()
            return chain.proceed(request)
        }
    }

    참고 [Android] MVVM Part.2

    해당 블로그에 설명이 매우 잘 되어있어서 공부 후 정리하는데 많은 도움이 되었다.

    반응형
Designed by Tistory.