Overview MVC패턴의 장단점 MVVM 패턴 LiveData Model 예제 LiveData Model AAC ViewModel 예제 AAC ViewModel DI (Dependency Injection) 예제 DI (Dependency Injection) Koin 예제 Koin Koin ViewModel 예제 Koin ViewModel Conclusion 참고

Overview

2014년부터 브랜디와 하이버를 홀로 개발하기 시작하여, Android 브랜디 소스를 개발한지 5년이 지나고 있습니다. 2018년에 드디어 Android 개발 인원이 충원되었고, 현재는 Android 개발 파트 3명이서 협업으로 개발하고 있습니다.

지난 시간 동안 브랜디는 빠른 속도의 개발이 최우선이었고, 기존 아키텍처의 변경을 최소한의 선에서 변경을 해왔습니다. RxJava, Kotlin, AndroidX 등을 적용했지만, 소스의 관리와 인원 배분의 문제는 많이 줄지 않았습니다. 왜냐하면 기존 소스는 MVC 패턴에서 크게 변경하지 못했기 때문입니다. View와 Controller의 경계선이 모호하고, 지속적인 앱 개발로 인해 불필요한 소스와 현재 사용되는 소스를 명확하게 구별하기도 쉽지 않아 기존 개발자가 아니라면 접근성이 매우 불편했습니다. 이러한 불편을 개선하여 신규 개발자와의 협업에 용이한 MVVM 패턴을 도입하고자 합니다.

MVC 패턴의 장단점

그렇다면 어째서 기존에 MVC 패턴으로 브랜디를 만들었는지 생각해 보겠습니다.

장점은 너무 명확했습니다. 브랜디 개발 초창기, 빠른 개발과 수정을 하기 편한 패턴이었기 때문입니다. 스타트업 초창기 빠른 개발 속도의 대응, 적은 인원 혹은 혼자서 개발을 해야 했기 때문에 MVC를 선택하였고, 그 당시에는 나쁜 선택이 아니었습니다.

하지만 현재 브랜디가 어느 정도 궤도에 올라오고, 개발 볼륨 자체가 커진 플랫폼 서비스로 오면서 MVC 패턴을 사용할수록 장점보다 단점이 더 커졌습니다. View와 Controller의 의존성이 높아짐에 따라 작은 소스 변경도 조심스럽고 로직을 확인하기 위한 TDD도 진행하기 쉬운 구조가 아니었습니다.

커진 서비스만큼 개발자의 작은 실수도 서비스에 큰 영향을 끼치는 수준에 다다랐습니다.

MVVM 패턴

그리고 현재 서로 다른 실력의 3명의 개발자가 서로의 난이도를 고려하여 개발을 할 수 있도록 모듈 간의 의존성이 낮은 MVVM 패턴을 도입하기로 결정하였습니다.

01

LiveData Model

View (UI), ViewModel (비즈니스 로직), Model (데이터)로 분리가 되며, 데이터를 주고받는 역할은 LiveData로 처리가 될 것입니다. Observe 패턴 + LifeCycle을 같이 관리할 수 있는 AAC(Android Architecture Components)입니다.

01

위 표와 같이 Activity의 라이프 사이클에 따라 Observer Update가 달라집니다.
Activity가 백그라운드로 진입할 경우 LiveData의 postValue(C), (D)는 수신되지 않습니다.

예제 LiveData Model

ACC (Android Architecture Components)의 기본적인 LiveData Model을 구현하는 기초 소스입니다.

ExampleData.kt

class ExampleData {
    var value: Int = 0
}

MainActivity.kt

class MainActivity : AppCompatActivity() {
    val TAG: String = this.javaClass.simpleName

    //변경할 수 있는 LiveData
    private val _exampleLiveData: MutableLiveData<ExampleData> = MutableLiveData()

    //Observe 를 이용한 데이터 수신을 위한 LiveData
    val exampleData: LiveData<ExampleData>
        get() = _exampleLiveData


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

        //LiveData 에 콜백 등록
        exampleData.observe(this, Observer {
            Log.v(TAG, "${it.value}")
        })

        //송신 데이터 생성
        val exampleData = ExampleData().apply {
            value = 10
        }

        //MutableLiveData 에 송신 데이터 전달
        _exampleLiveData.postValue(exampleData)
    }
}

MVVM에서 데이터를 주고받는 기본적인 단위인 LiveData을 확인해 보았습니다.

AAC ViewModel

AAC에서 제공하는 ViewModel 클래스는 수명 주기를 고려하여 UI 관련 데이터를 저장하고 비즈니스 로직을 관리하도록 설계되었습니다.

01

예제 AAC ViewModel

LiveData를 AAC ViewModel를 이용하여 비즈니스 로직을 분리시키는 예제입니다.

ExampleViewModel.kt

class ExampleViewModel : ViewModel() {
    val TAG = this.javaClass.simpleName

    //변경할 수 있는 LiveData
    private val _exampleLiveData: MutableLiveData<ExampleData> = MutableLiveData()

    //Observe 를 이용한 데이터 수신을 위한 LiveData
    val exampleData: LiveData<ExampleData>
        get() = _exampleLiveData

    fun requestData() {
        //송신 데이터 생성
        val exampleData = ExampleData().apply {
            value = 10
        }

        _exampleLiveData.value = exampleData
    }

    //ViewModel를 가지고 있는 Activity 혹은 Fragment 가 종료될 경우 ViewModel 이 정리된다.
    override fun onCleared() {
        super.onCleared()
        Log.v(TAG, ">>> onCleared")
    }
}

MainActivity.kt

class MainActivity : AppCompatActivity() , ViewModelStoreOwner {
    val TAG: String = this.javaClass.simpleName

    lateinit var exampleViewModel: ExampleViewModel

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

        //AAC ViewModel 생성
        val androidViewModelFactory = ViewModelProvider
                                    .AndroidViewModelFactory
                                    .getInstance(application)
        exampleViewModel = ViewModelProvider(this, androidViewModelFactory)
                                    .get(ExampleViewModel::class.java)

        //LiveData 에 콜백 등록
        exampleViewModel.exampleData.observe(this, Observer {
            Log.v(TAG, ">>> ${it.value}")
        })

        exampleViewModel.requestData()
    }
}

AAC ViewModel 객체를 생성하는 방법과 비즈니스 로직을 분리하여 View에서 요청과 응답을 받는 부분을 분리하는 예제를 확인했습니다.

DI (Dependency Injection)

DI (Dependency Injection)는 의존성 주입을 의미합니다. 의존성이란 클래스간 객체로 참조가 되는 경우를 말합니다. MVVM 모델은 이미 완성했지만, 모듈 간 독립성과 TDD가 용이한 구조로 만들기 위해서 DI에 대해서 알아보도록 하겠습니다.

예제 DI (Dependency Injection)

의존성이 무엇인지 대한 간단한 예제입니다.

SessionData.kt

class SessionData {
    val sessionId: String

    constructor() {
        this.sessionId = "abcd"
    }
}

ExampleData.kt

class ExampleData {
    var value: Int = 0
    val sessionData: SessionData

    init {
        sessionData = SessionData()
    }
}

위처럼 ExampleData 클래스는 내부에서 SessionData 클래스를 참조하고 있습니다.

ExampleData 클래스는 SessionData 클래스에 의존성을 갖고 있습니다. 이렇게 생긴 의존성은 SessionData 클래스 생성자가 바뀌게 된다면 참조하고 있는 클래스는 매번 변경되어야 합니다.

DI를 통해 이와 같은 의존성을 해결함으로써 종속된 코드와 결합도를 낮추면서 클래스의 유연성과 확장성을 동시에 확보할 수 있습니다.

Koin

Koin은 DI 라이브러리로 Android에서 DI를 사용하기 위한 기본적인 기능을 손쉽게 제공합니다.

예제 Koin

Koin을 이용한 의존성 주입의 예제입니다.

app.glade

dependencies {
    // Koin for Kotlin
    implementation "org.koin:koin-core:2.1.0-alpha-7"
    // Koin extended & experimental features
    implementation "org.koin:koin-core-ext:2.1.0-alpha-7"
    // Koin for Unit tests
    testImplementation "org.koin:koin-test:2.1.0-alpha-7"


    // Koin AndroidX Scope features
    implementation "org.koin:koin-androidx-scope:2.1.0-alpha-7"
    // Koin AndroidX ViewModel features
    implementation "org.koin:koin-androidx-viewmodel:2.1.0-alpha-7"
    // Koin AndroidX Fragment features
    implementation "org.koin:koin-androidx-fragment:2.1.0-alpha-7"
    // Koin AndroidX Experimental features
    implementation "org.koin:koin-androidx-ext:2.1.0-alpha-7"
}

PackageRepository.kt

class PackageRepository(context: Context) {
    val packageName: String

    init {
        packageName = context.packageName
    }
}

PrintService.kt

class PrintService (val packageRepository: PackageRepository) {
    fun printHello() {
        Log.v("PrintService", "Hello ${packageRepository.packageName}")
    }
}

InjectCountData.kt

class InjectCountData {
    companion object {
        var injectCount = 0
    }

    init {
        injectCount++
    }

    fun printCount() {
        Log.v("InjectCountData", "injectCount $injectCount")
    }
}

MainApplication.kt

class MainApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        //application 실행시 startKoin을 실행한다.
        startKoin {
            androidLogger()
            androidContext(this@MainApplication)
            modules(myModule)
        }
    }
}

val myModule = module {

    //싱글톤 구조
    single {
        //context 를 파라미터로 생성하는 PrintServer 클래스
        PackageRepository(androidContext())
    }

    single {
        //PackageRepository 객체로 생성되는 PrintService 클래스
        //get() 생성에 필요한 파라미터를 찾아서 주입힌다.
        //PrintService (val packageRepository: PackageRepository)
        PrintService(get())
    }

    //주입 할 때마다 InjectCountData 객체 생성
    factory {
        InjectCountData()
    }

}

ExampleViewModel.kt

class ExampleViewModel : ViewModel(),
    //Activity, Fragment 가 아닌 클래스에서 주입받기 위해서 KoinComponent 를 상속받는다.
    KoinComponent {
    val TAG = this.javaClass.simpleName

    private val _exampleLiveData: MutableLiveData<ExampleData> = MutableLiveData()

    //PrintService를 생성자 없이 inject로 주입힌다.
    val printService: PrintService by inject()

    val exampleData: LiveData<ExampleData>
        get() = _exampleLiveData

    fun requestData() {
        val exampleData = ExampleData().apply {
            value = 10
        }

        _exampleLiveData.value = exampleData
    }

    //ViewModel를 가지고 있는 Activity 혹은 Fragment 가 종료될 경우 ViewModel 이 정리된다.
    override fun onCleared() {
        super.onCleared()
        Log.v(TAG, ">>> onCleared")
    }

    fun printHello() {
        printService.printHello()
    }
}

MainActivity.kt

class MainActivity : AppCompatActivity() , ViewModelStoreOwner {
    val TAG: String = this.javaClass.simpleName

    lateinit var exampleViewModel: ExampleViewModel

    //inject 로 생성자 없이 주입한다.
    val inject_0 by inject<InjectCountData>()
    val inject_1 by inject<InjectCountData>()

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

        //AAC ViewModel 생성
        val androidViewModelFactory = ViewModelProvider.AndroidViewModelFactory.getInstance(application)
        exampleViewModel = ViewModelProvider(this, androidViewModelFactory).get(ExampleViewModel::class.java)

        //LiveData 에 콜백 등록
        exampleViewModel.exampleData.observe(this, Observer {
            Log.v(TAG, ">>> ${it.value}")
        })

        exampleViewModel.requestData()
        exampleViewModel.printHello()

        inject_0.printCount()
        inject_1.printCount()
    }
}

위 예제를 통해 어떻게 의존성을 주입하는지 간단하게 확인할 수 있습니다. 또한 Koin DLS의 single, factory, get을 확인할 수 있습니다.

  • DSL 키워드
    • module : Koin 모듈 정의 블록
    • factory : inject 주입하는 시점에 해당 객체를 생성
    • single : Singleton 객체 주입
    • get : 주입해야 하는 컴포넌트들의 의존성을 처리

Koin ViewModel

Koin을 이용한다면 AAC ViewModel을 Koin ViewModel로 변경하여 의존성 주입을 이용할 수 있습니다.

예제 Koin ViewModel

Koin ViewModel을 이용한 의존성 주입의 예제입니다.

MainApplication.kt

class MainApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        startKoin {
            androidLogger()
            androidContext(this@MainApplication)
            modules(myModule, myViewModel)  //myViewModel 추가

        }
    }
}

//viewModel을 주입하기 위한 module
val myViewModel = module {
    //vieModel 정의
    viewModel {
        ExampleViewModel(get()) //PrintService를 get()으로 주입
    }
}

val myModule = module {
    single {
        PackageRepository(androidContext())
    }

    single {
        PrintService(get())
    }

    factory {
        InjectCountData()
    }

}

ExampleViewModel.kt

class ExampleViewModel
    //KoinComponent 를 사용한 주입이 아닌 생성자를 통해서 주입
    (val printService: PrintService)
    : ViewModel() {

    val TAG = this.javaClass.simpleName

    private val _exampleLiveData: MutableLiveData<ExampleData> = MutableLiveData()

    val exampleData: LiveData<ExampleData>
        get() = _exampleLiveData

    fun requestData() {
        val exampleData = ExampleData().apply {
            value = 10
        }

        _exampleLiveData.value = exampleData
    }


    fun printHello() {
        printService.printHello()
    }

    override fun onCleared() {
        super.onCleared()
        Log.v(TAG, ">>> onCleared")
    }
}

MainActivity.kt

class MainActivity : AppCompatActivity() {
    val TAG: String = this.javaClass.simpleName

    //Koin을 이용한 ViewModel 주입
    val exampleViewModel: ExampleViewModel by viewModel()
    val inject_0 by inject<InjectCountData>()
    val inject_1 by inject<InjectCountData>()

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

        //AAC ViewModel 생성
        /*
        val androidViewModelFactory = ViewModelProvider
                            .AndroidViewModelFactory
                            .getInstance(application)
        exampleViewModel = ViewModelProvider(this, androidViewModelFactory)
                            .get(ExampleViewModel::class.java)
        */

        //LiveData 에 콜백 등록
        exampleViewModel.exampleData.observe(this, Observer {
            Log.v(TAG, ">>> ${it.value}")
        })

        exampleViewModel.requestData()
        exampleViewModel.printHello()

        inject_0.printCount()
        inject_1.printCount()

    }

}

AAC ViewModel을 Koin ViewModel로 변경하는 것부터 MVVM + Koin을 이용한 의존성 주입까지 알아보았습니다. MVVM의 장점과 의존성 주입을 더하여 생성자를 통한 객체 생성이 아닌 의존성 주입을 통해서 모듈 간의 독립성을 확보할 수 있는 구조를 만들어 보았습니다.

Conclusion

기존 소스를 리펙토링 하는 작업은 언제나 쉬운 일이 아닙니다. 서비스 애플리케이션 업데이트 일정을 진행하면서, 2019년 여름부터 하이버 MVVM 일정도 진행하였습니다. 이제 2020년 기점으로 MVVM + Koin을 하이버에 반영 완료할 예정입니다. 준비가 길었던 만큼 좋은 결과를 얻을 수 있도록 2020년에도 멈추지 않는 MA팀이 되도록 노력하겠습니다.

참고

https://developer.android.com/topic/libraries/architecture/livedata
https://medium.com/harrythegreat/jetpack-android-livedata-알아보기-ed49a6f17de3
https://developer.android.com/topic/libraries/architecture/viewmodel
https://insert-koin.io/
https://start.insert-koin.io/


고재성 | MA팀
gojs@brandi.co.kr
브랜디, 오직 예쁜 옷만