아키텍처 설계에 대해 고민을 하기 시작했다. 최근 사이드 프로젝트를 새로 진행하면서 구조를 짜게 된 덕분이다. 특히 흔히 사용되는 MVVM + 클린 아키텍처를 적용하려면 제대로 이해하고 정리할 필요성을 느꼈다.
사실 앱 개발을 처음 배울 때, 단순히 "요즘은 MVVM을 많이 쓴다"는 흐름을 따라 MVVM 패턴을 적용하기 시작했다. 그러나 실제로 적용하면서 깊은 고민은 뒤로 미뤄두고 있었다. 바쁘다는 핑계... 멈춰...
그래서 이번 기회에 MVC, MVVM, 그리고 클린 아키텍처를 비교할 뿐만 아니라 처음 접했을 때의 고민들, 해당 패턴들이 어떻게 그 개선점이 되는지 궁금했던 점에 대해 글로 상세히 정리해보았다.
MVC 건너뛰고 MVVM을 적용하면서 자연스럽게 든 의문이 있었다. 나는 한 파일에 300줄 넘어가면 길다고 생각하는 편인데, 음... 기능이 추가될수록 코드가 길어졌다.
"분명 MVC의 Controller가 비대해져서 MVVM으로 넘어왔다고 하는데, ViewModel 파일 코드도 길어지는데...?"
"중간에 Controller 대신 ViewModel이 있는 것뿐, 결국 이름만 다르고 비슷한 거 아닌가?"
이게 클린 아키텍처가 적용되는 이유라는 건 나중에야 알았다. 사실 초기에는 "MVVM vs 클린 아키텍처" 둘 중 하나를 선택해야 한다고 오해했었다. 각 패턴의 목적과 역할을 제대로 이해하지 못했기 때문인 듯. 이 둘은 상호보완적으로 함께 사용된다.
아무튼 MVC 와 MVVM 이 뭐가 다른지 본격적으로 궁금해지기 시작했기에 하나씩 살펴보기로 했다. 우선 MVC 부터 보자. 직접 비교해봐야 MVVM 을 애용하는 이유를 잘 알 수 있을테니까...
MVC = Model, View, Controller 의 약자이다. 각 역할은 다음과 같다.
(코틀린 코드입니다)
// [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() // 이메일 입력 받고 갱신
}
보면 알겠지만 Controller 가 필요에 따라 Model을 "직접 수정"한다. 수정된 Model을 바탕으로 Controller가 View를 업데이트한다. Controller가 내용을 직접 수정하기 때문에 View와 Model 모두에 강하게 결합되는 것! 이 부분이 MVVM 과 비교되는 가장 큰 차이점이다.
MVVM 에서는 ViewModel이 Model을 간접적으로 수정한다. View는 ViewModel을 관찰(observe)하거나 바인딩(binding)하여 자동으로 갱신된다. 중개자 역할만 하기에 결합도가 약해진다는 것.
MVVM = Model, View, ViewModel 의 약자이다. 각 역할은 다음과 같다.
(코틀린 코드입니다)
// [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()
}
아까 위에서 말했듯이, MVVM 의 가장 큰 특징은 ViewModel 이 View를 직접 알지 못한다는 것이다. 이름 그대로 관찰(observe) 구조 기반으로 동작한다. 상태를 던져주고 어떻게 사용할지는 View에 위임임한다.
이렇게 되면 ViewModel은 단지 상태를 관리하고 노출할 뿐, View가 어떻게 그 상태를 사용하는지 알 필요가 없다. 이는 Controller가 View와 Model 양쪽을 직접 제어하는 MVC와 가장 큰 차이점이다.
ViewModel이 너무 바빠진다
버튼을 누르면 이메일을 바꿔야 해 → ViewModel이 처리
이메일 바꿀 때 서버에 요청도 해야 해 → ViewModel이 처리
요청 실패하면 에러도 보여줘야 해 → ViewModel이 처리
사용자 정보도 저장해야 해 → ViewModel이 또 처리
MVC에서는 View와 Controller가 서로를 직접 참조하거나 Model을 직접 수정하는 일이 많아지면서, 구조가 복잡해지고 테스트가 어려워지는 문제가 있었다.
MVVM 을 통해 View와 Model 간의 결합도는 확실히 줄어들었지만, ViewModel이 지나치게 많은 역할을 하게 되면서 비대해지는 문제는 여전히 발생한다.
ViewModel이 데이터 준비, 사용자 이벤트 처리, 비즈니스 로직 등을 모두 떠맡다 보면 결국 수백 줄짜리 코드가 한 클래스에 몰리게 되고, 유지보수가 어려워지는 것이다.
즉, 결합도는 낮췄지만, 책임은 과하게 몰렸다고 볼 수 있다.
MVVM만으로는 ViewModel의 책임이 지나치게 커지는 문제가 있었다. 이를 해결하기 위해 등장한 것이 클린 아키텍처(Clean Architecture)이다.
클린 아키텍처는 소프트웨어 구조를 여러 계층으로 나누어 각 계층이 명확한 역할만 수행하도록 하는 설계 방법이다.
쉽게 말해 "일을 나눠서 하자"는 것이다.
일반적으로 Presentation, Domain, Data의 세 계층으로 구성되며, 각 계층은 서로 느슨하게 연결되어 있다.
사용자와 직접 상호작용하는 부분.
ex) View, ViewModel
핵심 로직 계층. 애플리케이션의 핵심 비즈니스 로직이 존재하는 부분. UI나 데이터 저장소에 의존하지 않음.
ex) Usecase, Repository interface
외부와 통신하는 계층. 서버 API, 로컬 DB 등 구체적인 데이터 소스와 연결되는 부분이다.
Domain Layer에서 정의한 Repository 인터페이스를 실제로 구현하여 데이터를 제공한다.
ex) ApiService, LocalDatabase, RepositoryImpl
이렇게 설명만 들으면 진짜 헷갈린다. 역할을 비교해보자.
MVVM만 썼을 때 vs 클린 아키텍처까지 도입했을 때
MVVM
vs
클린아키텍처
검증, 처리, 저장 같은 ‘로직’은 Domain 계층이 맡게 되면서 ViewModel 의 책임이 분산되는 것!
MVVM
vs
클린 아키텍처
이제... 아키텍처에 대해 잘 이해했으니 새 프로젝트 구조를 잘 짜기만 하면 된다 ^^!