안녕하세요. 이번 포스트는 MVVM 아키텍처를 사용할 때 Combine framework로 input/output을 정의하기 전에 MVVM과 MVC의 차이점을 파해쳐보면 좋을 것 같아 글을 따로 작성하게 되었습니다. 소프티어 부트캠프 활동을 하며 새로 알게된 내용이 많아 정리도 할 겸 글을 작성하게 되었습니다.
중요. 가장 중요하다고 생각이 드는 MVC와 MVVM의 차이가 무엇인지 생각하기
MVC와 MVVM의 차이점이 무엇일까요? 지금 당장 차이점을 모른다면 ViewModel을 선언해서 사용 할 이유도 없고 input/output binding으로 흐름을 제어하는 이유가 없습니다. 굳이 MVVM을 사용하는 이유도 없습니다. 제 글을 통해 조금이라도 도움이 되셨으면 좋겠지만 제가 표현을 잘 못해서 글이 조금 길어졌네요......
"누군가 하니까?"는 면접관 분들이 좋아하지 않는다고 합니다. 예전에는 그냥 유데미? 강의에서 해외 개발자가 써서!! 그리고 주변 사람들이 MVVM 패턴을 써서 MVC만 쓰다가,, "MVC보다 트랜디한 아키텍처인가?"라는 생각이 들어 유데미 강의로 공부하면서 막 적용하려고 애를 스며 프로젝트에 적용했습니다.
지금까지 공부하며 "MVVM 마스터했다"는 생각이 들었습니다. 그래서 이 글을 23.04.07일에 작성했었는데 3개월 뒤!! 소프티어 부트캠프를 활동을 하게 됬습니다. 활동을 하면서 여러 고민을 하고 OOP, POP, Unit test등의 개념들을 자연스럽게 접하면서 MVVM의 차이점도 파악하게 됬습니다. 지금부터 제 주관적인 분석을 토대로 자세하게 써나가려고 합니다.
주의! 제 글의 내용에서 틀린 부분이 있을 수 있습니다. 댓글로 알려주신다면 정말 감사히 배우겠습니다.
MVC와 MVVM 패턴에서 서로 layer간 대화하는 형식을 그려봤습니다. 여기서 대화란 데이터가 전달된다거나 사용자의 이벤트가 전달된다거나 서버에서 응답 요청이 온다거나 등등을 생각하시면 좋습니다.
"ViewController와 ViewModel! 단지 이름만 다르지, 위치도 같고 대화하는 경우도 같은데" 도데체 무슨 차이가 있을까요?....
먼저 기억해야 할 것은 '네모' 를 layer라고 구분지어 호칭할 것입니다. 여기서 네모의 의미를 부여하자면, "View와 ViewController, Model간에 경계(layer)가 있고, 특정한 네모들은 서로 대화를 할 수 있다"라고 생각하시면 됩니다. 그 외의 네모들은 대화를 직접 하지 않습니다. 서로 소통하지 않는 네모들은 중간에 ViewController 네모나 ViewModel 네모를 거쳐야 합니다.
위 그림에서 공통점을 살펴보겠습니다.
1. ViewController, ViewModel은 View와 Model간에 대화를 합니다. 위 그림만 봐서 역할이 똑같아 보입니다.
2. View는 Model과 대화 하지 않습니다.
3. View가 Model과 소통을 하기 위해서는 ViewController or ViewModel을 거쳐야 합니다.
이제 차이점을 알려면 각 layer간 의존(의존성) 관계를 확인해야 합니다. 의존 관계는 화살표로 그려질 수 있겠습니다: ] 이게 제일 중요한 것 같습니다.
MVC | ViewController
의존성 측면에서 ViewController는 View도 갖고, Model도 인스턴스로 갖습니다(Own).
View에서 발생하는 user interaction은 ViewController가 깊게 관여합니다. 뷰가 로드되었을 때도 뷰컨트롤러의 life cycle이 호출됩니다. 땔래야 땔 수 없습니다. SwiftUI는 잘 모르겠네요(아직 공부를 안했는데,,) 그래서 오른쪽 사진과 같이 ViewController는 View에 정말 많은 것을 관여하기 때문에.. realistic MVC 처럼 표현이 되는 사진을 볼 수 있습니다. 뷰도 마찬가지입니다.
여기서 주요깊게 봐야할 것은 ViewController라는 클래스 내부에 View도 있고, Model도 있습니다. "뷰 컨트롤러"라는 클래스 내부에 인스턴스로 "뷰, 모델" 두 layer의 인스턴스를 소유한다는 것은 ViewController layer는 두 layer에 의존성이 생긴다는 것입니다.
MVVM | ViewModel
사실 '대화' 가 아니라 의존성에 따라 화살표를 그렸을 때 (점선은 일단 스킵하는 것으로 가정한다면)
View는 ViewModel을 소유한다.
ViewModel은 Model을 소유한다.
class MyView: UIView{
/// 강한 결합. MyViewModel은 구체타입. 여기서 추상타입이 되려면 ViewModelable을 타입으로 했어야 합니다.
private let vm: MyViewModel = .init()
}
protocol ViewModelable { }
class MyViewModel: ViewModelable {
private let model: MyModel = .init(...)
}
여기서 의존성이 생긴다는 의미는 어느 한 객체(View/ViewController)가 다른 객체(ViewModel)를 소유할 때 상위 객체(View/ViewController)는 하위객체(ViewModel)에 의존적이다! "의존성이 생긴다"라는 뜻입니다. (주의할건 상속이랑 햇갈리면 안되는,, composition의 개념!) 이때 상위 객체가 하위 객체를 인스턴스로 가질 때 타입이 구체타입일 경우 강한 결합이 형성됩니다.
ViewContoller와 ViewModel 차이
차이점은 MVC패턴에서 ViewController라는 layer(계층, 영역)은 View도 소유하고 Model도 소유한다는 점입니다. 그리고 View는 view lifecycle, user interaction 과 UIResponder 등 알게 모르게 땔 수 없을 정도로 ViewController와 밀착되어 있습니다. 만약 모델의 데이터를 갱신해야 한다면? 모델을 소유하는 ViewController가 전담하게 됩니다.
뷰 컨트롤러 내부에는 뷰(1...n개)와 모델(0...n개)관련 프로퍼티들을 여러 개 갖고 있을 수 있습니다. 뷰 컨트롤러에서 뷰 컨트롤러가 수행해야 할 특정 로직이 들어간 함수를 테스트하려고 할 때, 그 함수 테스트를 위해 뷰 컨트롤러를 생성해야 합니다. 이때 뷰 컨트롤러를 생성하기 위해 내가 원하는 테스트와 상관없는 뷰 관련 인스턴스, 모델 관련 인스턴스들도 전부 생성해줘야 한다는 점입니다. ViewController는 View와 Model과 강하게 결합되어 있기 때문입니다.
반면 MVVM 패턴에서 ViewModel은 View를 소유하지 않습니다. ViewController보다 가볍기 때문에 뷰 모델에 있는 로직을 테스트할 때도 손쉽습니다. 재사용에도 편합니다.
저는 그래서,, 의존관계로 두 아키텍처의 차이를 설명할 수 있다는 생각이 들었습니다.
MVC와 MVVM 간 차이는 layer간 의존 관계라고 생각합니다. ViewController layer는 View도 갖고 있고 model도 갖고 있습니다. 즉. 뷰 컨트롤러라는 객체는 1...n개의 뷰와 0...m개의 모델 인스턴스가 반드시 필요로 한다는 것(의존) 입니다.
ViewController라는 클래스는 UI 로직과 비즈니스 로직 전부 갖고 있습니다. 그렇기에 unit test 하기엔 너무 많은 의존성(view, model etc..)들이 있습니다. 뷰 컨트롤러를 테스트하기 위해서는 내부에 선언된 모든 UIView 컴포넌트도 초기화 해야하는 등.. 고려해야 할 것이 많고 unit test 범위를 벗어나기 때문입니다. (삐빅.. SPR 어렵습니다)
MVVM 패턴은 위에 올린 사진 처럼 MVC와 의존 방향이 다릅니다.
어떻게 하위 객체가 상위 객체를 소유하거나 참조하지 않으면서 notify를 할 수 있을까요?
MVVM 패턴의 꽃은 바인딩입니다. 바인딩 한다는 것은 하위 객체가 상위 객체의 존재를 모르지만!!!! 상위 객체에게 알림을 보낼 수 있다는 것입니다.
엥..?!
하위 객체(ViewModel)가 상위 객체(View)의 인스턴스를 소유하고 있지 않은데, 상위 객체에게 notify할 수 있는 이유 '바인딩' 때문입니다. 여기서 더 나아가 상위 객체는 View는 하위객체 ViewModel을 소유할 수 있는데, 구체타입이 아닌 protocol로 선언된 추상타입을 선언(DIP: DependencyInversionPrinciple)하고 외부에서 주입(DependencyInject)받게 되었을 때 View layer와 ViewModel layer는 강한 결합이 아니라 느슨한 결합(Loose Coupling)을 띄게 됩니다. 이 말은 View 가 추상적인 protocol 타입을 의존하기에 외부에서 프로토콜을 준수한 어느 객체든 상관없이 protocol에 의존하는 상태입니다. 자세한 것은 DIP관련 정리글을 참고해주시면 감사합니다: )
그리고 또 중요한 것은 layer간 양방향으로 데이터가 흘러간다면, 흐름을 제어하기 힘들어집니다. 그래서 이번 포스트는 MVVM에서 View와 ViewModel은 바인딩을 하는데, 단방향으로 데이터와 이벤트의 흐름을 제어할 수 있는 방법을 소개하려고 합니다 :) "단방향"의 흐름이 중요시되야 합니다. 단방향으로 데이터가 흘러간 다는 것은 코드의 의존성을 낮출 수 있고 결합도도 낮출 수 있습니다.
참고로 결합도는 특정 객체의 코드가 변경될 때, 연관된 다른 객체들의 코드가 최소한으로 변경되거나 변경되지 않을 때 베스트 결합도!! 결합도가 낮다고 표현합니다. ( DIP + DI를 통한 느슨한 결합, SPR을 만족할때라는 생각이 듭니다. 근데 더 많을수도 있지만 아직 제 지식에 한해서는 이게 최대인 것 같습니다.)
그래서 위 그림처럼 MVVM에서 단방향 흐름을 지원하기 위해서는 바인딩을 활용해야 합니다. 그리고 DIP + DI를 사용해서 객체간 결합도를 느슨하게 해야 합니다.
Why use Inputs/Outputs binding in ViewModel?
View와 ViewModel이 바인딩을 하기 위해서 kvo, closure, delegate pattern, notificationCenter등을 사용할 수 있고 RxSwift나 Combine을 통해서도 바인딩 할 수 있습니다.
Input/Outputs binding을 할 경우 그리고 DIP + DI(View가 추상타입을 의존하게 되는 것 + 외부에서 ViewModel 주입)를 준수할 경우 단방향으로 데이터의 흐름이나 상태를 제어할 수 있습니다. View에서 발생되는 user interactive 등 이벤트 흐름은 ViewModel한테 전달할 수 있습니다.
View는 ViewModel이 비즈니스 로직을 어떻게 처리하는지 데이터를 어떻게 제어하고 관리하고 요청하는지 구체적인 방법을 알 필요가 없습니다. 뷰는 그저 사용자의 이벤트가 발생하는 것을 감지하고, 데이터를 화면에 보여주기 위해 UI render에 신경쓰면 됩니다. View와 Model 사이의 관계도 깔끔하게 분리할 수 있다는 점이 있습니다.
Inputs/Outputs binding with Combine in ViewModel!!
ViewModel은 user interaction에서 발생되는 이벤트 및 view lifecycle에 필요한 비즈니스 로직을 담당해야 합니다.
위와 같이 View에서 발생되는 모든 이벤트를 View에서 발생 가능한 Input으로 정의 할 수 있습니다.
ViewModel은 View에서 발생되는 이런 Input 이벤트들에 대한 비즈니스 로직 처리를 담당합니다. 그리고 그 결과를 View에 반영되야 할 View의 새로운 state를 viewModel의 Output이라고 정의할 수 있습니다. 데이터가 변경되거나 새로 갱신되거나 삭제되는 등 기존의 화면에서 보여지는 View를 새로 그려야 하는 상황이나 업데이트 해야 할 때 Output의 한 경우로 정의할 수 있습니다. ViewModel에서 발생되는 Output를 기반으로 View는 기존의 화면에서 새로운 state에 따른 UI를 render 합니다.
그래서 비즈니스 로직이 뭘까요? 저도 처음 공부할 때 진짜 이해 안갔었는데 아래의 예를 통해 살펴보겠습니다.
간단 예로 흰색 하트를 누를 때 핑크색 하트가 보여집니다.
하트 View 관점에서 어느 역할을 담당해야 하는 것일까요? 정의는 다음과 같습니다.
1. 사용자의 터치 interaction을 감지해야한다.
2. 특정 상태(state)에 따라 화면을 핑크색으로 render(업데이트)해야한다.
3. 하트가 채워지는 애니메이션이 동작되야 한다.
4. View는 특정 이벤트를 다른 객체에게 전달할 수 있습니다.
뷰 모델의 관점에서는?
1. 특정 포스트에 대한 id를 기반으로 서버에 통신을 합니다. (서버야 나 하트 눌렀는데 특정 포스트 하트 상태를 변경해줘!!)
2. 서버에서 요청이 온다면(비동기적으로) 뷰모델은 상황에 따라 Model에 값을 변경시킬 수 있습니다. 또는 자신의 상위 객체인 View한테 빨간색으로 배경 바꾸라는 특정 state를 notify를 줄 수 있습니다.
여기서 비즈니스 로직은 서버에 하트 상태 변경, 데이터 처리 담당이 대표적이라고 생각합니다.
그럼 Model은 뭐를 하지?..
kodeco에서 advanced ios appArchitecture책을 아주 쪼금 공부할 때
var 그때당시해결하지못했던개념 = """
어???? 왜 이 그림은 Model이 ViewModel한테 notify하지? 진짜 이상하네 그림을 잘못 그린것도 아닐텐데?! Model은 값을 저장하는 구조체 아닌가? 유데미 강의에선 거의 전부 Model 폴더 안 swift 파일들은 struct 선언하고 값만 정의하던데.. 어떻게 Model이 ViewModel한테 notify하려면...... 예를들어 특정 모델이 30개의 프로퍼티를 가졌다면 특정 프로퍼티가 값이 변할 때 자신의 상위 객체(ViewModel)한테 notify하려면 각각의 변수에 대해 바인딩을 해야하나? 각각 바인딩을 하는건 좀 아닌것 같은데, 최선의 방법으로 모델을 선언한 변수에 @Publihsed나 didSet 붙이면 모델에서 뭐가 바뀐지는 모르지만 변경됨을 notify하기 쉬울거같은데 .. 굳이?
근데 왜 이렇게 그렸을까? 모델(최하위 계층)은 뷰 모델(상위 계층)을 몰라야 하는데, 데이터 주려면 바인딩이나 옵저버 달아야 하는데?!?!
"""
저는 예전엔 Model이 단순 값만 저장하는 struct로 구성된 layer로만 생각했고 모델 계층은 할게 진짜 없다고 느껴졌습니다.
소프티어 부트캠프 활동하며 이전에 해결하지 못한 고민을 해결할 수 있을지,, 여러 질문?!을 하면서 생각해봤더니 원래 뷰모델이 비즈니스 로직처리는 코드를 그렇게 짠다면 당연히 담당 할 수 있습니다.
근데 HTTP 통신을 위해서, Service or Network or Repository or UseCase 등의 layer를 정의해서 서버와의 호출에 필요한 코드 로직을 분리했다면, 그리고 그렇게 사용중이라면 MVVM 패턴에서, 이 layer는이들은 Model layer에 속할 수 있습니다. 이때 얘내가 사실 위에서 말한 비즈니스 로직을 처리한다고 생각하시면 됩니다. 사실 앱의 핵심을 구성하는!! 화면에 보여줘야 할 데이터 처리, 서버와 통신, 데이터 가공 등등!!!!이 model layer에 포함된답니다.(물론 코드를 위에 언급한 layer처럼 구성한다면)
그리고 위 코드에서 Model이 ViewModel에게 notify하는 것은! 단순 값을 저장하는 모델이 하는게 아니라,, network, service, usecas등의 모델을 사용해서 서버에서 response된 (DTO)데이터들은 ViewModel에서 completionHandler로 받는다면? 이 부분은 "Model이 ViewModel한테 notify한다"라고 표현할 수 있다는 생각이 들었습니다. 사실 notify도 아니고 그냥 비동기적으로 서버에서 불러와진 completion handler 호출해주는 정도?! 이러면 위 그림에서 notify가 해결될 수 있겠다는 생각이 들었습니다.
소프티어 부트캠프 활동을 하며 JK iOS 마스터님과 여러 iOS팀원들과 질문과 대화를 하며, 진짜 이해안가고 궁금했던 개념들을 풀 수 있었는데, 이 과정에서 제가 얻은 결론이었습니다. 근데 지극히 제 개인적인 생각이라 틀린 부분이 있을 수 있습니다. 알려주신다면 감사합니다!!!
중요! ViewModel은 그럼 진짜 하는게 뭐지..?
그렇다면 실제로 ViewModel이 하는 역할은 무엇일까요? 간단히 생각하자면, 모델로부터 데이터를 받아서, 누군지 모르겠지만!!!! 자신한테 바인딩한 상위 객체(view layer)에게 전달합니다.
또는?! 뷰로부터 발생하는 이벤트를 받고, 모델에게 전달합니다.(그럼 모델은 서버에 하트눌렸다고.. "디비야 고객님이 우리 앱 사용중이다 일해라!!")
또는?! 모델로부터 notify받고 새로 갱신된 데이터가 기존에 보여지는 화면이 새로 바뀌어야 한다면.. state를 업데이트 하라고 상위 객체(view)에게 notify합니다. (상위 객체가 뭘 하던 말던 알려주기만 합니다. 뭐 데이터도 보낼 수 있지만 상위 객체가 이 데이터를 갖고 뭐 하는지는 모릅니다.)
또는 비즈니스 로직 처리,,?
우와!!! 그래서 사실 이 경우에는 뷰에 대한 user interaction이나 model을 기반으로 데이터를 화면에 어떻게 보여줄지 제어하는 등 ViewModel은 View의 중간다리가 될 수 있습니다. (ViewController처럼)
근데 차이점은?!??!!?
ViewModel은 플렛폼에 종속적이지 않습니다!! 즉 UIKit을 상속받은 객체가 없어야 합니다. 플렛폼에 종속적이지 않는다는 것은 ViewController보다 가볍다! ViewController보다 testable한 코드라고 할 수 있습니다.
어머,, 설마 그렇다면... View를 위한 ViewModel을 Presentation layer라고 부르던데?! 비즈니스 로직을 포함한 Model layer를 더 세부적으로 Domain과 Data layer로 분리해 의존성을 바깥!에서 안!으로 향하도록 설계한다면? 클리느 아키텍쳐?!?!
본인: "뷰는 화면에 그릴 속성 정의하고, 화면에 보여주고 때로는 업데이트해서 다른 형태로 보여줘! 그리고 모델은 값을 저장하거나 서버에서 비즈니스 로직 처리합니다 그럼 뷰모델이 너무 가벼운데요?"
이에 대한 예시로 방금 감자 튀김 먹다 떠올랐습니다.
뷰는 "캐챱과 함께 바삭한 감자 튀김"을 화면에 보여주길 원합니다. 때마침.. 서버에서 흙묻은 감자가 Model을 통해 ViewModel로 전달됬다고 notify했습니다. 오!! 흙묻은 감자도 줬군,, 오감자?!
ViewModel은 이 감자를 씻고 잘게 쪼개고 기름에 튀겨서 "보기 좋은 감자튀김"의 형식으로 바꿉니다.
이러면 좋겠지만.. 뷰에서 사용하는 "캐챱과 함꼐 바삭한 감자 튀김" 형식과 다릅니다. 캐챱과 함께 바삭한 감자 튀김을 만들기 위해서는 우리 앱만의 비즈니스 로직이 있습니다.
감자는 5도 이하의 차가운 물로 씻어야 합니다. 감자를 썰 때, 크기는 0.5cm으로 썰어야 합니다. 최대한 길게 썰어야 합니다.
튀길 때는 150도에서 약 6분 13초간 튀겨야 합니다. 중요한 건.. 바삭하게 튀기고 다 튀기면 기름 탁탁 털어 없애야 합니다. 그리고 케챱을 추가합니다.
이런 로직을 뷰컨이나 뷰에서한다면 코드가 길어집니다. 뷰모델에서 한다면 나중에 위에 작성한 로직을 따로 테스트하기도 좋아보입니다. (MVVM에서 비즈니스 로직에 해당되는 것 같네요,,) 뷰는 이렇게 가공된 감자 튀김의 형식을 화면에 보여주게 됩니다!
너무 추상적이네요... 실제 경험을 좀 더 쌓아서 구체적 예시로 채워넣어야겠습니다.
MVC와 MVVM의 차이도 알았겠다.. MVVM Input/ Ouptut with combine 을 정리(관련 포스트 링크)했는데 한번 봐주시면 감사합니다 :)