아키텍처 설계 고민의 시작

순순·2025년 4월 27일

개요

아키텍처 설계에 대해 고민을 하기 시작했다. 최근 사이드 프로젝트를 새로 진행하면서 구조를 짜게 된 덕분이다. 특히 흔히 사용되는 MVVM + 클린 아키텍처를 적용하려면 제대로 이해하고 정리할 필요성을 느꼈다.

사실 앱 개발을 처음 배울 때, 단순히 "요즘은 MVVM을 많이 쓴다"는 흐름을 따라 MVVM 패턴을 적용하기 시작했다. 그러나 실제로 적용하면서 깊은 고민은 뒤로 미뤄두고 있었다. 바쁘다는 핑계... 멈춰...

그래서 이번 기회에 MVC, MVVM, 그리고 클린 아키텍처를 비교할 뿐만 아니라 처음 접했을 때의 고민들, 해당 패턴들이 어떻게 그 개선점이 되는지 궁금했던 점에 대해 글로 상세히 정리해보았다.

  • MVC 패턴과 MVVM 은 어떤 근본적인 차이가 있는지?
  • 왜 MVVM 만으로는 모자라서 클린 아키텍처까지 필요하게 됐는지?

MVVM 에 대한 의문

MVC 건너뛰고 MVVM을 적용하면서 자연스럽게 든 의문이 있었다. 나는 한 파일에 300줄 넘어가면 길다고 생각하는 편인데, 음... 기능이 추가될수록 코드가 길어졌다.

"분명 MVC의 Controller가 비대해져서 MVVM으로 넘어왔다고 하는데, ViewModel 파일 코드도 길어지는데...?"

"중간에 Controller 대신 ViewModel이 있는 것뿐, 결국 이름만 다르고 비슷한 거 아닌가?"

이게 클린 아키텍처가 적용되는 이유라는 건 나중에야 알았다. 사실 초기에는 "MVVM vs 클린 아키텍처" 둘 중 하나를 선택해야 한다고 오해했었다. 각 패턴의 목적과 역할을 제대로 이해하지 못했기 때문인 듯. 이 둘은 상호보완적으로 함께 사용된다.

아무튼 MVC 와 MVVM 이 뭐가 다른지 본격적으로 궁금해지기 시작했기에 하나씩 살펴보기로 했다. 우선 MVC 부터 보자. 직접 비교해봐야 MVVM 을 애용하는 이유를 잘 알 수 있을테니까...


(1) MVC

MVC = Model, View, Controller 의 약자이다. 각 역할은 다음과 같다.

  • Model: 데이터와 비즈니스 규칙 관리
  • View: 사용자 인터페이스(UI)
  • Controller: View와 Model 사이를 연결하고 입력 이벤트를 처리

코드 예시

(코틀린 코드입니다)

// [Model] 사용자 정보를 담는 데이터 클래스
data class User(val id: Int, val name: String, var email: String)
// [View] 사용자에게 정보를 보여주고 입력을 받는 클래스
class UserView {
	// 사용자 정보 출력
    fun displayUserInfo(name: String, email: String) {
        println("User: $name | Email: $email")
    }
    
    // 사용자로부터 새 이메일 입력 받기    
    fun getUserInput(): String {
        println("Enter new email:")
        return readLine() ?: ""
    }
}
// [Controller] Model과 View를 연결하고 로직을 처리하는 클래스
class UserController(private val view: UserView) {
	// 초기 사용자 모델 생성
    private val userModel = User(1, "Kim", "kim@example.com")
    
    // 사용자 정보 출력
    fun displayUser() {
        view.displayUserInfo(userModel.name, userModel.email)
    }
    
    // 사용자 이메일 업데이트 로직
    fun updateUserEmail() {
        val newEmail = view.getUserInput() // View에서 입력 받음
        userModel.email = newEmail  	   // Model 업데이트
        view.displayUserInfo(userModel.name, userModel.email) // 변경된 정보 출력
    }
}

// 사용 예시
fun main() {
    val userView = UserView()
    val controller = UserController(userView)
    
    controller.displayUser() // 초기 사용자 정보 표시
    controller.updateUserEmail() // 이메일 입력 받고 갱신
}

핵심 흐름

  • View - 사용자 입력 발생
  • Controller - 이 입력을 받아 처리

보면 알겠지만 Controller 가 필요에 따라 Model을 "직접 수정"한다. 수정된 Model을 바탕으로 Controller가 View를 업데이트한다. Controller가 내용을 직접 수정하기 때문에 View와 Model 모두에 강하게 결합되는 것! 이 부분이 MVVM 과 비교되는 가장 큰 차이점이다.

MVVM 에서는 ViewModel이 Model을 간접적으로 수정한다. View는 ViewModel을 관찰(observe)하거나 바인딩(binding)하여 자동으로 갱신된다. 중개자 역할만 하기에 결합도가 약해진다는 것.


(2) MVVM

MVVM = Model, View, ViewModel 의 약자이다. 각 역할은 다음과 같다.

  • Model: 데이터 소스 및 비즈니스 로직
  • View: 사용자에게 보여지는 화면(UI)
  • ViewModel: View의 상태를 관리하고, Model로부터 데이터를 받아 View에 제공

코드 예시

(코틀린 코드입니다)

// [Model]: 사용자 정보를 담는 데이터 클래스 (상태 자체를 표현)
data class User(val id: Int, val name: String, var email: String)
// [ViewModel] UI와 데이터를 연결하는 중간 계층
class UserViewModel {
    private val user = User(1, "Kim", "kim@example.com")  // 내부 상태로 사용자 정보 보유
   
    private val _userState = MutableLiveData(user)
    val userState: LiveData<User> = _userState
    
    // 상태 갱신
    fun updateEmail(newEmail: String) {
        val updatedUser = user.copy(email = newEmail)
        _userState.value = updatedUser // LiveData 변경 → View에 자동 반영됨
    }
}
// [View] 사용자와 상호작용, ViewModel 관찰
class UserView(private val viewModel: UserViewModel) {

    init {
    	// ViewModel의 상태를 구독하여 변경 시 UI 갱신
        viewModel.userState.observe(this) { user ->
            displayUserInfo(user.name, user.email)
        }
    }
    
    // 사용자 정보를 화면에 출력
    fun displayUserInfo(name: String, email: String) {
        println("User: $name | Email: $email")
    }
    
    // 사용자 입력을 받아 ViewModel에 위임 → 간접적으로 Model 갱신
    fun onUpdateEmailClicked() {
        println("Enter new email:")
        val newEmail = readLine() ?: ""
        viewModel.updateEmail(newEmail)
    }
}
// 사용 예시
fun main() {
    val viewModel = UserViewModel()
    val view = UserView(viewModel)
    
    // 사용자 입력 이벤트 시뮬레이션
    view.onUpdateEmailClicked()
}

핵심 흐름

  • View - ViewModel을 구독하거나 바인딩
  • ViewModel - 상태를 관리하고 필요한 데이터를 준비해서 노출
    Model의 변화가 생기면 ViewModel이 변화를 트리거
  • View는 ViewModel의 상태 변화를 관찰하고 자동으로 UI 업데이트 한다

아까 위에서 말했듯이, MVVM 의 가장 큰 특징은 ViewModel 이 View를 직접 알지 못한다는 것이다. 이름 그대로 관찰(observe) 구조 기반으로 동작한다. 상태를 던져주고 어떻게 사용할지는 View에 위임임한다.

이렇게 되면 ViewModel은 단지 상태를 관리하고 노출할 뿐, View가 어떻게 그 상태를 사용하는지 알 필요가 없다. 이는 Controller가 View와 Model 양쪽을 직접 제어하는 MVC와 가장 큰 차이점이다.


(3) MVC vs MVVM 요약

  • MVC: View가 Controller에 명령을 보내는 직접적인 구조
  • MVVM: View가 ViewModel의 상태를 관찰하는 간접적인 구조

(4) 왜 MVVM만으로는 부족할까?

ViewModel이 너무 바빠진다
버튼을 누르면 이메일을 바꿔야 해 → ViewModel이 처리
이메일 바꿀 때 서버에 요청도 해야 해 → ViewModel이 처리
요청 실패하면 에러도 보여줘야 해 → ViewModel이 처리
사용자 정보도 저장해야 해 → ViewModel이 또 처리

MVC에서는 View와 Controller가 서로를 직접 참조하거나 Model을 직접 수정하는 일이 많아지면서, 구조가 복잡해지고 테스트가 어려워지는 문제가 있었다.

MVVM 을 통해 View와 Model 간의 결합도는 확실히 줄어들었지만, ViewModel이 지나치게 많은 역할을 하게 되면서 비대해지는 문제는 여전히 발생한다.

ViewModel이 데이터 준비, 사용자 이벤트 처리, 비즈니스 로직 등을 모두 떠맡다 보면 결국 수백 줄짜리 코드가 한 클래스에 몰리게 되고, 유지보수가 어려워지는 것이다.

즉, 결합도는 낮췄지만, 책임은 과하게 몰렸다고 볼 수 있다.


그래서 등장한 클린 아키텍처 (Clean Architecture)

MVVM만으로는 ViewModel의 책임이 지나치게 커지는 문제가 있었다. 이를 해결하기 위해 등장한 것이 클린 아키텍처(Clean Architecture)이다.

클린 아키텍처는 소프트웨어 구조를 여러 계층으로 나누어 각 계층이 명확한 역할만 수행하도록 하는 설계 방법이다.
쉽게 말해 "일을 나눠서 하자"는 것이다.

일반적으로 Presentation, Domain, Data의 세 계층으로 구성되며, 각 계층은 서로 느슨하게 연결되어 있다.

계층 구분

(1) Presentation Layer

사용자와 직접 상호작용하는 부분.
ex) View, ViewModel

(2) Domain Layer

핵심 로직 계층. 애플리케이션의 핵심 비즈니스 로직이 존재하는 부분. UI나 데이터 저장소에 의존하지 않음.
ex) Usecase, Repository interface

(3) Data Layer

외부와 통신하는 계층. 서버 API, 로컬 DB 등 구체적인 데이터 소스와 연결되는 부분이다.
Domain Layer에서 정의한 Repository 인터페이스를 실제로 구현하여 데이터를 제공한다.
ex) ApiService, LocalDatabase, RepositoryImpl


역할을 비교해보자

이렇게 설명만 들으면 진짜 헷갈린다. 역할을 비교해보자.
MVVM만 썼을 때 vs 클린 아키텍처까지 도입했을 때

(1) ViewModel

MVVM

  • ViewModel이 이것저것 해서 바쁘다고 했다.
  • ex) 데이터 저장, 에러 처리, 로직 처리, 서버 요청 등...

vs

클린아키텍처

  • ViewModel은 더 이상 로직을 직접 처리하지 않는다.
  • 로직은 usecase 가 담당한다. (Domain Layer)
  • ex) 사용자가 버튼을 누르면 ViewModel은 해당 유스케이스를 호출하기만 함

검증, 처리, 저장 같은 ‘로직’은 Domain 계층이 맡게 되면서 ViewModel 의 책임이 분산되는 것!


(2) Model

MVVM

  • Model이 비즈니스 처리를 모두 하는 경우가 많다.
  • ex) 서버 요청, DB 저장, 데이터 가공, 유효성 검사 등

vs

클린 아키텍처

  • Model의 역할을 두 개의 계층으로 나누어 더 세분화한다. (Domain, Data)
  • Domain Layer
    • 비즈니스 로직만 담당
    • 외부 시스템(DB, API)이 없어도 동작 가능한 순수 로직만 위치한다.
    • ex) 이메일 주소 형식이 맞는지 검증, 사용자 권한 확인, 로그인 처리 등
  • Data Layer
    • 실제 데이터를 외부에서 가져오고 저장
    • 외부 연결은 모두 이 계층이 담당 (API 호출, SQLite 접근 등)
    • ex) 서버에 요청을 보내거나 DB에서 값을 꺼내는 부분

(3) UseCase

  • MVVM만 사용할 때에는 별도로 존재하지 않았던 계층.
  • 앱이 제공하는 "하나의 기능 단위 로직"을 담당
  • UI와 인프라(DB, API 등) 모두와 독립적인, 순수한 로직 계층.
  • ex) 사용자 이메일을 변경한다, 할인을 적용한다, Todo 완료 처리 등

요약

  • MVC는 Controller의 비대화 및 결합도 문제로 대규모 프로젝트에는 한계가 있다.
  • MVVM은 View와 Model 간 결합도를 줄이지만, ViewModel 비대화라는 새로운 문제가 있다.
  • MVVM + 클린 아키텍처를 함께 사용하면 각 계층의 책임을 명확히 분리하고 비대화 문제를 효과적으로 해결할 수 있다.

이제... 아키텍처에 대해 잘 이해했으니 새 프로젝트 구조를 잘 짜기만 하면 된다 ^^!

profile
플러터와 안드로이드를 공부합니다

0개의 댓글