본문 바로가기

OS/Android

안드로이드 패턴.. 어떤걸 쓰지??

반응형

서론

2년 동안 앱을 개발하면서 유지보수 해야 하는 어플도 많아지고 있다.

관리해야 하는 앱이 많다보니 전에 작성한 코드들이 생각이 안 날 때가 많다.

물론 코드를 짤 때 유지보수가 용이하도록 모듈화 하려고 노력하지만 실력이 부족해서인지 이것도 한계를 느끼고 있다.

조금 더 쉽게 코드들을 관리하기 위해선 패턴을 사용해야 겠다는 생각이 들었고 앞으로 만드는 프로젝트들은 패턴을 적용시켜서 개발하려고 한다.

 

그래서 어떤 패턴을 쓸건데?

보통 안드로이드에서 사용하는 패턴은 3가지이다.

1. MVC

MVC 패턴은 개발자라면 한 번쯤은 들어봤을만큼 유명한 패턴이다.

웹 / 앱 가리지 않고 많이 사용하는(했던?) 패턴이다. 

구성을 설명하자면

- Model : 일반적으로 데이터베이스나 서버에서 데이터를 받아서 가공하는 등의 비지니스 로직을 담당한다.

- View : 유저에게 보여지는 화면이다.

- Controller : 유저의 이벤트에 근거하여 Model과 View를 제어한다.

 

http://blog.dramancompany.com/2016/08/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C%EC%97%90-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EB%8F%84%EC%9E%85%ED%95%98%EA%B8%B0/

정리를 하자면

View에서 이벤트가 발생하게 되면 이벤트와 함께 Controller를 호출한다.

Controller는 Model을 호출하여 데이터를 업데이트하거나 데이터를 불러온다.

Model은 데이터베이스에서 가지고 온 데이터를 포맷에 맞게 가공하여 Controller에게 응답 OR 보여줄 View를 선택해서 화면에 보여준다.

위에 흐름도를 보면 결국 MVC는 UI와 로직을 분리하여 유지보수를 용이하게 만드려는 패턴이다.

분명 위의 구조는 UI와 로직들을 분리함으로써 하나의 페이지에 모든 로직을 작성하는 것 보단 유지보수가 더 쉽겠지만 Model과 View의 의존성이 완전히 분리되지 않았기에 기능들이 많아질 수록 구조가 복잡해질 수 있다. 

 

- MVC 패턴을 선택하지 않은 이유

예전에 Spring으로 웹 개발할 때 겪었던 경험으로는 Controller가 많은 일들을 담당하기 때문에 코드가 너무 방대해져 관리하기가 까다로웠다.

또한 기본적으로 안드로이드는 Activity나 Fragment가 View영역과 Controller영역까지 담당하고 있다는 점에서 이미 MVC패턴이 적용되어 있다고 볼 수 있다.

 

2. MVP

MVP는 안드로이드에서 대략 4~5년 전부터 많이 사용하고 있는 패턴이다.

MVP 패턴은 MVC 패턴에서 파생된 패턴이며 기존 MVC와의 차이점은 Model과 View의 의존성을 극단적으로 줄이려고 한다는 것이다.

구성은 다음과 같다.

- Model : 일반적으로 데이터베이스나 서버에서 데이터를 받아서 가공하는 등의 비지니스 로직을 담당한다.

- View : 유저에게 보여지는 화면을 뜻하며 UI 로직들을 담당한다.

- Presenter : 본질적으로는 MVC의 Controller와 같지만 View에 직접 연결되는 것이 아니라 인터페이스로 연결된다. 

 

http://blog.dramancompany.com/2016/08/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C%EC%97%90-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EB%8F%84%EC%9E%85%ED%95%98%EA%B8%B0/

 

View에서 이벤트가 발생하게 되면 이벤트와 함께 Presenter를 호출한다.

Presenter는 프리젠트 로직을 처리하고 Model을 호출한다.

Model은 데이터와 관련된 비지니스 로직을 처리하고 Presenter에게 결과를 반환한다.

Presenter는 Model에게 받은 결과를 가지고 VIew를 갱신한다.

 

Presenter는 View와 Model을 서로 모르게 제어하기 위해 Model과 View를 가지고 있어야 하는데 이때 사용되어지는 것이 Contract(계약) 인터페이스이다.

소스를 직접 짜봐야 느낌을 알 것 같아서 간단하게 테스트 코드를 작성해봤다.

class MainActivity : AppCompatActivity(),  MainContract.View
{
    private lateinit var m_presenter: MainContract.Presenter
    private lateinit var m_btnRequestData: Button
    private lateinit var m_app : MyApp
    private var m_toast : Toast? = null

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

        m_app = application as MyApp
        m_presenter = MainPresenter(this, m_app.m_repository)

        m_btnRequestData = findViewById(R.id.v_btnRequestData)
        m_btnRequestData.setOnClickListener {
            m_presenter.saveClick()
        }
    }



    override fun showMyToast(nCount: Int)
    {
        m_toast?.cancel()
        m_toast = Toast.makeText(this, "$nCount", Toast.LENGTH_SHORT)
        m_toast?.show()
    }
}

 

interface MainContract
{
    interface View
    {
        fun showMyToast(nCount : Int)
    }

    interface Presenter
    {
        fun saveClick()
    }
}

 

class MainPresenter(
    private val m_view: MainContract.View,
    private val m_repo: CountRepository
) : MainContract.Presenter {

    override fun saveClick()
    {
        GlobalScope.launch {
            m_repo.saveClick()
            val nCount = m_repo.getClickCount()
            withContext(Dispatchers.Main) {
                m_view.showMyToast(nCount)
            }
        }
    }
}

 

interface CountRepository
{
    suspend fun saveClick()
    suspend fun getClickCount() : Int
}

 

class CountRepositoryImpl(
    private val m_database : AppDatabase
) : CountRepository
{

    override suspend fun saveClick()
    {
        m_database.daoClick().insert(ClickVo())
    }

    override suspend fun getClickCount() : Int
    {
        return m_database.daoClick().getCount()
    }
}

 

@Database(entities = [ClickVo::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase()
{
    abstract fun daoClick() : DaoClick

    companion object
    {
        private val DB_NAME = "my_db"

        private var m_instance: AppDatabase? = null

        fun getInstance(context: Context) : AppDatabase
        {
            return m_instance ?: synchronized(this)
            {
                m_instance ?: buildDatabase(context).also { m_instance = it }
            }
        }

        private fun buildDatabase(context: Context) : AppDatabase
        {
            return Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, DB_NAME)
                    .fallbackToDestructiveMigration()
                    .addCallback(object : RoomDatabase.Callback()
                    {
                        override fun onCreate(db: SupportSQLiteDatabase)
                        {
                            super.onCreate(db)
                        }
                        override fun onOpen(db: SupportSQLiteDatabase)
                        {
                            super.onOpen(db)
                        }
                    }).build()
        }
    }

    @Dao
    interface DaoClick
    {
        @Insert(onConflict = OnConflictStrategy.REPLACE)
        fun insert(vo: ClickVo)

        @Query("SELECT COUNT(*) FROM tb_click")
        fun getCount(): Int
    }
}
@Entity(tableName = "tb_click")
data class ClickVo(
    @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "aid") val nIdx : Int = 0
)

 

 

테스트 코드를 작성하면서 느낀점

장점

- 예전에 구글측에서 작성한 코드들이 MVP로 개발된게 많다보니 알게 모르게 친숙하다.

- 분리가 확실하다.

- View에서 오버라이딩된 함수만 보아도 대충 어떤 기능이 들어가 있을지 추측할 수 있다.

 

단점

- View와 Presenter의 관계가 1:1이기 때문에 기능 하나 개발하는데 만들어야 하는 클래스가 너무 많다.

Presenter 때문에 Interface도 만들어야 하고 Contract도 만들어야 하고..

간단한 기능을 가진 화면도 있을텐데 View마다 제각각 Presenter와 Contract를 만들어야 한다면 프로젝트가 커졌을 때 파일 관리가 조금 힘들지 않을까 싶다.

- 재사용에 중점을 둔 아키텍처라기 보단 Model과 View를 완전히 분리시키기 위한 패턴인 것 같다.

 

- MVP 패턴을 선택하지 않은 이유

분리만 보았을 때는 꽤 쓸만한 패턴이라고 생각한다. 

하지만 MVC 패턴과 마찬가지로 프로젝트가 점점 커질 수록 Presenter에 코드량이 집중될 것이고 만들어야 하는 파일들이 많기 때문에 관리가 힘들어질 것 같다.

 

3. MVVM

MVVM은 MVP에서 파생된 아키텍처 패턴이며 요즘 안드로이드에서 많이 사용하고 있는 대세(?) 패턴이다.

구글에서 AAC(Android Architecture Component)를 출시함으로서 MVVM 채택하는 회사들이 많아진 것 같다. (가끔 다른 회사들이 어떤 기술들을 사용하는지 확인하기 위해 채용공고를 확인해보는데 다 MVVM 우대임)

MVVM의 구성은

- View : 유저에게 보여지는 화면을 뜻하며 UI 로직들을 담당한다.

- ViewModel : View와 Model의 중간 매개체라고 할 수 있다. MVP와는 다르게 ViewModel은 View를 가지고 있지 않으며 View 1 : ViewModel : N이 가능하다.

- Model : 일반적으로 데이터베이스나 서버에서 데이터를 받아서 가공하는 등의 비지니스 로직을 담당한다.

 

https://ko.wikipedia.org/wiki/MVVM

관계는

View는 ViewModel을 옵저빙하고 있다.

ViewModel은 Model의 데이터를 옵저빙하고 있다.

ViewModel은 Model을 가지고 있지만 View나 View의 Context를 참조하지 않는다.

 

흐름은

View에서 이벤트가 발생하게 되면 이벤트와 함께 ViewModel을 호출한다.

ViewModel에서는 Model을 호출하게 된다.

Model은 비지니스 로직을 수행하고 데이터를 갱신한다.

ViewModel은 Model 데이터 업데이트 통지를 받는다.

ViewModel은 해당 로직을 처리하고 상태를 변경한다.

View는 ViewModel에게 상태 변화 통지를 받는다.

View는 화면을 갱신한다.

 

MVVM에서는 바인딩을 사용함으로써 위에서 소개했던 패턴들보단 각 구성요소들이 비교적 유기적인 모습들을 보여주고 있다.

 

조금 더 쉽게 이해하기 위해 테스트 코드를 작성해 보았다. 

class MainActivity : AppCompatActivity()
{
    private lateinit var m_btnRequestData: Button
    private lateinit var m_app : MyApp
    private var m_toast : Toast? = null
    private lateinit var m_viewModel : MainViewModel

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

        m_app = application as MyApp
        m_btnRequestData = findViewById(R.id.v_btnRequestData)
        m_viewModel = ViewModelProvider(this, MainViewModelFactory(m_app.m_repository)).get(MainViewModel::class.java).apply {
            m_nCount.observe(this@MainActivity, Observer {
                showMyToast(it)
            })
            m_btnRequestData.setOnClickListener {
                m_viewModel.saveClick()
            }
        }
    }


    fun showMyToast(nCount: Int)
    {
        m_toast?.cancel()
        m_toast = Toast.makeText(this, "$nCount", Toast.LENGTH_SHORT)
        m_toast?.show()
    }
}

 

class MainViewModelFactory(
    private val m_repo: CountRepository
) : ViewModelProvider.Factory
{
    override fun <T : ViewModel?> create(modelClass: Class<T>): T
    {
        return MainViewModel(m_repo) as T
    }
}
class MainViewModel(
    private val m_repo: CountRepository
) : ViewModel()
{

    var m_nCount : MutableLiveData<Int> = MutableLiveData()

    fun saveClick()
    {
        GlobalScope.launch {
            m_repo.saveClick()
            m_nCount.postValue(m_repo.getClickCount())
        }
    }
}

테스트 코드를 작성하면서 느낀점

장점

- ViewModel을 재사용할 수 있어서 MVP보단 파일 관리가 용이할 것 같다. 

- 분리가 확실하다.

- 바인딩 개념을 도입함으로서 ViewModel의 부담이 줄어 들었다.  

- 장점인지는 모르겠지만 RxJava와 같은 함수형 프로그래밍 방식으로 개발하면 조금 더 유리한 점이 있을 것 같다.

단점

- 현재 회사에서 서비스하고 있는 앱들을 MVVM으로 리팩토링한다고 가정 했을 때 아키텍처 구성이 쉽지 않을 것 같다. 

 

결론

 

결론부터 얘기하자면 MVVM을 선택하는게 여러 방면에서 이득일 것이라 생각한다.

현재 상황에서 가장 필요한 건 로직 분리이기도 하고, 구글에서 ViewModel, LiveData, LifeCycle과 같은 라이브러리들을 계속 출시하는 걸 보면 앞으로 공식 코드들도 MVVM으로 나올 확률이 클 것 같다. 

그리고 무엇보다 요즘 대세라고 하니까....

반응형