[Android] MVVM 패턴 적용기

강승구·2023년 9월 19일

MVVM 패턴을 왜 써야하나요?

그동안 앱 개발 프로젝트들을 진행해보면서 MVVM 패턴에 많이 들어왔다.

구글에 MVVM 패턴에 대해 검색을 하면, 굉장히 많은 글이 나온다.

그리고 모든 글에서는 MVVM 패턴을 써야한다고 하고 있다. MVVM 패턴을 적용하면 코드의 유지보수성이 높아지고 관심사의 분리가 명확해진다고 한다.

하지만 나는 MVVM 패턴을 적용하지 않고 그냥 앱을 개발해도 불편한 점을 느끼지 못했다.

우선 내가 그동안 경험해본 프로젝트들은 규모가 매우 작았다. 앱에서 필요로하는 기능들이 많이 없고, 사용하는 API들이 적은 편이니, 디자인 패턴을 적용하지 않고 UI 로직, 비즈니스 로직, 네트워크 호출과 같은 모든 코드들을 Activity에 넣어도 딱히 문제가 되지 않았다.

또한, 프로젝트가 끝나면 유지보수를 할 일이 없었다. 기능을 개발하면서 코드가 복잡해져도, 일단 기능이 정상적으로 돌아가도록 구현만하고 프로젝트가 끝나게 되면 다시 코드를 볼 일이 없었니, 응집도가 낮은 코드가 당장에는 문제가 되지 않았다.

그렇기 때문에 MVC 패턴만을 고집해왔는데 최근 팀프로젝트로 "환경 캠페인 인증 커뮤니티" 앱을 개발하는 과정에서 MVVM 패턴 적용의 필요성을 느끼게 되었다.

MVC 패턴을 사용하면서 Activity에 UI 로직뿐만 아니라 비즈니스 로직과 네트워크 호출까지 처리하게 되면서, 응집도가 낮아져 Activity 파일의 코드가 600줄이 넘어가게 되었고, 코드가 지나치게 복잡해졌다. 특히, 인증된 캠페인 목록을 서버에서 받아와 표시하는 기능을 구현할 때, 네트워크 요청 처리, 데이터 파싱, UI 업데이트가 모두 Activity에 몰리면서 가독성이 떨어졌고, 오류가 발생했을 때 원인을 파악하기가 어려웠고 오류를 수정하면서 다른 부분에까지 영향을 미치는 문제가 발생했다.

이러한 이유로 해당 프로젝트에 MVVM 패턴을 적용해 리팩토링을 해보기로 하였다.


MVVM? 디자인패턴?

우선 MVVM은 패턴이다. 구체적으로는 디자인 패턴이라고 한다.
디자인 패턴이란 소프트웨어를 개발하면서 발생하는 문제를 해결하기 위한 방법을 일반화 시킨것이다.

What's a design pattern? / Refactoring Guru 에서는 디자인 패턴을 다음과 같이 설명하고 있다.

디자인 패턴은 소프트웨어 개발 중 흔하게 발생하는 문제들을 해결할 수 있는 일반론들을 모아 놓은 것이다. 디자인 패턴은 미리 만들어진 청사진과 같아서, 현재 발생하고 있는 설계 (*design) 문제들을 자신의 코드에 맞게 커스터마이징해서 적용하여 사용하는 것이다.

따라서 특정 디자인 패턴을 찾았다고 해서, 코드에 복붙을 할 수는 없다. 디자인 패턴은 함수나 라이브러리가 아니라서, 특정 코드 블럭을 복붙한다고 해서 적용할 수 있는 것이 아니기 때문이다.

대신 디자인 패턴은 특정 문제를 풀기 위한 넓은 의미로서의 개념과 같다. 디자인 패턴을 사용하되, 세밀하게 현재 코드에 맞도록 적용함으로서 이 개념을 적용할 수 있다.

그렇다면 MVVM은 어떤 문제를 해결하기 위한 방법론인걸까?
MVVM의 목적은 비즈니스 로직을 UI로부터 분리하는 것이다.

우선 비즈니스 로직과 UI 로직이 무엇인지부터 차근차근 살펴보자.

1. 비즈니스 로직
비즈니스 로직이란 애플리케이션이 처리해야 하는 핵심 데이터와 그 데이터를 다루는 규칙이나 절차를 의미한다.
즉, 무엇을 해야 하고, 어떻게 처리할 것인지를 결정하는 부분이다.

내가 개발중이던 "환경 캠페인 참여 인증 커뮤니티" 앱에서는 캠페인 참여 버튼을 누르면 참여 인증을 할 수 있는 기능이 있다.

해당 기능에서는 캠페인 참여 여부를 서버에서 확인하고, 인증 사진을 서버에 업로드하는 과정이 비즈니스 로직에 해당한다.

이러한 과정에서는 다음과 같은 작업들이 수행된다.

  • 사용자가 캠페인 참여 버튼을 누를 때 서버에 요청을 보내 참여 여부를 확인.
  • 인증 사진을 서버로 업로드하고, 서버에서 성공 여부를 받아 처리.
  • 캠페인 참여 기록을 로컬 DB나 캐시에 저장하여 오프라인 상태에서도 확인 가능하게 함.

이런 데이터 처리와 규칙을 구현하는 것이 비즈니스 로직이다.


2. UI 로직
UI 로직은 데이터를 사용자에게 어떻게 보여줄지를 결정하는 부분이다. 즉, 사용자 인터페이스(UI)에 표시할 데이터를 가공하거나, 사용자의 입력을 받아 데이터를 처리하는 역할을 한다.

"환경 캠페인 인증 커뮤니티" 앱에서 서버에서 받은 캠페인 리스트를 화면에 출력하는 기능을 생각해보자.

서버에서 데이터를 가져오고 사용자에게 보여줄 준비를 하는 것이 UI 로직이다.
이 과정에서는 다음과 같은 작업들이 이루어진다.

  • 서버에서 받아온 캠페인 데이터를 날짜순으로 정렬.
  • 사용자의 입력에 따라 특정 조건에 맞는 캠페인만 필터링해서 보여줌.
  • 캠페인 참여 성공/실패 여부에 따른 상태 메시지를 화면에 적절히 표시함.

이처럼, UI 로직은 비즈니스 로직에서 처리된 데이터를 정리하여 화면에 표시하는 역할을 한다.

비즈니스 로직과 UI 로직을 분리하면 좋은 점은 다음과 같다.

  1. 각 부분이 분리되어 있기 때문에, UI를 수정하거나 데이터를 처리하는 로직을 변경할 때도 다른 부분에 영향을 주지 않고 수정할 수 있다.
  2. 비즈니스 로직과 프레젠테이션 로직이 분리되어 있으면 UI와 무관하게 로직 자체를 테스트하기가 훨씬 쉬워진다.
  3. 프로젝트가 커지고 기능이 많아질수록 각 부분이 독립적으로 관리되므로 새로운 기능을 추가하거나 수정하는 데 용이하다.

정리하자면, MVVM 패턴은 Activity에 모든 코드를 작성해 응집도가 낮아지는 문제를 해결하기 위해 ViewModel에서는 UI에 필요한 데이터를 보유하고 관리하며, UI(Activity, Fragment)는 ViewModel로부터 데이터를 받아 화면에 표시하는 역할을 담당하여 각자의 책임을 분리하는 것이다.

이로 인해 코드의 유지보수성과 가독성이 향상되고, 비즈니스 로직이 분리되어 재사용성이 높아지며, 비즈니스 로직이나 UI의 변경이 서로에게 미치는 영향을 최소화할 수 있다.


MVVM 패턴의 구성요소

  1. View : 사용자가 직접 보는 UI를 담당 (Activity)
  2. ViewModel: View와 Model 사이에 위치하여 중재하는 역할. View와 데이터바인딩을 통해 서로 연결한다. 데이터 변경이 발생하면 Model을 갱신하거나 가져와서 View에 필요한 Observable Data로 가공한다.
  3. Model : 앱 내에서 필요한 데이터, 상태, 비즈니스 로직을 저장하고 처리하는 역할

MVVM 패턴은 다음과 같은 과정을 거쳐 동작한다.

  1. 사용자의 Action들은 View를 통해 들어온다.
  2. View에 Action이 들어오면 ViewModel에 Action을 전달한다.
  3. ViewModel은 Model에게 데이터를 요청한다.
  4. Model은 ViewModel에게 요청받은 데이터를 응답한다.
  5. ViewModel은 응답 받은 데이터를 가공하여 저장한다.
  6. View는 Data Binding을 이용해 UI를 갱신시킨다.

로그인 과정을 예로 들자면 다음과 같이 설명할 수 있다.

1. 사용자의 Action들은 View를 통해 들어온다.
사용자가 로그인 화면(Activity)에서 로그인 버튼을 클릭하면, 사용자 입력(아이디, 비밀번호)과 함께 Action(로그인 버튼 클릭)이 View(Activity)로 전달된다.

2. View에 Action이 들어오면 ViewModel에 Action을 전달한다.
로그인 버튼 클릭 이벤트가 발생하면, View는 사용자가 입력한 아이디와 비밀번호를 ViewModel에 전달해 로그인 요청을 전달한다.

class LoginActivity : AppCompatActivity() {
	override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)
        ...
        loginButton.setOnClickListener {
            // ViewModel에게 로그인 요청
            viewModel.login(usernameEditText.text.toString(), passwordEditText.text.toString())
        }
    }
}

3. ViewModel은 Model에게 데이터를 요청한다.
ViewModel은 전달받은 아이디와 비밀번호를 이용해, Model(Repository 등)에 로그인을 요청한다. Model은 실제 데이터베이스 또는 서버와 통신하여 로그인 정보를 확인하는 역할을 한다.

fun login(username: String, password: String) {
    repository.login(username, password)
}

4. Model은 ViewModel에게 요청받은 데이터를 응답한다.

이때 LiveData와 DataBinding은 왜 필요한걸까?

LiveData
LiveData는 데이터 변경을 감지하고, 이를 자동으로 UI에 반영할 수 있도록 한다.
예를 들어, ViewModel에서 LiveData 값을 업데이트하면, 이를 관찰하는 Activity나 Fragment는 그 변화에 따라 UI를 즉시 갱신할 수 있어 코드에서 수동으로 UI 갱신을 처리할 필요가 없어진다.

UI와 로직이 직접적으로 연결되지 않고, LiveData를 통해 간접적으로 연결되기 때문에, 관심사의 분리가 명확해지고 의존성이 줄어들게 된다.


DataBinding
DataBinding은 XML 레이아웃에서 UI 요소와 ViewModel의 데이터를 직접 연결해주는 역할을 담당한다.
이를 통해 XML 파일에서 ViewModel의 데이터를 참조하거나, 이벤트 처리를 직접 정의할 수 있다.

DataBinding을 사용하면 XML 레이아웃 파일에서 ViewModel의 LiveData를 직접 바인딩할 수 있습니다. 이렇게 하면 코드에서 UI를 업데이트하는 부분을 줄이고, 데이터와 UI가 직접적으로 연결되어 자동으로 UI가 업데이트된다.

또한 DataBinding을 사용하면 Activity나 Fragment에서 UI 업데이트 코드가 줄어든다. 일반적으로는 ViewModel의 데이터를 관찰하고, 데이터가 변경될 때마다 UI 요소를 수동으로 업데이트해야 하지만, DataBinding을 통해 XML에서 바로 데이터를 연결하면 이런 작업이 자동화되어 코드가 간결해지게 된다.

profile
강승구

0개의 댓글