개인 광고 영역

GDG 인천 발표 자료를 정리합니다.

수정(2017.02.20) : MVVM 관계 설명 오류 수정

MVC/MVP/MVVM을 각각 정리하고, 이를 kotlin 자료로 정리합니다.

해당 자료의 샘플은 Kotlin/MVP로 작성하였습니다

GDG Incheon 발표 자료


정리 내용


읽기 전에

그간 Android MVP에 대해서 정리하였던 글과 샘플이 있습니다.

이번 발표에서 사용한 샘플은 kotlin으로만 작성한 샘플이며, 기존 글과 샘플은 java/kotlin으로 작성한 샘플입니다.

MVP가 처음이시면 기존 글에서 순차적으로 MVP 적용 방법(java/kotlin으로 작성한 샘플과 함께)을 살펴보시는 걸 추천드립니다.


MVC

MVC의 가장 흔한 이미지입니다.

mvc


이를 flow로 정의하면 다음과 같이 설명할 수 있습니다.

mvc-flow


Model

데이터를 정의하는 부분입니다. 네트워크를 통해 전달받은 데이터, Local Database의 데이터 등 모든 데이터를 정의하게 됩니다.


View

View의 정의는 정리하는 사람마다 다를 수 있습니다. MVC의 View는 저의 경우 아래와 같이 정리합니다.

Activity/Fragment를 포함한 모든 View 정의

위와 같이 정의합니다.

최근 Realm 포스팅인 안드로이드의 MVC, MVP, MVVM 종합 안내서를 참고하시면 View의 정의를 다음과 같이 합니다.

layout(xml)만을 View라고 정의합니다. 그외에 제가 생각하는 Activity/Fragment의 경우에는 Control로 정의합니다.

엄밀히 말하면 View의 정의가 Realm에서 정의하고 있는 내용이 좀 더 맞을 것 같습니다.

단순히 View 그 자체를 뜻합니다.


Control

버튼의 클릭 이벤트 및 사용자의 이벤트들이 발생하는 부분을 말합니다. Android에서는 Activity/Fragment가 이러한 역할을 대신합니다.

Android에서의 MVC 정의 부분을 아래와 같이 정리할 수 있습니다.

여기에서 View를 분리한다고 하면 layout을 View라고 정리가 가능하겠습니다.(Realm 문서 참고)

mvc-android


MVP

MVP는 View와 Presenter가 1:1의 관계를 유지합니다.

그래서 Presenter가 View에 종속적입니다.

종속적이다 보니 1개의 View에는 1개의 Presenter가 따라붙게 됩니다.

mvp


MVP의 flow는 다음과 같이 정의할 수 있습니다.

mvp-flow


Model

MVC의 Model 정의와 동일합니다.

데이터를 정의하는 부분입니다. 네트워크를 통해 전달받은 데이터, Local Database의 데이터 등 모든 데이터를 정의하게 됩니다.


View

MVP의 View는 View + Control입니다.

Android 입장에서는 설명하기 좀 더 쉬운데 onClick과 같은 사용자 이벤트를 받는 Activity/Fragment 등이 모두 View입니다.

주요 역할은 단순히 Presenter에 이벤트를 전달하고, Presenter로부터 받은 이벤트를 통해 View를 갱신만 합니다.

View는 MVC에 비해서 완전한 View가 만들어지게 됩니다.

모델과 완전히 분리되었기 때문에 깔끔하게 callback 형태의 코드가 만들어지고, 호출하는 코드만 남게 됩니다.


Presenter

Presenter는 View와 1:1 관계를 유지하기 때문에 종속적일 수밖에 없습니다.

1개의 View가 생성되면, Presenter 역시 1개가 만들어질 수 있습니다.

이 Presenter가 하는 역할은 View에서 Model의 개입을 중계해주는 역할을 합니다.

그래서 Presenter만이 View의 정보를 알고, View에 어떤 정보를 넘겨줄지도 Presenter가 처리합니다.

소스 코드 상 데이터가 변경되는 상황이 발생하더라도 Presenter에서 이러한 부분을 최소한으로 줄여주는게 좋습니다.


MVVM

MVVM은 ReactiveX 등을 사용하여 ViewModel을 생성합니다.

이는 Presenter와는 다르게 종속적이지 않도록 만들고, View에 대한 Model 정의를 하기 위함입니다.

mvvm

위와 같이 View (View) N : 1 (ViewModel) 관계를 가집니다.

이는 View는 ViewModel에 대한 참조를 가지지만, 반대로 ViewModel은 View에 대한 참조를 가지지 않기에 가능합니다.

이것이 다양한 View와 하나의 ViewModel간의 N:1 사용이 가능한 이유입니다.

MVVM을 사용하는 간단한 샘플은 아래와 같을 수 있습니다.

class SampleViewModel {
  val viewModelTest = PublishSubject.create()
  // 생략...
}

class SampleActivity : AppCompactActivity() {
  val sampleViewModel = SampleViewModel()
  // 생략

  fun test() {
    sampleViewModel.viewModelTest
      .subscribe {
        // ...
      }
  }
}


ViewModel이 유용한 경우

  • 다양한 View(화면 기준)에서 동일한 ViewModel을 필요로 하는 경우
  • 종석적이지 않은 관계를 위해서 ReactiveX를 함께 사용

예) 로그인/로그아웃

  • 로그인과 로그아웃의 경우에는 다양한 View에서 호출될 수 있습니다.
  • ViewModel을 통해서 로그인/로그아웃을 분리해둔다면 필요로 하는 화면에서 이를 ReactiveX를 통해 접근하고, 갱신할 수 있습니다.


Model

MVC의 모델/MVP의 모델 모두 동일합니다.

데이터를 정의하는 부분입니다. 네트워크를 통해 전달받은 데이터, Local Database의 데이터 등 모든 데이터를 정의하게 됩니다.


View

MVP의 View와 동일합니다.

하지만 ViewModel을 여러 개 불러다 사용할 수 있습니다.

Android에서는 ReactiveX를 통해서 이를 구현하게 됩니다.

ReactiveX에서 이벤트를 받아오면, 이를 통해 View 갱신을 합니다.


ViewModel

Presenter와 동일한 역할을 합니다.

다만 ViewModel이기 때문에 View 별로 Model을 구현해줄 수 있습니다.

이 경우는 위에서 정의하였듯 로그인/로그아웃에 대한 ViewModel을 별도로 정의하는게 좋겠습니다.


ViewModel을 참고하려면

ReactiveX를 통해 ViewModel을 정의합니다. 그렇게 해야 MVP에서 발생하는 종속적인 관계를 끊을 수 있게 됩니다.


Architecture 적용

구글에서는 Android Architecture Blueprints를 제공합니다.

Android Architecture의 미래를 정리하는 중입니다.

Android MVP를 통해서 이를 하고 있지만… 하다 보니 MVVM처럼 엮어서 사용할 수도 있습니다.

좀 자유롭게 MVP를 작성하되 ViewModel의 형태를 차용하여도 문제없습니다.

aab-logo

저는 Android Architecture를 차용하여 MVP를 사용하고 있습니다.

Android Base를 아래와 같이 구현하여 사용하고 있습니다.

er-diagram


이런 Architecture 적용했을 때의 장점

  • 코드 복잡도를 줄입니다. 그와 함께 스파게티 코드도 미리 방지할 수 있습니다.
  • 명확성을 높일 수 있습니다.
  • 결국 유지 보수가 편해야 합니다.
    • 깔끔한 코드와 수정 구간을 줄임으로써 이런 장점을 가질 수 있습니다.
  • 테스트 가능한 형태의 코드 작성이 가능합니다.
    • 자주 변경되는 UI를 제외하려는 시간은 필요로 합니다.
  • 레이어를 분리하여 깔끔한 코드 작성을 하는 기초가 됩니다.


Presenter 적용하는 방법

크게 3가지로 분리할 수 있습니다.


Kotlin 샘플

kotlin + MVP로 작성하였으며, kotlin base 만들어둔 라이브러리를 사용합니다.

작성한 샘플은 아래와 같습니다.

sample


샘플 코드에 적용한 라이브러리


주요 코드 - LoginContract

Contract 정의는 아래와 같습니다.

LoginActivity에서 사용하는 모든 구현 부를 정의합니다.

interface LoginContract {

    interface View : BaseView {
        fun showProgress()
        fun hideProgress()
        fun updatePasswordError(string: String)
        fun updateEmailError(string: String)
        fun failLoginError(string: String)
        fun successLogin(email: String)
    }

    interface Presenter : BasePresenter<View> {
        var context: Context
        var loginRepository: LoginRepository
        // 사용자 로그인
        fun loginUser(email: Editable, password: Editable)
        // 사용자 로그아웃
        fun logout()
    }
}


주요 코드 - LoginActivity

BasePresenterActivity에서 onCreatePresenter가 오게 되면 이를 생성만 하면 됩니다.

그 외에는 presenter로부터 콜백을 받는 LoginContract.View를 상속받아서 구현하였습니다.

class LoginActivity : BasePresenterActivity<LoginContract.View, LoginContract.Presenter>(), LoginContract.View {

    // Presenter 생성
    override fun onCreatePresenter() = LoginPresenter()

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

        presenter?.context = this
        presenter?.loginRepository = LoginRepository

        password.setOnEditorActionListener { textView, i, keyEvent ->
            if (i == R.id.login || i == EditorInfo.IME_NULL) {
                login()
                return@setOnEditorActionListener true
            }
            false
        }

        email_sign_in_button.setOnClickListener {
            login()
        }

        btn_logout.setOnClickListener {
            Toast.makeText(this, "Logout", Toast.LENGTH_SHORT).show()
        }
    }

    private fun login() {
        presenter?.loginUser(password = password.text, email = email.text)
    }

    // 상속 함수 생략
}


주요 코드 - LoginPresenter

LoginPresenter는 LoginContract.Presenter를 상속받아서 구현합니다.

class LoginPresenter : LoginContract.Presenter, AbstractPresenter<LoginContract.View>() {

    // Context, Model(LoginRepository)를 구현합니다.
    override lateinit var context: Context
    override lateinit var loginRepository: LoginRepository

    // RxJava를 활용하여 throttleFirst를 처리합니다.
    private val loginSubject: PublishSubject<TempData> = PublishSubject.create()
    private lateinit var loginDisposable: Disposable

    init {
        initLogin()
    }

    private fun initLogin() {
        loginDisposable = loginSubject
                .throttleFirst(1, TimeUnit.SECONDS, AndroidSchedulers.mainThread())
                .subscribe(
                        { validLogin(it.email, it.password) },
                        { view?.failLoginError(context.getString(R.string.fail_login)) }
                )
    }

    private fun validLogin(email: Editable, password: Editable) {
        // 생략
        view?.showProgress()

        // email, password를 불러와서 로그인 사용자 체크를 진행합니다.
        loginRepository.dumpLogin(User(email.toString(), password.toString()))
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .filter {
                    if (!it) {
                        view?.failLoginError(context.getString(R.string.error_field_required))
                        return@filter false
                    }
                    it
                }
                .doOnComplete {
                    view?.hideProgress()
                }
                .subscribe {
                    view?.successLogin(email.toString())
                }
    }

    // View에서 이벤트를 전달 받는다
    override fun loginUser(email: Editable, password: Editable) {
        loginSubject.onNext(TempData(email, password))
    }

    // 생략
}


주요 코드 - Model

class LoginLocalDataSource : LoginSource {

    /**
     * A dummy authentication store containing known user names and passwords.
     */
    val DUMMY_CREDENTIALS: List<User> = mutableListOf(User("test@test.com", "1234"), User("test2@test.com", "4321"))

    override fun dumpLogin(user: User): Flowable<Boolean> {
        return Flowable.just(user)
                .delay(2000, TimeUnit.MILLISECONDS)
                .map {
                    Flowable.fromIterable(DUMMY_CREDENTIALS)
                            .map {
                                it.email == user.email && it.password == user.password
                            }
                            .filter {
                                it
                            }
                            .count()
                }
                .map {
                    it.blockingGet() == 1L
                }
    }
}


마무리

발표 자료를 정리하였습니다. 부족한 부분을 포함해서 전체 내용을 갱신합니다.

감사합니다.


Android MVP 무작정 따라하기

그간 Android MVP에 대해서 정리하였던 글과 샘플이 있습니다.

이번 발표에서 사용한 샘플은 kotlin으로만 작성한 샘플이며, 기존 글과 샘플은 java/kotlin으로 작성한 샘플입니다.

MVP가 처음이시면 기존 글에서 순차적으로 MVP 적용 방법(java/kotlin으로 작성한 샘플과 함께)을 살펴보시는 걸 추천드립니다.


개인 광고 영역

Tae-hwan

Android, Kotlin .. Create a content development.