CatLab Studio
article thumbnail

아키텍처 패턴? 디자인 패턴?

인터넷을 검색하다 보면, MVC, MVP, MVVM에 관해 설명한 여러 글들을 볼 수 있습니다. 어떤 사람들은 이를 디자인 패턴이라 칭하기도 하고 어떤 사람들은 이를 아키텍처 패턴이라 칭하기도 합니다. 그래서 글을 작성하기 앞서 둘의 차이점에 대해 짚고 넘어가겠습니다.

 

디자인 패턴은, 프로그램 개발 과정에서 자주 발생하는 공통적인 문제들을 쉽게 해결하기 위해 재사용 가능한 일종의 모범 템플릿입니다. 주로 클래스나 객체 간의 상호작용을 개선하는 데 사용되며, 예시로 싱글톤, 빌더, 옵저버 패턴 등이 이에 해당합니다. 

 

아키텍처 패턴은, 소프트웨어 시스템 전체의 구조와 구성요소 간의 관계를 설계하는 청사진이라 볼 수 있습니다. 이 또한 디자인 패턴과 같이 공통적인 문제들을 해결하기 위한 재사용 가능한 일종의 모범 템플릿이라 볼 수 있지만, 적용되는 범위가 더 광범위하다는 차이점이 있습니다. 

 

MVC, MVP, MVVM는 아키텍처 패턴입니다. 그리고 이러한 아키텍처 패턴을 구현할 때, 다양한 디자인 패턴이 사용됩니다.  같은 MVVM 패턴을 구현하더라도 Singleton, Observer, Repository 등 디자인패턴을 사용하거나 사용하지 않을 수 있습니다. 결론적으로 MVC, MVP, MVVP 패턴은 프로그램 개발 과정에서 자주 발생하는 공통적인 문제들을 쉽게 해결하기 위한 템플릿으로 디자인 패턴이라 부를 수 있지만, 좀 더 세부적으로는 소프트웨어 시스템 전체의 구조를 설계하는 템플릿이기에 아키텍처 패턴이라고도 할 수 있습니다.

 

왜 적용해야 할까?

장점

  • 모델, 뷰, 그리고 컨트롤러/프레젠터/뷰모델 간의 역할 분리로 코드 관리 및 유지보수가 용이합니다
  • 각 구성 요소가 독립적이므로, 코드 재사용성이 높아집니다
  • 분리된 구조 덕분에 각 구성 요소를 독립적으로 테스트하기가 용이합니다
  • 애플리케이션의 다른 부분에 영향을 주지 않고, 각 구성 요소를 독립적으로 확장할 수 있습니다

단점

  • 패턴을 구현하기 위한 초기 세팅이 복잡하고, 시간이 소요됩니다
  • 간단한 애플리케이션의 경우, 관심사의 과도한 분리가 오히려 개발을 복잡하게 만들 수 있습니다

MVC / MVP / MVVM 패턴

기본적으로 안드로이드 프로젝트의 구조를 살펴보면, 사용자에게 표기되는 xml과 사용자의 입력을 받아 데이터를 처리하고 업데이트하는 Activity(혹은 Fragment 등)로 이루어져 있습니다. 하지만 이러한 구조로 개발을 지속하다 보면 Activity 내의 코드가 과하게 증가하고 같은 데이터에 접근하더라도 각 Activity마다 중복된 코드를 입력하는 등 가독성과 유지보수 측면에서도 다양한 문제점이 발생합니다. 이를 해결하기 위한 다양한 아키텍처 패턴을 알아보겠습니다.

전통적인 MVC 패턴

MVC 패턴은 Model, View, Controller의 약자로, 이후 MVP 패턴, MVVM 패턴 등으로 발전할 수 있게 해 준 초석과 같은 패턴이라 볼 수 있습니다. 그 과정에서 여러 플랫폼마다 다양하게 해석되어 인터넷에 MVC 패턴을 검색해 보면 정말 다양한 구현 방식을 확인할 수 있습니다. 우선은 전통적인 방식의 MVC 패턴을 확인해 보겠습니다.

 

전통적인 MVC 패턴

View

  • Model을 통해 데이터를 받아 사용자에게 정보를 표기합니다

Controller

  • 사용자의 입력 이벤트를 처리하고 Model에 데이터 처리 요청을 보냅니다
  • 간혹 직접 View를 업데이트합니다

Model

  • 데이터와 비즈니스 로직을 담당합니다
  • 데이터가 변경되면 View에게 이를 알립니다
비즈니스 로직은 소프트웨어에서 실제로 비즈니스 요구 사항을 수행하는 부분을 의미합니다. 이는 데이터 처리, 규칙 및 조건에 대한 로직을 포함합니다. 예를 들어, 은행 애플리케이션에서 고객의 계좌 잔액을 확인하고 거래를 처리하는 것이 비즈니스 로직입니다.

 

안드로이드 MVC 패턴

이러한 MVC 패턴은 시간이 지나면서 다양한 형태로 변형되었습니다. 과거의 MVC 패턴은 View와 Model이 서로 직접적으로 데이터를 주고받았다면 현재 주로 구현되는 MVC 패턴은 Controller가 이 역할을 중간에서 맡고 있는 형태입니다. View가 모델로부터 데이터를 전달받기 위해선 Controller를 꼭 거쳐야 하는 구조가 됐습니다. 어떻게 보면 Controller가 View의 역할도 겸하게 됐다고 볼 수 있습니다.

 

현재의 MVC 패턴

View

  • 사용자에게 정보를 표기합니다

Controller

  • 사용자의 입력 이벤트를 처리하고 Model에 데이터 처리 요청을 보냅니다
  • Model로 부터 데이터 처리 결과를 받아서 View의 UI를 갱신합니다 (기존 View의 역할)

Model

  • 데이터와 비즈니스 로직을 담당합니다

MVC 패턴  장·단점

장점

  • 각각의 역할과 관심사가 분리되어 유지보수와 가독성이 개선되었습니다
  • 간단한 프로젝트의 경우, 빠르게 구현이 가능합니다

단점

  • 기존 View 역할도 사실상 Controller가 맡게 되며 Controller의 코드가 비대해집니다
  • View와 Model간의 의존성이 높다고 볼 수 있습니다. 규모가 커짐에 따라 유지보수가 어려워집니다
  • View와 Controller의 차이를 명확히 구분 짓기 어렵습니다

 

MVC 예시코드

// Model
class CounterModel {
    private var value = 0

    fun getValue(): Int {
        return value
    }

    fun incrementValue() {
        value++
    }
}

// View (xml)
<LinearLayout ... >
    <TextView android:id="@+id/textView" ... />
    <Button android:id="@+id/button" android:text="Increment" ... />
</LinearLayout>

// Controller (Activity)
class MainActivity : AppCompatActivity() {
    private val model = CounterModel()

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

        val textView = findViewById<TextView>(R.id.textView)
        val button = findViewById<Button>(R.id.button)

        button.setOnClickListener {
            model.incrementValue()
            textView.text = model.getValue().toString()
        }
    }
}

MVP 패턴

MVP 패턴은 Model, View, Presenter의 약자로, 해당 패턴에서는 이전에 Controller로 인해 애매했던 View의 역할이 명확해지고, Presenter가 현재의 Controller처럼 중재자 역할을 완전히 담당하게 되면서 View와 Model 간의 직접적인 의존성이 없어지고 각 구성 요소 간의 역할이 보다 명확하게 분리됐습니다.

 

MVP 패턴

View

  • 사용자의 입력 이벤트를 직접 받습니다
  • Presenter를 참조하며, 입력 이벤트를 전달합니다
  • Presenter와 1:1 관계를 갖습니다

Presenter

  • View와 Model간의 중재자 역할을 합니다
  • View의 입력을 받아서 Model을 통해 데이터를 처리하고, View에 UI 갱신 요청을 보냅니다
  • View를 참조하며, View와 1:1 관계를 갖습니다

Model

  • 데이터와 비즈니스 로직을 담당합니다

MVP 패턴 장·단점 

장점

  • View의 역할이 명확해졌습니다
  • View와 Model간의 중재자로 Presenter가 생겨 단위 테스트가 보다 용이해졌습니다

단점

  • View와 Presenter가 1:1로 서로 결합되어 있습니다 (보통 interface를 이용해 의존성을 낮춤, 테스트 용이성 향상)

 

MVP 예시코드

// Model
class CounterModel {
    private var value = 0

    fun getValue(): Int {
        return value
    }

    fun incrementValue() {
        value++
    }
}

// View(xml)
// 생략

// View Interface
interface MainView {
    fun displayValue(value: Int)
}

// View(Activity)
class MainActivity : AppCompatActivity(), MainView {
    private lateinit var presenter: MainPresenter

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

        presenter = MainPresenter(this)

        val button = findViewById<Button>(R.id.button)
        button.setOnClickListener {
            presenter.incrementValue()
        }
    }

    override fun displayValue(value: Int) {
        findViewById<TextView>(R.id.textView).text = value.toString()
    }
}

// Presenter
class MainPresenter(private val view: MainView) {
    private val model = CounterModel()

    fun incrementValue() {
        model.incrementValue()
        view.displayValue(model.getValue())
    }
}

MVVM 패턴

MVVM 패턴은 Model, View, ViewModel의 약자로, 현재 자주 사용되고 있는 패턴입니다. 이전의 MVC 패턴에서 MVP 패턴으로 넘어갈 때 View와 Model 사이의 의존성을 제거했다면, 이번에는 ViewModel(이전의 Presenter 역할을 대신)에서의 View에 대한 참조와 의존성을 제거해 View와의 1:1 관계에서 벗어난 패턴입니다. 

 

MVVM 패턴에서의 ViewModel은 View(xml, Activity)로부터 입력 처리 요청을 받아 Model을 이용해 데이터를 처리하고 그 데이터의 상태를 보유합니다. 그리고 ViewModel은 Presenter과 달리 View에 대한 참조를 전혀 가지고 있지 않습니다. 그렇다면 어떻게 View는 ViewModel의 데이터를 사용자에게 보여줄 수 있을까요? 바로 바인딩(binding)입니다. View는 ViewModel의 속성에 직접 바인드(bind, 연결)된 채로 UI를 업데이트합니다.

 

그리고 이 바인딩을 도와주는 것이 Databinding 라이브러리입니다. AAC Databinding은 View(xml)에서 ViewModel의 데이터와 뷰를 직접 바인드 하여 선언적인 방식으로 UI를 정의할 수 있도록 도와줍니다. 이는 MVVM 패턴에서 요구하는 '선언적인 데이터 바인딩 기술'에 완전히 부합하고, 바인더(Binder) 역할을 한다고 볼 수 있습니다. 또한, AAC ViewModel을 사용하면 Configuration Change가 발생했을 때 뷰의 데이터를 보존하는 등 여러 이점이 있습니다.

 

그렇다면 AAC Databinding, AAC ViewModel을 사용하지 않으면 MVVM 패턴이라 볼 수 없고, 사용하면 무조건 MVVM 패턴이라 볼 수 있는 걸까요? 그렇지는 않습니다. 위 라이브러리는 MVVM 패턴을 구현할 때 있어서 사용할 수 있는 좋은 수단이지 그 자체가 MVVM 패턴을 의미한다고 볼순 없습니다. 예시로 두 라이브러리를 사용하지 않고, StateFlow나 LiveData를 이용하여 직접 View(Activity)에서 ViewModel의 데이터를 observe, collect 하여 데이터를 바인드 한다면 충분히 MVVM 패턴을 구현할 수 있을 것입니다. (이 경우 엄격하게 따지면 boilerplate code가 발생하기에 완벽한 MVVM 패턴이라 하기에는 문제점이 있습니다.)

 

MVVM 패턴

View

  • 사용자의 입력 이벤트를 받아 ViewModel의 메서드를 호출합니다
  • ViewModel의 데이터와 바인드하여 UI를 갱신합니다

ViewModel

  • View를 참조하지 않습니다
  • View와 1:1 관계를 가지지 않습니다
  • Model을 통해 데이터를 처리하고, 데이터의 상태를 보유합니다

Model

  • 데이터와 비즈니스 로직을 담당합니다

MVVM 패턴 장·단점

장점

  • ViewModel에서 View에 대한 참조와 의존성이 없어지면서 테스트가 매우 용이해졌습니다
  • ViewModel:View 1:n 관계로 한 ViewModel을 여러 View에서 재활용이 가능해졌습니다

단점

  • 간단한 프로젝트에서는 과한 설계로 느껴질 수 있습니다

 

MVVM 예시코드

// Model
class CounterModel {
    private var value = 0

    fun getValue(): Int {
        return value
    }

    fun incrementValue() {
        value++
    }
}

// View(xml)
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <variable
            name="viewModel"
            type="your.package.name.MainViewModel" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{viewModel.currentValue}" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Increment"
            android:onClick="@{() -> viewModel.incrementValue()}" />
            
    </LinearLayout>
</layout>

// View(Activity)
class MainActivity : ComponentActivity() {
    private val viewModel: MainViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding: ActivityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        binding.lifecycleOwner = this
        binding.viewModel = viewModel
    }
}

// ViewModel
class MainViewModel : ViewModel() {
    private val model = CounterModel()

    private val _currentValue = MutableStateFlow(0)
    val currentValue: StateFlow<Int> = _currentValue.asStateFlow()

    fun incrementValue() {
        model.incrementValue()
        _currentValue.value = model.getValue()
    }
}

마치며

이상으로 MVC, MVP, MVVM 패턴의 변화 과정과 특징에 대해 알아보았습니다. 감사합니다

profile

CatLab Studio

@CatLab

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!