제목: What are MVP and MVC and what is the difference?- StackOverflow


Q. RAD(드레그-드롭과 구성)을 넘어 볼때, 사용자 인터페이스를 만드는 방법에서 많은 툴이 지향하는 방법은 Model-View-ControllerModel-View-Presenter, Model-View-ViewModel 이 세가지 디자인 패턴으로 이해되었다. 내 질문은 세가지이다.
  1. 이 패턴이 해결할 이슈들은 무엇인가?
  2. 이것들은 어떻게 비슷한가?
  3. 이것들은 어떻게 다른가?


A.
Model-View-Presenter
MVP에서는 프레젠터가 뷰를 위한 UI 비즈니스 로직을 담고 있다. 뷰에서 나온 모든 호출은 프레젠터로 직접 델리게이트한다. 프레젠터는 뷰와 바로 분리되있고 인터페이스를통해 이야기한다. 이것은 유닛테스트에서 뷰를 목(mock) 할 수 있게 해준다. MVP의 한가지 공통된 특징은 양방향 디스패치가 되야한다는 것이다. 예를들어 누군가 "저장" 버튼을 누를때 이벤트 핸들러는 프레젠터의 "OnSave" 메소드에 델리게이트한다. 저장이 완료되면 프레젠터는 인터페이스를 통해 뷰에게 콜백하여, 뷰는 저장이 완료되었다고 표시할 수 있다.

MVP는 웹 폼(Web Form)에서 분리된 표현을 달성하기에 매우 자연스러운 패턴이되는 경향이 있다. 그 이유는 뷰가 항상 ASP.NET 런타임에의해 가장 먼저 만들어지기 때문이다. 여기서 더 다양한 종류에대해 확인할 수 있다.

두가지 주요 종류
수동적인 뷰(Passive View): 이 뷰는 가능한 멍청하고 거의 로직을 가지고 있지 않는 뷰이다. 프레젠터는 뷰와 모델에게 말을 하는 중간자 역할을 한다. 뷰와 모델은 서로 완전히 막혀있다. 모델이 이벤트를 만들어내지만, 프레젠터는 뷰를 생신하기위해 그것을 구독(subscribe)한다. 수동적인 뷰에서는 직접적인 데이터 바인딩은 없고, 프레젠터가 데이터를 셋(set) 하는데 사용되는 뷰의 세터(setter) 프로퍼티로 노출시킨다. 모든 상태는 뷰가 아닌 프레젠터에서 관리된다.
  • 장점: 최대의 테스트성; 뷰와 모델의 분리가 명확하다.
  • 단점: 모든 데이터 바인딩을 여러분 스스로 해야하는, 더 많은 일거리(예를들어 모든 세터 프로퍼티들).

감독 컨트롤러(Supervising Controller): 프레젠터가 사용자 제스처를 다룬다. 뷰는 데이터 바인딩으로 모델을 직접 바인딩한다. 이 경우, 모델을 뷰에 보내주는게 프레젠터의 일이라서 바인딩할 수 있다. 이 프레젠터는 버튼 누르기, 화면 이동 등 제스쳐를 위한 로직을 담고 있을 것이다.
  • 장점: 데이터 바인딩을 이용하여 코드의 양을 줄인다.
  • 단점: 더 낮은 테스트성(데이터 바인딩 때문에), 모델에 직접 말하기 때문에 뷰는 더 낮은 캡슐화가 된다.

Model-View-Controller
MVC에서 컨트롤러는 앱 로딩같은 어떤 액션에 반응하여, 어떤 뷰가 표시될지 결정하는 책임을 가진다. 이것은 액션이 뷰를 통해 프레젠터로 라우트한다는 부분이 MVP와 다른 점이다. MVC에서는, 뷰에서의 모든 액션이 컨트롤러에 호출하여 상호 관련이 있다. 웹에서는 각 액션이 응답할 컨트롤러가있는 다른편에서 URL 호출을 포함한다. 컨트롤러가 그 처리를 완성하면, 올바른 뷰를 돌려줄 것이다. 이런 순서는 어플리케이션의 라이프 내내 그 방법으로 계속된다.


Action in the View
     -> Call to Controller
     -> Controller Logic
      -> Controller returns the View



MVC에대해 한가지 크게 다른점은 뷰가 모델을 직접 바인딩하지 않는다는 것이다. 뷰는 간단하게 랜더링만하고 완전한 상태없는(stateless)것이된다. MVC의 구현에서 뷰는 보통 코드 뒤에서 로직이 하나도 없다. 이것은 절대적으로 필요한 MVP와 상반되는데, 그 뷰가 프레젠터에게 델리게이트하지 않으면 절때 호출되지 않을 것이기 때문이다.

Presentation Model
우리가 볼 또다른 패턴은 프레젠테이션 모델 패턴이다. 이 패턴에는 프레젠터가 없다. 대신에 뷰가 직접 프레젠테이션 모델을 바인딩한다. 그 프레젠테이션 모델은 뷰를 위해 면밀하게 만들어진 모델이다. 이 의미는 이것이 seperation-of-concern의 위배일 수 있으므로, 모델은 절때 도메인 모델일 수 없는 프로퍼티들을 호출시킨다. 이 경우, 프레젠테이션 모델은 도메인 모델을 바인딩하고, 모델에서 나오는 이벤트를 구독할 것이다. 그럼 뷰는 프레젠테이션 모델에서 나오는 이벤트를 구독하고 적절히 스스로 갱신한다. 프레젠테이션 모델은 뷰가 액션을 호출하는데 사용하는 명령을 노출시켜 놓을 수 있다. 이런 방법의 이점은, 프레젠테이션 모델이 완전히 뷰를 위한 모든 동작을 캡슐화하기 때문에, 본질적으로 코드 뒤에서 함께있는 것을 제거할 수 있다. 이 패턴은 WPF 어플리케이션에서 사용하기에 강한 후보이고, 또한 Model-View-ViewModel이라 부르기도 한다.



A. 얼마전에 이것에대해 글을 썼는데, 이 두가지 차이점을 훌륭하게 포스팅한 Todd Snyder 글 을 인용한다.

여기에는 패턴간의 핵심적인 차이가 있다.
MVP 패턴
    • 뷰가 모델에 더 느슨하게 연결되있다. 프레젠터는 모델을 뷰에 바인딩할 책임을 가진다.
    • 뷰와의 인터렉션이 인터페이스를 통하기 때문에 유닛테스트하기 더 쉽다.
    • 보통 뷰:프레젠터는 1:1로 맵핑된다. 복잡한 뷰는 여러 프레젠터를 가질 것이다.
MVC 패턴
    • 컨트롤러는 행동 기반이고, 뷰를 통해 공유될 수 있다.
    • 표시를 위해 어떤 뷰를 선택할지 결정하는 책임일 수 있다.

내가 찾은 것중에 웹에서는 최고의 설명이다.


A. 이건 디자인 패턴의 여러 종류를 과하게 간단하게 만든 그림이긴 하나, 두가지 차이를 생각하기에는 좋아보인다.

MVC
MVC

MVP
enter image description here



이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.

제목: Looking at Model-View-Controller in Cocoa

This article is copyright 2017 Matt Gallagher, https://cocoawithlove.com. The original English version is available here: Looking at Model-View-Controller in Cocoa. This translation is produced and hosted with permission of the original author.

애플의 문서에의하면, 코코아 어플리케이션에서 표준 패턴을 Model-View-Controller라 부른다. 그 이름에도 불구하고 이 패턴은 기존의 smalltalk-80의 Movel-View-Controller 정의와 완전히 다르다. 코코아의 앱 디자인 패턴은 실제로 원래의 Smalltalk의 용어보다 Taligent(1990년대부터 애플과 공동으로 개발해온 프로젝트)에서 만들어진 방법에 더 일반적으로 겹친다.

이 글에서는 코코아에서 주로 사용하는 앱 디자인 패턴의 뒷 배경과 약간의 이론을 보려한다. 나는 코코아의 Model-View-Controller 접근법의 주요 결점에대해 이야기해볼 것이다. 이런 결점을 해결하는데 실패한 애플의 노력과, 다음 메이저 개션으로부터 생기는 의문을 보게 될 것이다.

컨텐츠
  1. Smalltalk-80
  2. 코코아(AppKit/UIKit)
  3. Taligent
  4. 컨트롤러 문제
  5. 바인딩
  6. 새로운 무언가?
  7. 결론

Smalltalk-80
아마 UI 개발에서 널리 인용된 패턴은 Model-View-Controller(MVC)이다. 또한 많이들 잘못 인용되고 있다. 나는 MVC가 이것처럼 아무것도 아니게 되버렸다고 설명한 것을 보는데 시간가는줄 몰랐다 - Martin Fowler, GUI Architectures

나는 Martin Fowler가 사용한 정의로 위의 인용에서 Fowler가 말하고자하는바를 빠르게 알려주려한다. 코코아 앱 개발에 보통 사용되는 이 접근법은 Model-View-Controller가 아니다.

Smalltalk-80에서는 상호 소통하는 뷰가 완전히 분리된 두 오브젝트로 쪼개진다. 바로 뷰 오브젝트와 컨트롤러 오브젝트이다. 뷰 오브젝트는 화면 출력을 실행하나, 모든 클릭이나 인터렉션은 뷰 오브젝트에서 하지 않고, 대신 그 파트너인 컨트롤러 오브젝트에의해 디스패치된다. 중요하게 이해하고 넘어가야할것은 컨트롤러는 뷰를 불러오거나 셋업을 관리하지 않으며, 한 컨트롤러는 여러 뷰를 위한 동작을 다루지 않는다; 원래 Model-View-Controller 정의에서는 뷰와 컨트롤러는 간단하게 동작하고, 화면상의 하나의 조작면에서만 출력한다.

Smalltalk's version of Model-View-Controller
Smalltalk-80의 Model-View-Controller

Smalltalk-80의 Model-View-Controller의 다이어그램은 오브젝트 그래프의 중앙에 모델이 있고, 모델이 뷰나 컨트롤러와 직접 우선적으로 소통한다는 것을 보여준다.

이 명확한 패턴은 Smalltalk-80이 어떻게 사용자 입력을 처리했는지 반영하며, 현대의 프로그램에는 이 명확한 패턴을 사용하는데 조금만 필요로 한다. 이런 의미에서, 어떠한 현대의 프레임워크도 Model-View-Controller가 아니거나, 혹은 용어의 정의가 다른 의미로 바뀌어가고 있는 것이다.

코코아(AppKit/UIKit)
코코아가 Model-View-Controller를 논할때, 대부분 어플리케이션 설계에서 분리된 표시와 컨텐트의 개념을 일깨우려고 노력하고 있다(이 방법은 모델과 뷰가 분리되게 설계하고, 구성에서 느슨하게 연결되있는 것이다). 사실 코코아만 Model-View-Controller를 이런식으로 사용하는게 아니다. 현대의 많은 이 용어의 사용이 원래의 Smalltalk-80 정의보다는 분리된 표시를 전달하기 위함이다.

코코아가 실제로 쓰고있는 정확한 패턴을 보면서 애플의 코코아 참조 가이드가 사용하는 Model-View-Controller가 어떻게 생겼는지 보자.

Cocoa's version of Model-View-Controller
코코아의 Model-View-Controller

주의해야할 중요한 점은 컨트롤러가 오브젝트 그래프의 중앙에서 대부분 소통을 컨트롤러를통해 한다는 것이다. 모델이 그래프의 중앙에 왓던 Smalltalk-80와는 다르다.

코코아는 앱에서 이 패턴을 강요하진 않지만, 모든 어플리케이션 템플릿으로 강력히 내포하고 있다. NIB 파일로부터 불러오는 것은 NSWindowController/UIViewController 사용을 강력하게 지향한다. NSTableView/UITableView나 그 관련 클래스의 델리게이트 필요 조건은 전체 표시의 책임을 이해하는 조정자 클래스를 강하게 의미한다. UITabBarController와 UINavigationController 같은 클래스들은 뷰를 조정하기위해 명시적으로 UIViewController인스턴스를 필요로 한다.

Taligent
학술적 토론에서, 코코아가 Model-View-Controller로 부르는 그 패턴은 모통 Model-View-Presenter라 불린다. 이 두가지는 코코아가 컨트롤러라 부르는 것을 프레젠터라 부르는 것만 빼면 동일하다. "프레젠터"라는 이름은 화면을 셋업하고 동작을 중재하는 역할을 맡는다. 몇몇 케이스에서는 프레젠터 오브젝트를 "감독 컨트롤러(Supervising Controller)"라 부르기도 한다. (왜 Model-View-Supervising Controller가 Model-View-Controller로 다시 돌아오게 되었는지 이해할 수 있을 것이다)

Model-View-Presenter라는 용어는 Taligent에서 기원된다. 일반적으로 많이 인용된 논문은 1997년에나온 “MVP: Model-View-Presenter, The Taligent Programming Model for C++ and Java”인데, 이 모델을 구현하기위한 Taligent의 클래스들은 적어도 1995년만큼 이르게 문서화 되었다.

Taligent는 원래 코드명 "Pink"(그 방법에 사용된 색깔 색인 카드 이후)라는 프로젝트로 System 7(이것은 "Blue" 색인 카드에 해당함)를 대체하는 OS를 지원하기위해 애플 안에서 시작된 회사였다. 애플이 동등하게 운이다한 Copland 프로젝트에 집중하고 시선을 돌리는 동안, 이 프로젝트는 일련의 인기있는 개발과 관리 문제를 가졌었다. Taligent는 1998년에 막을 내리기 전까지, OS 대신에 일련의 어플리케이션 프레임워크로 CommonPoint라는 이름으로 IBM이 배포해왔다.

This Wired article from 1993 gives an interesting insight into Taligent and the apparent bloat and infighting that doomed it.

NeXTStep이 Taligent보다 앞서 나왔지만, AppKit(지금은 AppKit의 Model-View-Controller 디자인 패턴의 양상을 정의하고 있지만)에는 컨트롤러 클래스가 1996년에 NeXTStep 4 전까지 없었다(NeXTStep의 메이저 재설계와 NS가 접두에 붙는 첫번째 NeXTStep 버전이 오늘날까지도 macOS에 남아있다). NeXTStep이 Taligent의 것을 직접 빌렸는지는 모르겠다(이것이 한 점으로 수렴해버린 진화일 수도 있고, 여러 회사가 같은 인재 풀에서 고용을 했기 때문일 수도 있다).

The Taligent documentation, from 1995, is fascinating to read. The Guide to Designing Programs discusses many ideas relevant to application design, 22 years later. However, the Programming with the Presentation Framework tutorial is horrifically bad: baffling, over-technical and unapproachable.

컨트롤러 문제
코코아의 Mode-View-Controller를 Model-View-Presenter 패턴으로 이해하는것이 매우 중요한데, 이 패턴에서는 "컨트롤러 문제"라는 커다란 문제를 유발한다.

"메시브(Massive)/거대한(Huge)/자이언트(Giant) 컨트롤러"라고도 불리는 컨트롤러 문제는 코코아에 있는 컨트롤러가 여러 분리된 역할(특히 뷰가 들어있어 생기는데 기능적으로나 데이터 의존성에도 이럴 필요없다)을 가지면서 끔찍하게 커져버리는 경향의 문제이다. 많은 보통 프로젝트들은 2000줄 혹은 그 이상의 컨트롤러 하나를 가진다.

명확하게 해달라. 이 문제가 단순히 그 크기에만 있는것이 아니라, 컨트롤러가 다루는것이 커지게 되는 점이다. 코코아에서 컨트롤러는 관련된것 혹은 관련되지 않은 역할들의 한 집합체이다. 하나의 뷰 컨트롤러는 대여섯개 혹은 그 이상의 뷰의 역할이 있을테고, 각각은 구조, 구성, 데이터표시, 데이터갱신, 레이아웃, 애니메이션과 동작들을 가질것이며, 끝내 부모 뷰 컨트롤러가 된 상태 보존 역할이 있을 것이다.

상당한 규모로 이 독립적인 역할들과 상호으존적인 역할들의 집합체는 악몽의 유지보수이다. 많은 양의 코드 덩어리들은 실제 의존성을 만들고, 찾기 힘든 상호 의존적인 기능을 만든다. 컨트롤러는 항상 테스트하기 힘들지만 (커다란 앱과 고립시키기는 어렵게 만드는 묶인(bundle) 상태의 의존성 때문에) 규모와 반의존성(semi-depenency)의 문제는 모든것을 더 나쁘게 만든다.

컨트롤러 문제를 해결하는 유일한 방법은 커다란 뷰컨트롤러를 계속적으로 더 작고 간편한 컨트롤러로 리팩토링하는 것이다. 이것에는 뷰컨트롤러에서 빼내와, 여러 뷰컨트롤러를 가지도록하는 분별력있는 접근법을 설계하여 의존성을 빼내고 제거하기위해 데이터 구조를 다시 설계하고 다시 생각해야한다. 완료할 수 있을지라도 수많은 일이 있을 것이고, 각 변경마다 버그가 나오는 일반적인 위험을 가지며, 여전히 테스트하기 힘들고(모든 뷰컨트롤러가 연관되있기 때문에), 이 모든 것에도 불구하고 끝단 사용자에게는 기능을 추가해 주지도 못한다.

바인딩
애플은 그들의 Mac OS X 10.3의 코코아 바인딩을 소개함으로서 얼마동안은 컨트롤러 문제에대해 알고 있었다.

바인딩은 두 컴포넌트 사이에 경로가 만들어진 런타임이다. 이 컴포넌트들은 보통 데이터의 소스이거나 데이터의 옵저버이다. 바인딩은 명시적인 코드 경로가 필요없이 이 컴포넌트들이 변경사항을 소통할 수 있게 해준다. 대신에, 두 컴포넌트 사이에 경로는 데이터안에 정의되있다(코코아 바인딩의경우 "key-path"라 부른다). 컨트롤러를통해 뷰 상태를 조절하는 모델 프로퍼티까지 key-path를 명시함으로서, 바인딩은 컨트롤러를 통하는 코드 경로를 과감히 줄여주어서(제거하기도한다) 컨트롤러 문제를 개선시킬 수 있다.

Cocoa's Model-View-Controller with Bindings
컨트롤러를 통한 코드경로는 바인딩에의해 대체된다

그 소개 이후, 수십가지의 코코아 바인딩이 모두 잊혀진채 남아있다. AppKit에서 아직 바인딩을 사용할 수는 있지만(절때 디프리케이트 시키진 않는다) 절때 UIKit에 넣진 않았고, 뷰를 더 쉽게 프로그래밍하고자하는 그 목적은 완전히 이룰 수 없는 결과를 초래한다. 내 생각에는 직면해야할 문제를 마주하고 몇몇의 경우는 매우 잘 동작할 수 있지만(특히 NSTableView에 있는 NSArrayController로 결합되어있을때) 왜 Mac 프로그래밍을 대체하지 않았는지 이해할 수 있다.

(여러분의 뷰컨트롤러에서 더 적은 코드의) 코코아 바인딩의 핵심 이점은 인터페이스 빌더 인스팩터 패널에서 수많은 설정(configuration)을 할 수 있다는 것이다. 이것은 여러분 코드에서 기능을 찾아볼 때 혼란스러울 수 있고, (Xcode의 프로젝트 범위 검색이 XIB 파일을 검색할 수 있게 되었지만) 그래도 검색하기 어렵고, 디버깅에 힘들며(모델 데이터가 여러분의 코드를 따라가는 스택 추적 없이 변한다), 인스팩터 패널로 검색하고 싶지 않은 새로운 누군가에게 가르치는 것은 매우 힘들며, 시작할때 코드보다 더 보기 힘들어지고(XIB 파일에 주석을 달거나 재구성할 수 없다), 로컬라이제이션이 섞인(localization maxup)것과같은 인터페이스 빌더 이슈나 버전 컨트롤 머지 이슈의 늪에 빠져버릴 수 있다.

내 개인적인 견해로는 코코아 바인딩의 중대한 실패는 커스텀 변형과 커스텀 프로퍼티를 추가하는데 있어 어려움으로 남아있다. 이것들은 모두 완료될 수 있지만 변형자를 등록하고 바인딩 딕셔너리를 노출시키는 일은 지루한 일이다. 항상 바인딩 없이 뷰컨트롤러를 통해 데이터를 보내는것은 쉬워보인다. 이 의미는 바인딩이 가장 간단한 문제를 도울 수 있는 경향은 있지만 (크게 도움이 되진 않는다) 힘든 문제에는 큰 효과를 얻지 못한다.

새로운 무언가?
Mac OS X 10.3의 코코아 바인딩 이래로, 애플은 코코아 앱에 사용될 대안의 디자인 패턴을 찾는데 어떠한 명확한 시도도 해보지 않고 있다.

iOS5와 Mac OS X 10.10에서 스토리보드를 내놓았지만, 스토리보드는 현재 디자인 패턴을 용이하게 만드는 시도만큼 디자인 패턴을 바꾸기위한 시도가 아니다. 스토리보드는 NS/UIViewController 사용을 지향하면서 Model-View-Presenter 디자인 패턴을 보강한다. 스토리보드는 더 작고 더 초점이 가는 뷰 컨트롤러를 지향하고, 많은 셋업과 트랜지션의 "표현(Presentation)"을 아주 약간 줄여주는 역할이다. 그러나 인터페이스 빌더에서 구성될 수 있으므로 코코아 바인딩에 영향을 준 여러 이슈들을 보여준다.

어플리케이션 디자인 패턴에서 더 야심찬 무언가를 기대한 우리로써는, 스토리보드가 그 새로운것을 많이 제공하진 않았다.

어플리케이션 디자인에서 새로운 방법이 존재한다. 애플 바깥에는, 리엑티 프로그래밍(이것을 선택하면 많은 바인딩의 역할로 채울 수 있다), Model-View-ViewModel(변형된 섹션의 모델을 뷰에 가깝게하여 연결함으로서 컨트롤러의 작업을 줄인다), 상호 방향적 데이터흐름(unidirectional dataflow)(이것은 모든 데이터 변경을 앱 전체에 방송하고 리듀서(reducer)를 통해 데이터 변경을 함으로서 바인딩의 필요를 줄인다); 이 모든것들을 다른 사이클로 인기가 있다(원문: all of which are popular within different circles).

리엑트 네이티브(React Native)나 Swift-Elm같이 근본적으로 다른 프레임워크도 있다. 비록 스위프트나 코코아가 희생되는것이 전적으로 중요한 결점을 동반할지라도 말이다.

이런 어떤 것들이 공식적인 코코아 앱 개발에 어떤 영향을 줄지는 불명확하다. 애플은 이따금 과감하게 바꾸고 싶어 한다는 것을 스위프트가 증명했고, 스위프트는 언어면의 이점을 수용한 디자인 패턴이나 뷰 프레임워크 갈망이 점점 더 커진다는 견해가 있다. 그러나 애플은 스위프트만으로된 메이저 프레임워크를 소개하려하기 전까지는 시간이 좀 걸릴것 같다.

결론
코코아의 현재 Model-View-Controller 패턴의 원래 데이터로서 NeXTStep 4를 받아드린다면 올해(2017년)로 20년째이다. 망가지진 않았지만 결점을 가지고 있고, 한번 그렇게 했었던만큼 흥분되거나 능률적인것으로 보이진 않는다.

애플은 디자인패턴 개선을 위해 비교적 이르게 코코아 바인딩을 내놓았었다. 이런 수용은 섞였고 애플의 새 플랫폼에까지 도달하지 않아왔다(원문: Its reception was mixed and it has not been carried forward onto Apple’s newer platforms.).

AppKit 혹은 UIKit 팀에게서 내부적인 노력에대한 다른 정보는 없지만, 애플이 가까운 미래에 과감한 변화를 할 것 같지는 않아보인다. 코코아에서 어플리케이션 디자인 패턴 전반을 개선하는 목적의 써드파티 프레임워크를 쓸 수 있는 여러 디자인 패턴들이 있지만, 반드시 이중 하나가 앞으로의 방향이라는 합의는 없어보인다. 나는 이런 노력들이 어떤 종류의 개선에 관심을 반영한다고 생각한다.



이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



안드로이드 프로젝트에서 MVC, MVP, MVVM을 위한 간략한 가이드

Model View Controller(MVC)
MVC 디자인 패턴은 세가지 양상으로 앱을 쪼갠다: Model, View, Controller. 일들을 강제로 분리시켜 도메인 model과 controller 로직을 유저 인터페이스(View)로부터 분리시켜준다. 결과적으로 앱을 유지보수하기 간편하고 테스트하기 쉽게 만들어준다.

Model
Model은 비즈니스 로직(즉 비즈니스 Model)과 데이터 접근 기능(Data Model)을 담은 클래스들의 집합으로 표현한다. 또한 데이터가 어떻게 바뀌고 다뤄지는지에대한 비즈니스 규칙을 정의한다.

View
View는 UI 컴포넌트를 표현한다. View는 Controller로부터 받은 결과의 데이터를 화면에 표시하는 역할만을 가지고 있다. 또한 Model을 UI에 넣어 적용하는 일도 한다.

Controller
Controller는 들어온 요청을 처리하는 역할을 한다. Vie를 통해 사용자의 입력을 받으면 Model의 도움으로 사용자의 데이터를 처리하고 다시 그 결과를 View에 보내준다. 보통은 View와 Model 사이에 중재자 역할을 한다.

Model View Presenter(MVP)
이 패턴은 Controller 대신 Presenter가 들어간 MVC와 유사한 형태의 패턴이다. 이 디자인 패턴은 세가지 주 양상으로 앱을 쪼갠다: Model, View, Presenter


Model
Model은 비즈니스 로직(즉 비즈니스 Model)과 데이터 접근 기능(Data Model)을 담은 클래스들의 집합으로 표현한다. 또한 데이터가 어떻게 바뀌고 다뤄지는지에대한 비즈니스 규칙을 정의한다.

View
View는 UI 컴포넌트를 표현한다. View는 Controller로부터 받은 결과의 데이터를 화면에 표시하는 역할만을 가지고 있다. 또한 Model을 UI에 넣어 적용하는 일도 한다.

Presenter
Presenter는 View의 도움으로 모든 UI 이벤트를 다루는 역할이다. view를 통해 사용자로부터  입력을 받고, Model의 도움으로 사용자의 데이터를 처리한 뒤, 다시 View에 결과물을 돌려준다. View와 Controller에서와는 다르게 View와 Presenter는 서로 완전히 분리되있고 인터페이스에의해 서로 소통하는 방식이다.
또한 Presenter는 Controller처럼 들어오는 요청 트래픽을 관리하지 않는다.

MVP 패턴의 요점
  • 사용자는 View에서 상호작용한다.
  • View와 Presenter는 one-to-one 관계를 가진다. 이 의미는 하나의 View는 하나의 Presenter에 맵핑된다.
  • View는 Presenter에 참조하고 있지만, Model에는 참조하지 않는다.
  • View와 Presenter 사이에 두 방향으로 소통할 수 있다.

Model View ViewModel(MVVM)
MVVM은 Model-View-ViewModel로 정의된다. 이 패턴은 View와 ViewModel 사이에서 두방향 데이터 바인딩을 지원한다. 이것은 ViewModel에서 View에게 자동으로 변화를 전달할 수 있다. 일반적으로 ViewModel에서 View로 변화를 알림받는 옵저서 패턴을 사용한다.


Model
Model은 비즈니스 로직(즉 비즈니스 Model)과 데이터 접근 기능(Data Model)을 담은 클래스들의 집합으로 표현한다. 또한 데이터가 어떻게 바뀌고 다뤄지는지에대한 비즈니스 규칙을 정의한다.

View
View는 UI 컴포넌트를 표현한다. View는 Controller로부터 받은 결과의 데이터를 화면에 표시하는 역할만을 가지고 있다. 또한 Model을 UI에 넣어 적용하는 일도 한다.

ViewModel
ViewModel은 View의 상태를 유지, View의 액션 결과로 Model을 다루기, View 자체에서 이벤트를 트리거하는 그런 메소드나 명령, 다른 프로퍼티들을 노출시키는 역할을 한다.

MVVM 패턴의 요점
  • 사용자는 View에서 상호작용한다.
  • View와 ViewModel은 many-to-one 관계를 가진다. 그 의미는 여러 View는 하나의 ViewModel에 맴핑될 수 있다.
  • View는 ViewModel에 참조되지만 ViewModel은 View에대해 모른다.
  • View와 ViewModel의 사이에 두방향 데이터 바인딩을 제공한다.

안드로이드 구현




앞으로 "Controller"라는 용어를 앞에서 말한 Controller, Presenter, ViewModel과 같은 의미로 사용할 것이다.

일반적으로 안드로이드에서는 Activity 클래스가 Controller이고 Fragment 클래스가 View 영역이다. 그러나 이것은 코드의 재사용성을 줄인다. 또한 Fragment와 Activity는 제한적으로 화면 전환 애니메이션이 가능하다.


View 영역(layer)을 위한 UI 클래스(i.e. LinearLayout) 
View 영역은 LinearLayout이나 ViewGroup과 같은 View(UI) 엘리먼트를 상속하여 View를 구현할 수 있다.
  • Activity / 앱 플로우의 독립적인 기능을 재사용할 수 있다.
  • Activity 수를 줄인다.(앱 용량을 덜 잡아 먹는다)
  • Controller와 의존적인 부분을 줄인다.

(독립된) Controller 클래스
Controller 클래스는 어떤 안드로이드의 클래스를 상속받아서도 안된다. Activity와 Fragment로부터 독립적이게 해주어야 재사용이 가능할 것이다.
  • Controller를 가볍게 만들어라, View와 Model을 연결해주는 역할만 하면 된다.(단일 책임 원칙)
  • 이벤트를 다른 Controller에게 넘겨주어라(i.e. analytics)
  • 안드로이드 클래스로부터 분리해라 재사용을 위해

관련된 좋은 습관들
Activity 클래스의 의존성을 줄이기
  • Controller는 추상화에 의존한다(interface)
  • 시스템 구성과 의존성을 컨트롤하기위해 코드 중심에 위치시킨다.
  • 차후에 의존성 주입(dependency injection)을 옮길 수 있게 한다.

Analytics, A/B 테스트 등을 분리하기 위한 파사드(facade)
  • 우리는 여러 기록장치를 사용하기 때문에 파사드를 이용해 이 API들을 한데 모아둘 필요가 있다.
  • A/B 테스트나 다른 임시적인 기능들은 추상화돼있어야하고, 분리된 Controller나 파사드를 통해 접근가능해야한다.

이것들은 어떻게 생겼을까?
테스트에 용이
저렇게 잘 구현하여 Activity로부터 완전히 분리시킨다면, 테스트하기 아주 수훨해 질 것이다. 일반적으로 Activity는 수많은 것(디바이스의 시스템 구성, 네비게이션, 스타일, 액션바..)을 다루며 이것이 테스트의 범위를 너무 크게 만들어버린다.

위와같은 방법으로 Robelectric 테스트는 모든 의존성의 모의 객체(mock)를 만들어주고, 바깥에서 유닛 테스트 할 수 있게 해준다.

사용가능한 라이브러리&프레임워크
아래 안드로이드 프레임워크는 앞서 말한 것들을 구현할 수 있게 해준다. 우리는 지금 시점에서 저것들이 필요하지 않을 수 있으나, 나중에 코드베이스 전반에 걸쳐 적용시켜보려한다면 손쉽게 사용해볼 수 있을 것이다.

  • Square mortar: Activity 라이프 사이클의 행동으로부터 분리시켜, View를 가볍게 만들고 View를 Controller와 한 쌍으로 만들어주는 간편한 라이브러리
  • inloop AndroidViewModel: 엄청난 양의 코드 없이 Fragment나 Activity로부터 데이터와 상태를 분리시켜준다. 벙어리(dumb) View가 되는걸 줄인다.
  • sockeqwe mosby: 현대 안드로이드 앱을 위한 Model-View-Presenter 라이브러리.


'그 외' 카테고리의 다른 글

[번역] 리액티브 프로그래밍이란?  (0) 2017.03.03
(번역)Android Architecture  (0) 2016.10.01

6개월전 우리는 PlanGrid iOS앱에 Flux 아키텍처를 적용시키기 시작했다. 이 포스팅에서는 우리가 왜 전통적인 MVC에서 Flux로 갈아타게 되었는지 이야기해보고, 지금까지 겪은 경험을 공유하고자한다.

실제 제품에 코드와 함께 이야기함으로서 나는 Flux 구현의 큼직한 부분들 위주로 설명해볼 것이다. 만약 당신이 단지 고수준의 결론만 알고 싶다면, 포스팅 중간 부분은 스킵해버려도 좋다.

왜 우리가 MVC로부터 갈아타게 되었을까?
어떤 맥락속에서 우리가 Flux를 결정하게 되었는지 설명하기 위해, PlanGrid 앱이 해결해야할 과제들을 먼저 이야기 해보고 싶다. 그 중 몇몇은 엔터프라이즈 소프트웨어에 의존적이고, 나머지 대부분 iOS 앱에 적용시킬 수 있어야했다.

우리는 모든 상태를 가지고 있어야 한다.
PlanGrid는 꽤 복잡한 iOS 앱이다. 사용자에게 청사진을 보여주고 사용자들이 서로 다른 양식의 주석이나 이슈, 첨부(그리고 특정 산업 지식을 필요로하는 수많은 요소)들을 이용하여 협업할 수 있어야 했다.

또한 이 앱의 중요한 기능은 오프라인이 우선이라는 점이다. 유저들은 인터넷 연결 여부와 상관없이 앱의 모든 기능을 사용할 수 있어야했다. 이 말은 즉, 우리는 그 수많은 데이터와 상태들을 클라이언트에서 관리하고 있어야 했다는 뜻이다. 또한 부분적으로 비즈니스 정책으로서 특정 기능을 따로 실행할 수 있어야 했다(e.g. 특정 유저는 주석을 지울 수 있다던지?).

PlanGrid 앱은 iPad, iPhone 기기 둘 다에서 동작하지만, UI는 테블릿의 큰 화면에 최적화 되어있다. 이 말은 많은 iPhone 앱들과는 다르게 종종 Multiple View Controller를 한 화면에서 보여줘야 했으며, View Controller끼리 상태를 공유해야 했다.

상태 관리의 상태
우리 앱은 상태 관리라는 곳에 상당한 노력을 쏟아붇고 있다. 앱에서의 갱신은 보통 아래의 순서를 따른다.
  1. 로컬 객체에서 상태를 갱신
  2. UI를 갱신
  3. 데이터베이스를 갱신
  4. 네트워크 연결이 가능해지면 서버로 보낼 그 변화를 큐에 넣기
  5. 다른 객체에 상태 변화를 알리기

나중에 또 한번 위의 과정을 담은 우리의 새 아키텍처에대해 포스팅을 할 예정이므로 오늘은 5번째 단계에 대해서만 이야기해보자. 우리는 어떻게 상태를 갱신받아 처리할 수 있을까?

이 질문은 앱 개발시 항상 나오는 질문이다.

PlanGrid를 포함한 대부분의 iOS 엔지니어들은 다음 대답들을 내놓는다:
  • Delegation
  • KVO
  • NSNotificationCenter
  • Callback Blocks
  • 소스의 신뢰로서 DB를 이용하기
위 접근법들은 수많은 시나리오에 걸쳐 검증되었을 것이다. 그러나 수년에 걸쳐 바뀔 수 있는 커다란 코드베이스에서 수많은 옵션들이 있다면 이것은 매우 부적합하다고 할 수 있을 것이다.

자유는 위험하다.
원리의 MVC는 데이터와 데이터 표현을 분리하는 것만을 추구했다. 다른 구조적인 가이드가 부족했으므로, 나머지 모든 것들이 개발자 개인에게 떠넘겨졌다.

오랜 시간동안 (다른 iOS 앱들 처럼) PlanGrid 앱도 상태 관리를 위한 패턴을 정하지 못해왔었다.

델리게이션이나 블럭과 같은 현존하는 수많은 현존하는 상태 관리 도구는 컴포넌트 사이에 강한 의존성을 만드는 경향이 있다 ― 두 View Controller가 서로 상태 갱신을 공유하고자하면 바로 단단히 엮여버린다.

KVO나 Notofication과 같은 다른 도구들은 눈에 보이지 않는 의존성을 만들어낸다. 거대한 코드베이스의경우 그것들을 사용하면 더더욱 예상치 못한 사이드 이팩트가 발생할 수 있고, 많은 코드 수정을 해야할지도 모른다.

이러한 수많은 구조적인 이슈는 작은 모순점에서 시작되어 시간이 점차 흐르면 심각한 문제를 초례한다. 반면 철저한 코드리뷰와 스타일 가이드 만이 이 문제를 잘 해결할 수 있다. 잘 정의된 패턴이 적용된다면 미연에 그 문제를 인지하기 훨씬 쉽다.

상태 관리를 위한 구조적인 패턴
PlanGrid 앱을 리팩토링하면서 우리의 가장 중대한 목표는 깨끗한 패턴들과 최고의 습관을 만들어 놓는 것이었다. 이렇게하면 미래에 훨씬 모순 없는 방식으로 코드를 짤 수 있고, 새로운 엔지니어가 투입 되었을 때도 매우 효율적이다.

이 앱에서 상태 관리는 가장 큰 복잡함을 제공하는 원인 중 하나였고, 우리는 앞을 계속 사용할 수 있게 완전히 새로운 패턴을 정의하기로 마음먹었다.

페이스북에서 처음 Flux 패턴을 소개했을때, 그들이 말한 문제점과 우리가 현재 코드베이스에서 느낀 수많은 고통들이 강하게 매칭되었다:
  • 예측불가능하고, 순차적으로(cascading)처럼 상태가 갱신됨
  • 컴포넌트 사이에 의존성을 이해하기 쉽지 않음
  • 정보의 흐름이 엉켜있음
  • 소스의 신뢰가 불분명함
Flux는 우리가 경험하고 있던 많은 이슈를 해결하기에 적합해 보였다.

Flux로 들어가기전에 가벼운 설명
Flux는 페이스북의 웹 어플리케이션 클라이언트단에서 사용하는 경량의 아키텍처 패턴이다. 비록 참조하여 구현하였지만, 페이스북은 Flux패턴의 아이디어가 특수한 이 구현보다 더 많이 연관되있다고 강조했다.

서로 다른 Flux 컴포넌트를 보여주는 다이어그램과 함께 묘사할 수 있다:


Flux 아키텍처에서의 store는 앱의 특정 부분을 위한 정확한 단일 소스이다. store에서 상태가 업데이트되는 즉시 store를 구독하는 모든 view에 change event를 보낸다. 그 viewstore에의해서만 호출되는 유일한 인터페이스를 통해 갱신되었다는 소식을 받는다.

상태 업데이트는 action을 통해 일어날 수 있다.

action은 상태 변화를 하게 해주는 트리거지만 스스로 상태변화를 구현해놓지는 않는다. 상태 변화를 원하는 모든 컴포넌트들이 글로벌 dispatcheraction을 던진다. 이 storedispatcher와 함께 등록하고 그것들이  어디 action에 필요한지 알아내준다. action이 dispatch되면 바로 관련된 store들이  이것을 받는다.

action에 응답하는 동안 몇몇 store들은 그들의 상태를 갱신하고 새로운 상태를 view에게 알릴 것이다.

Flux 아키텍처는 위 다이어그램에서 보듯 단방향의 데이터 흐름을 행한다. 또한 엄격한 분리가 가능하다:
  • view는 오직 store로부터 데이터를 받는다. store가 갱신되면 view에 있는 메소드를 이용해 불러낸다.
  • view는 오직 action을 dispatch 함으로서 상태를 바꿀 수 있다. action은 단지 의도(intent)를 표현하는 역할이고 비즈니스 로직은 view로부터 숨겨져있기 때문이다.
  • store는 action을 받았을 때만 그 상태를 갱신한다.
이러한 제약들 덕에 새 기능을 설계하고 개발하며 디버깅하기 쉽게 만들어준다.

iOS를 위한 PlanGrid에서의 Flux
PlanGrid iOS 앱에서 우린 Flux를 약간 벗어나 구현했다. 우리는 각 store가 Observable 상태 프로퍼티를 가지고 있다. 기존 Flux 구현과는 다르게, store가 갱신될 때 change event를 보내지 않았다. 대신에 view가 store의 상태 프로퍼티를 Observe하고 있다. view가 상태 변화를 Observe하면 그들 스스로 변화를 감지하며 갱신까지 한다.


이것은 Flux 참조 구현에서 굉장히 미묘한 변경이지만 다음 섹션에서 위해 유용하게 쓰일 것 이다.

Flux 아키텍처 기반을 이해하면서, 이제 구체적인 구현이나 PlanGrid 앱에 Flux를 적용시키는 동안 필요했던 질문의 답변들을 한번 살펴보자.

store의 번주는 어디까지인가?
각 개별 store의 범주(scope)는 Flux 패턴을 처음 사용할 때 가장 먼저 떠오르는 질문이다.

페이스북이 Flux 패턴을 발표하고부터, 커뮤니티에의해 다른 변화들이 개발되어왔다. Redux는 그 중 하나인데, 각 어플리케이션당 오직 하나의 store만 가지도록 함으로서 Flux 패턴에서 번갈아가며 한 store를 사용한다. 이 store는 앱의 모든 상태를 가지고 있는다(수많은 다른, 사소한, 이 포스트 영역을 벗어난 그런것들).

Redux는 단일 store 아이디어로 수많은 앱의 아키텍처를 단순하게 해줌으로서 많은 인기를 얻고 있다. 그러나 다중 store를 사용하는 기존의 Flux에서는 조금 다른데, 특정 view를 그려야(reder)하기 때문에 다른 store에서 저장되 있는 상태를 합칠 필요가 있고, 이렇게 해야 앱이 돌아갈 수 있다. 이런 접근법은 곧바로 Flux패턴이 풀어야할 문제로 다시 떠오를 수 있다(다른 컴포넌트들 사이에 복잡한 의존성 같은).

PlanGrid 앱에서는 여전히 Redux 대신 기존의 Flux를 사용하기로 결정했다. 우리는 우리 앱이 얼마나 큰 앱이 될지 예측하지 못했기에, 앱의 모든 상태를 담은 단일 store보다는 다중 store를 선택하였다. 게다가 우리는 가장 작은 inter-store 의존성을 가지는 것을 인지했는데, 이것이 Redux를 대안에서 제외시키게 된 이유가 되었다.

우리는 아직 각 개별 store의 범주를 견고하게 만들어가고 있다.

지금까지도 나는 우리 코드베이스에서 두가지 패턴을 알아냈다:
  • 기능/view 특정 store : 각 View Controller(혹은 View Controller와 가깝게 연관된 각 그룹들)는 그것의 store를 받는다. 이 store는 view에 특화된 상태를 만든다.
  • 상태를 공유하는 store : 우리는 수많은 view들 사이에서 상태가 공유되는데, 이 상태들을 저장하고 관리하는 store를 가진다. 우리는 이 어마어마한 양의 store들을 최소화시키기위해 노력중이다. IssueStore가 그 예시인데, 이것은 현재 선택된 청사진을 볼 수 있는지 없는지에 관한 모든 이슈 상태를 관리한다. 이 이슈들을 화면에 보여주거나 소통하는 수많은 view들은 이 store로부터 정보가 나온다. 이 store의 타입은 필수로 실시간 갱신되는 데이터베이스 쿼리처럼 동작한다.
우리는 현재 상태 store에 공유된 처음 것을 구현하는 과정이고 아직 이 store 타입에서 서로 다른 view의 다중 의존성을 만드는 최고의 방법을 모색중이다.

Flux 패턴을 사용하여 기능을 구현하기
이제 Flux 패턴으로 만드는 세부적인 구현 기능들 안으로 파고 들어가보자.

다음 두 섹션에 걸쳐 예제를 보여주는데, PlanGrid 앱 제품에서의 기능들을 예시로 들 것이다. 그 기능은 사용자가 한 청사진에서 주석들을 필터링할 수 있게 해주는 것이다.


우리가 토론할 이 기능은 스크린샷의 왼편에 나타나있는 popover안에 만들어져있다.

1단계 : 상태를 정의하기
보통 나는 그것의 적절한 상태를 정함으로서 새 기능의 구현을 시작한다. 그 상태는 특정 기능의 표현응ㄹ 그리기위해 UI가 알아야하는 모든것을 나타낸다.

아래 보이는 것처럼 어서 주석 필터 기능을 위한 상태를 둘러보면서 우리 예제 속으로 들어가보자:

이 상태는 여러 필터의 리스트, 현재 선택된 필터 그룹, 어떤 필터가 활성화됬는지 지시하는 boolean 플래그로 구성된다.

이 상태는 정확히 UI에서 요구한 것이다. 필터 리스트는 Table View에 나타난다. 선택된 필터 그룹은 각 개별로 선택된 필터 그룹의 세부사항을 표시/숨김 하기위해 사용된다. 그리고 isFiltering 플래그는 UI에 버튼을 보이게할지 말지 정하는데 필터가 enabled인지 disabled인지에 따라 정해진다.

2단계 : Action을 정의하기
특정 기능을 위한 상태를 정의하고나면, 나는 보통 다음 단계에서 다른 상태 변화를 생각해본다. Flux 아키텍처에서 상태 변화는 action의 모양에 의해 만들어지는데, action은 상태 변화가 의도하는 것을 담고있다. 주석 필터 기능을 위한 action 코드들은 꽤 짧다:

그 기능의 깊은 이해 없이도 이 action이 초기화하는 상태 이동이 어떤 것인지 이해할 수 있을 것이다. Flux 아키텍처의 장점중 하나는 action 리스트는 각 기능들에의해 트리거될 수 있는 모든 상태변화를 한번에 담아낸다는 것이다.


3단계 : store에서 action으로 그 응답을 구현하기
이 단계는 기능의 핵심적인 비즈니스 ㄹ직을 구현하는 단계이다. 나는 개인적으로 이 단계를 TDD를 이용하여 구현하려하고, 나중에 TDD에대해 다시 이야기할 것이다. store의 구현은 아래처럼 요약될 수 있다:
  1. 연관된 모든 action을 dispatcher와 함께 store를 등록한다. 이 예제에선 모든 AnnotationFilteringActions이 될 것이다.
  2. 각 action들별로 호출할 수 있는 핸들러를 만든다.
  3. 핸들러와 함께 필요한 비즈니스 로직을 동작하고 완성에 상태를 갱신한다.

구체적인 예제로서 AnnotationFilterStoretoggleFilterAction을 어떻게 다루는지 확인할 수 있다:
self.annotationFilterService.applyFilter()를 호출 함으로서 시트위에 표시되는 주석들의 필터링을 실제 동작시킨다. 필터링 로직 그 자체는 다소 복잡하나, 일부를 떼어내서 옮겨놓았다.

각 store의 역할은 UI와 관련된 상태 정보를 제공하고 현재 상태를 동일하게 만들어 놓는 것이다. 그러나 이 작업을 위해 모든 비즈니스 로직을 store 안에 다 구현해라는 것은 아니다.

각 action 핸들러의 마지막 작업은 상태를 갱신하는 것이다. _applyFilter() 메소드와 함께, 어떤 필터가 활성화되어있는지 체크하여, 우리는 isFiltering 상태값을 갱신한다.

여기서 특정 store에 대해 인지해야할 중요한 사실이 하나 있다: 추가적인 상태 업데이트를 예상할 수 있다는 점인데, 이 업데이트는 AnnotationFilter에 저장되있는 필터들의 값을 갱신한다. 일반적으로 이것은 store를 어떻게 구현할 것이야는 것지만, 이번 구현은 약간 특별하다.

AnnotationFilterState에 저장된 필터들은 이전에 존재했던 Objective-C 코드와 연결되야 하므로 그들을 새 클래스로 만들기로 했다. 이 클래스는 타입과 store를 참조하고, 주석 필터링 UI는 같은 인스턴스 참조를 공유한다. 즉 store 안에서 필터에 일어나는 모든 변화는 UI의 시각적인 부분과 관계돼있다. 상태 구조체에서 값 타입을 독립적으로 사용함으로서 원래는 이러한 상황을 피하려고 해야한다. ― 그러나 이 포스팅은 실제 세계에서의 Flux 이야기이고 이 특수한 상황에서 좀 더 쉽게 Objective-C를 연결하기 위해 어느정도 타협점을 찾을 수 밖에 없었다.

만약 필터가 값 타입이면, 변화를 관찰한 UI 순서에 따라 우리 상태 프로퍼티에 갱신된 필터 값을 할당할 필요가 있다. 우리는 참조 타입을 사용하기 때문에, 대신 실체가 없는(phantom) 상태 갱신을 실행한다:

_state 프로퍼티에 할당하는 것은 UI를  갱신하는 매커니즘을 필요 없게 만든다. ― 잠시 후에 이 프로세스에 관한 세부적인 이야기를 해볼 것이다.

우리는 세부적인 구현에서 꽤 깊게 쪼개었고, 그래서 나는 이 섹션을 마치면서 store의 역할을 고수준에서 다시 한번 상기시켜보고자 한다:
  1. 필요로 하는 모든 action을 위해 dispatcher와 함께 store를 등록한다. 현재 예제에서는 모두 annotationFilteringActions이 되어야한다.
  2. 각 개별 action들을 위해 불릴 수 있는 핸들러를 구현한다.
  3. 핸들러 안에서 해당 비즈니스 로직을 실행하고 그 결과의 상태를 갱신한다.
다음으로 어떻게 UI가 store로부터 상태 갱신을 받는지 이야기 해보자.

4단계 : store로 UI를 바인딩하기
Flux 개념의 핵심 중 하나는, 상태 갱신이 나타나면 자동으로 UI를 갱신한다는 점이다. 이로인해 UI가 항상 최신 상태를 보여줄 수 있고, 수동으로 이 갱신을 유지하기 위해 필요한 어떤 코드도 만들 수 있어야한다. 이 단계에서는 MVVM 아키텍처에서 View가 ViewModel에 바인딩하는 것과 굉장히 유사하다.

이걸 구현하는데에는 사실 많은 방법들이 존재한다. ― PlanGrid에서는 ReactiveCocoa를 사용하기로 했는데, 이것을 store가 Observable한 상태 프로퍼티를 제공한다. 아래 코드는 AnnotationFilterStore가 어떻게 이 패턴을 구현했는지 보여준다.

_state 프로퍼티는 store 안에서 상태를 바꾸기 위해 사용되었다. state 프로퍼티는 store에 구독하기 원하는 클라이언트를 위해 사용된다. 이것은 store 구독자들이 상태 갱신을 받을 수 있게 해주나 이것은 직접적으로 그 상태를 바꾸게 하지는 못하게 해놓았다(상태 변경은 action을 통해서만 일어난다!).

초기화 시점에서 내부의  Observable한 프로퍼티는 간단하게 외부 시그널 producer로 간다:

이제 _state로 가는 모든 갱신에서는 자동으로 state에 저장된 시그널 producer을 통해 최신 상태 값을 보낼 것이다.

남은 것은 새 state 값을 보낼때 UI가 갱신되는지 확인하는 코드이다. 이 부분은 iOS에서 Flux 패턴을 처음  사용할 때 만든 꼼수의 부분이다. 웹에서 Flux는 페이스북의 React 프레임워크와 굉장히 잘 동작한다. React는 상태가 갱신되면 추가적인 코드가 필요없이 UI를 다시 렌더링 한다는 특정 시나리오를 전제로 설계되었다.

UIKit과 함께 작업하는 상황에서는 이 부분을 깔끔하게 해결하지 못하고 손수 UI 갱신을 구현해야한다. 이 부분에 대한 이야기는 너무 길어질 수 있기 때문에, 이번 포스트에서는 더 깊게 설명할 순 없다. 대신 최하단에 우리는 UITableView와 UICollectionView를 위해 API 형태로 제공하는 React 컴포넌트들을 만들어 놓았다. 나중에 그것에 대해 가볍게 보여주겠다.

만약 이 컴포넌트에대해 더 배워보고 싶으면 최근에 내가 말한 것을 한번 확인해보거나, 두 Github 저장소(AutoTable, UILib)를 보아도 된다.

이제 주석 필터링 기능은 다시 접어두고 실제 세상의 코드를 보자(이번에는 약간 생략되었다. 이 코드는 AnnotationFilterViewController에 있는 코드이다:

우리의 코드베이스에서 우리는 각 View Controller가 viewWillAppear: 메소드에서 부르게 될 _bind라는 메소드를 들고 있는 규칙을 가졌다. 이 _bind 메소드는 store의 상태를 구독하고 상태 변화가 일어날 때 UI를 갱신하는 역할을 한다.
 
우리는 부분적으로 UI 갱신을 우리 스스로 구현해야 했고, React스러운 프레임워크에만 의존할 수 없었으므로 이 메소드는 어떻게 특정 상태 갱신이 UI 갱신과 맵핑되는지에 대한 코드를 담고있다. 여기 ReactiveCocoa는 이 관계를 설정하기 쉽게 만들어주는 여러 오퍼레이터(skipUtil, take, map 등)을 제공함으로서, 사용하기 쉽게 해준다. 만약 이전에 Reactive 라이브러리를 사용해본 적이 없다면 이 코드가 약간 생소할 수 있다. ― 그러나 우리가 사용하는 ReactiveCocoa는 작은 부분인데다, 배우려고하면 꽤 빨리 배울 수 있다.

예제에서 첫째줄의 _bind 메소드는 상태 변화가 일어날때 Table View를 갱신하게 만든다. 빈 상태일때 갱신이 먹히지 않도록 ReactiveCocoa의 ignoreNil() 오퍼레이터를 사용한다. 우리는 Table View가 어떻게 보여질지 표현에서 store로부터 최신상태를 매핑하기위해 map 오퍼레이터를 사용한다.

이 맵핑은 annotationFilterViewProvider.tableViewModelForState 메소드를 통해 발생한다. 이것은 실행에서, UIKit을 감싸는 우리 커스텀 React가 발생되는 곳이다.

더 깊게 구현에대해 볼 순 없지만, 여기 tableViewModelForState 메소드가 있다.

tableViewModelForState는 인풋으로 최신 상태를 받고, FluxTableViewModel의 양식으로 Table View의 표현을 반환하는 순수 함수이다. 이 메소드의 아이디어는 React의 render 함수와 유사하다. FluxTableViewModel은 전적으로 UIKit과 독립적이고 테이블의 컨텐츠를 담은 구조가 간단하다. 당신은 오픈소스로 구현된 예제를 AutoTable 저장소에서 확인해볼 수 있다.

이 메소드의 결과는 ViewController의 TableViewDataSource 프로퍼티로 넘겨준다. 그 프로퍼티 안에 저장되있는 컴포넌트는 FluxTableViewModel에서 제공하는 정보를 기반으로 UITableView를 갱신하는 역할을 한다.

다른 바인딩 코드는 많이 간단하다. 예를들어 isFiltering 상태에따라 "Clear Filter" 버튼을 enable/disable 하는 코드가 아래에 있다:

UI 바인딩이 UIKit 프로그래밍 모델과 완벽하게 들어맞지 않아서 이것을 구현하는데 꼼수를 조금 사용하였다. 그러나 좀 더 쉽게 커스텀 컴포넌트를 만드려고 아주 약간만 노력을 기울였을 뿐이다. 전통적인 MVC 방식은 수많은 장황한 구현과 수많은 양의 View Controller 구현으로 UI를 갱신하는데, 우리 경험에서는 MVC를 쓰는것 대신 이 컴포넌트를 구현함으로서 구현 시간을 절약할 수 있었다.

이 UI 바인딩이 잘 구현되있다면, 우리는 Flux 기능 구현의 마지막 파트를 이야기할 차례이다. 내가 너무 많은 것을 이야기 했었던 것 같으니 Flux에서의 테스트를 설명하기 이전에 앞에 것들을 빠르게 한번 요약하겠다.

구현의 요약
Flux를 구현할 때 나는 일반적으로 아래 순서에 따라 작업을 쪼개어 한다:
  1. 상태 타입의 모양을 정의한다.
  2. action을 정의한다.
  3. 각 action들의 비즈니스 로직과 상태 변화를 구현한다. ― 이것은 store 안에 구현되있다.
  4. view를 표현하기 위해 상태를 맵핑하는 UI 바인딩을 구현한다.
이것은 우리가 얘기했던 세부적인 구현의 모든것들을 포괄한다.

이제 드디어 Flux에서 어떻게 테스트 할 지에대해 이야기해보자.

테스트 작성하기
Flux 아키텍처의 큰 장점중 하나는 일들을 엄격하게 분리한다는 점이다. 이것은 비즈니스 로직이나 UI 코드의 커다란 부분을 테스트하기 쉽게 해준다.

Flux에서는 테스트 해야하는 두가지 부분이 있다:
  1. store에서 비즈니스 로직
  2. view 모델 프로바이더(이것은 우리 React이다 ― 입력 상태에 따라 UI 표현을 처리하는 함수 형태이다)

store를 테스트하기
store들을 테스트하는 것은 보통 아주 쉽다. 우리 테스트는 action에서 호출하여 store와 함께 상호소통하게 할 수 있고, store에 구독하는 내부 _state 프로퍼티를 Observe하든 하여 상태 변화를 지켜볼 수 있다.

추가적으로 우리는 특정 피처를 구현해보거나 store의 초기화에서 이것을 심어보기위해, store가 소통하는데 필요한 어떤 외부 타입을 모의 객체(Mock Object)로 만들어 볼 수 있다.(특정 피처: API 클라이언트도 될 수 있고 데이터 접근 오브젝트가 될 수도 있다.) 이런 방식은 그 타입들이 우리가 예상한데로 호출되는지 집중할 수 있게 해준다.

PlanGrid에서는 Quick와 Nimble을 사용하여 작업에 관한 스타일의 테스트를 작성하였다. 여기 이 예제는 우리의 주식 필터링 store 부분에서의 테스트이다:

다시한번 말하자면, store를 테스트하는 것은 많은 메리트를 가지고 있다. 이 특정 테스트를 당장에 깊게 다루지는 않을 것이나 테스팅 철학은 명확하다. 가짜로 만든 모의 객체에서 store로 action을 보내고 나서 상태변화된 형태의 응답을 확인한다.

(여러분은 dispatcher를 이용하여 action을 dispatch 하지 않고, 왜 store에서 _handleActions 메소드를 호출하는지 의아해할 것이다. 원래 우리의 dispatcher는 action을 전달할 때, 비동기적 dispatcher를 사용했다. 그렇기에 비동기 테스트가 필요했고, dispatcher의 구현이 바뀌어왔기 때문에 테스트를 진행하면서 dispatcher를 사용할 수 있었다.

store에 비즈니스 로직을 구현할 때 나는 내 첫번째 테스트 코드를 작성하였다. Quick 행동 스펙(spec)과 함께 store 코드의 구조는 테스트 기반 개발 프로세스와 아주 잘 맞게 되어있었다.

view를 테스트하기
선언된 UI 레이어와 Flux 아키텍처는 view를 테스트하기 간단하게 짜여져있다. 팀 내부적으로 우리는 view 레이어에 목표로 하는 커버리지의 양을 아직 의논중이다.

실제로 우리 view에있는 모든 코드는 꽤 직관적으로 짜져있다. view는 store 안에서 우리 UI 레이어의 서로 다른 프로퍼티에 상태를 묶는다. 우리 앱의 경우 UI 자동 테스트를 통해 대부분의 코드를 커버하기로 결정했다.

그러나 여기엔 많은 대안들이 존재한다. view 레이어는 주입된 상태를 렌더링하기위해 초기화함으로 스넵샷 테스트도 매우 잘 동작할 수 있다. Artsy는 다양한 말과 블로그 포스트를 통해 스넵샷 테스팅 아이디어를 소개했다. 이 objc.io 글까지 포함해서 말이다.

우리 앱에선 UI 자동 커버리지가 충분하다고 판단했고, 이 이상 추가적인 스넵샷 테스트는 필요없었다.

또한 나는 view 프로바이더 함수를 유닛 테스트하는 경험도 했다.(e.g. 이전에 보았던 tableViewModelForState 함수) 이 view 프로바이더는 UI 표현을 위해 상태를 맵핑하는 순수 함수들이다. 따라서 입력과 출력 값에 기반한 테스트를 매우 쉽게 할 수 있었다. 그러나 이 테스트들은 실제 구현한 양과 비슷한 양으로 작성되기 때문에 많은 값을 넣어 볼 순 없었다.(However, I found that these tests don’t add too much value as they mirror the declarative description of the implementation very closely.)

우리가 앞에서 본 것처럼 UI 테스팅에는 많은 대안의 솔루션들이 있고, 나는 우리가 긴 기간동안 사용할 솔루션을 모색하는 중이다.

결론
많은 세부적인 구현을 본 뒤에 고수준의 관점에서 우리의 경험을 말해주고 싶었다.

우리는 오직 6개월동안 Flux 아키텍처를 사용해왔지만, 우리 코드를 보면서 이미 여러 장점을 발견할 수 있었다:
  • 새로운 기능을 조화롭게 구현한다. store, view 프로바이더, view controller의 기능의 구조는 거의 동일하다.
  • 상태와 action을 잘 정렬함으로서 그 기능이 어떻게 동작하는지 이해하기 쉽고, BDD 스타일로 테스트 할 수 있다.
  • store와 view를 강력하게 분리해준다. 특정 코드가 모호하게 있는 것이 드물다.
  • 코드 읽기가 굉장히 간단해진다. view가 의존하는 것이 명확하게 보인다. 이게 디버깅하기 매우 수훨하게 해주기까지 한다.
  • 위의 모든 것들이 새 개발자가 투입될때 쉽게 적응하게 만들어준다.

명백하게도 여긴엔 단점들도 있다:
  • UIKit 컴포넌트와 통합하는 첫 걸음이 약간 고통스러울 수 있다. React 컴포넌트와 다르게 UIKit view들은 새 상태에 의해 그들 스스로 간단하게 업데이트 되는 API 지원이 미흡하다. 이것이 조금 힘든 점이고, 우리는 view 바인딩에서 손수 구현하던지 UIKit 컴포넌트를 감싸는 커스텀 컴포넌트를 만들어야 할 필요가 있었다.
  • 아직 우리 모든 새 코드가 Flux 패턴을 정확히 따르지 못했다. 예를들어 Flux에서 동작하는 네비게이션/라우팅 시스템이 아직 자리잡지 못했다. 그래서 Flux 아키텍처에 동등한 패턴을 통합시키던지 ReSwift Router를 사용하여 비슷한 실제 라우터를 사용할 필요가 있었다.
  • 앱의 큰 요소들을 거쳐서쳐 공유되는 상태를 위해 좋은 패턴으로 만들어야한다.(이 포스팅의 초반부에서 "store의 영역은 어디까지인가?"라는 주제로 이야기하였다.) 기존 Flux 패턴으로의 store 사이에 의존성을 만들어야할까? 다른 대안은 무엇이 있을까?

더 많은 실제 구체적인 구현에서 더 많은 이점 혹은 단점이 존재한다. 나는 여기에 좀 더 깊게 파볼 것이고 나중에 블로그 포스트에서 더 세부적인 양상을 확인할 수 있기를 바란다.

지금까지는 이런한 선택으로인해 굉장히 기쁘고, 이 블로그 포스트를 통해 여러분께 Flux 아키텍처가 적절한지 알아볼 수 있는 기회를 제공했기를 바란다.

이제 마지막으로, 여러분이 Swift로 Flux와 함께 작업하고 싶거나 큰 산업을 위해 중요한 제품을 만드는데 도움을 주고 싶으면, 우리는 지금 고용중이다.

이 글의 초안을 검토해준 @zats, @kubanekl, @pixelpartner에게 감사하다.

참고:

  • Flux - 페이스북의 공식적인 Flux 사이트. 원래의 소개가 들어있다.
  • Unidirectional Data Flow in Swift - Swift에서의 Redux 개념과 원래의 ReSwift 구현에대해 이야기한다.
  • ReSwift - Swift에서 Redux를 구현한 것.
  • ReSwift Router - ReSwift 앱을 위한 정의된 라우터




iOS 개발자들은 iOS앱이 Model-View-Controller(MVC) 디자인 패턴으로 만들어진다고 말할 것 이다. 또한 그들은 이렇게 말한다. 그 패턴은 Massive View Controller라고.. 그 이유는 코드가 View와 Model에 명확하게 분배되지 않고 Controller에 코드를 쑤셔넣게 됨으로서, Controller는 비대해지고 한 클래스가 어마어마하게 커져버려 유지보수하기 힘들게 만들어버리기 때문이다.

Brigade의 iOS앱을 만들면서 우리는 몇가지 명확한 키포인트를 잡아 아키텍트를 짜길 원했다.
  • 재사용에 용이함
  • 동업에 적합함
  • 일을 잘 분배할 수 있음
  • 테스트하기 쉬움

그리고 우리는 VIPER라는 아키텍처를 만나게 되었고, 위 요구사항과 잘 들어맞아 보였다. VIPER는 Mutual Mobile에서 개발되었고, 그들은 이 블로그 글을 포함한 이상으로 VIPER를 소개한 멋진 블로그글을 써놓았다. 우리는 애플의 표준에서 벗어나 VIPER를 한 번 사용해보기로 결심했다.

VIPER는 무엇이고 어떻게 이것을 사용하는가?
만든 사람에 의하면, VIPER는 앱 아키텍처를 만들기 위한 가이드를 제공하며, 개별의 앱들을 맞춰 넣을 수 있다. 뒤에서 VIPER가 무엇인지. 그리고 어떻게 우리의 원하는 것들을 맞춰 넣을 수 있는지 이야기 해볼 것이다.

VIPER를 가장 빠르게 이해할 수 있는 방법은 MVC에 대한 모든것을 잊어버리는 것이다. VIPER는 완전히 새로운 종족이라 생각하고, 만일 아직 MVC를 마음에 담아두고 있다면 VIPER를 이해하는데 어려움을 겪을 것이다. 당신이 iOS 앱 구조에 대해 하나도 모른다고 생각하라. 이제부턴 MVC는 없다.

VIPER는 몇몇 역할 중 하나에 기능을 분리해 넣는 것이 목적이다.
  • View/User Interface
  • Interactor
  • Presenter/Event Handler
  • Entity
  • Router/Wireframe
Note : 다른 블로그에 가면 위 용어 대신 다른 용어를 사용할 수도 있음에 유의하라.

여기 예제에서는 몇개의 역할을 더 넣을 것 이다. 바로 Data Manager와 Service이다.

아래 그림은 우리 앱의 일반적인 VIPER "stack"(나중에 설명하겠다)의 다이어그램이다. 각 박스는 클래스 하나하나를 의미하고 그것을 잇는 선은 각 클래스의 객체를 참조한다는 뜻이다.




각 클래스들을 일꾼이라 생각하고 선으로 연결되었다고 생각해보자. 각 클래스들은 제한된 동작만 할 수 있도록 되있고, 다른 클래스에 의존하여 도움을 받아 작업을 완료한다.

이제 각 클래스의 첫번째 요소에대해 알아보고, 예제를 통해 유저의 인터렉션(interaction)에서부터 유저에게 처리된 데이터를 보여주기까지의 과정을 짚어갈 것이다.

View/User Interface



View의 책임
  • 유저게에 정보를 표시
  • 유저의 인터렉션을 감지
View는 Presenter에의해 어떤걸 보여줄지 표시하고, event가 일어나게되면 Presenter에게 알려준다.

유저에게 정보를 표시
View가 유저에게 에러를 표시하려 한다면 Presenter는 아래와 같이 메소드를 호출할 것이다.

그러면 요구한대로 에러를 화면에 띄우기 위해 경고뷰나 레이블등을 View에 표시할 것이다. 여기서는 어떻게 화면에 에러를 표시하는지에대해서 Presenter가 관여하지 않는다는 것이 중요하다. Presenter는 오직 표시가 되있는지 그 자체에만 관심이 있다.


유저 인터렉션을 감지
로그인 버튼 누르기와 같은 이벤트가 발생하면 View는 Presenter에게 아래와 같은 메소드를 호출 할 것이다.

이전에도 보았드시 메소드를 만드는 오브젝트는 그냥 한번 호출하여 다음 일꾼이 그 작업을 처리하도록 넘겨준다. View가 관여하는 한 유저의 반응을 감지하여 그 작업을 완료하고 Presenter에게 이벤트 형태로 일을 준다.


Presenter/Event Handler


Presenter의 책임

  • 무엇을 표시할지 View에게 말함
  • 이벤트를 관리(다룬다)
Presenter는 View에게 무엇을 표시할지 말해줘야하고 이벤트들을 적절하게 다룬다.

무엇을 표시할지 View에게 말함
우리는 방금, Presenter의 책임중 하나가 무엇을 표시할지 View에게 말하는 것이라 했다. 이 작업을 완료하기 위해서는 마치 도배업자(decorator)나 진행자(presenter)처럼 행동해야한다. View를 위해 알맞은 데이터로 맞추고 나면 의미있는 방법으로 표시될 수 있다.

이전에 에러 예제로 돌아가보자. Presenter는 에러를 받는게 에러 오브젝트를 받는 것처럼 하였다. View에 바로 에러를 뿌리는게 아니라, 적절한 메시지로 에러를 풀어서 View에게 던져주었다.

이벤트를 관리
Presenter는 보통 View나 Interactor, 이 두 경우로부터 메소드가 호출된다.

View 이벤트의 경우
Presenter가 View에의해 이벤트를 받으면 이 이벤트를 처리해야한다. 이것은 보통 Interactor에게 정보를 검색해달라고 하거나 몇 작업을 수행해달라고 요청하는 일이다.

우리의 로그인 예제에서 Presenter가 View로부터 특정 유저 이름과 패스워드를 담아 로그인을 시도하는 이벤트가 발생했다고 알림을 받는다. 그러면 Presenter는 적당한 메소드를 호출함으로써 Interactor에게 일을 넘길 수 있다.

아마 당신은 Presenter에서 아무런 처리도 하지 않음을 짐작할 수 있을것이다. Presenter의 주 목적은 이벤트를 관리하는 것이며 Interactor에게 작업을 넘겨주는 일을 한다.


Interactor 이벤트의 경우
Presenter는 또한 Interactor로부터 이벤트를 받을 수도 있다. Interactor에서 작업이 끝나고 유저가 작업의 결과를 알아야 할 때 이러한 상황이 발생할 것이다.

예를들어 로그인 시도에 실패할 경우, Interactor는 아래와같이 Presenter에게 알릴것이다.

Presenter는 이 에러를 받고, 사용자에게 보기 좋은 문자열로 바꾼뒤, View에게 화면에 표시하라고 말 할 것이다.

Interactor

Interactor의 책임
Interactor는 데이터를 주변에서 가져와서 앱의 비즈니스 로직을 실행시킨다.

비즈니스 로직을 실행시키기
Interactor는 View가 Presenter에게 넘긴 이벤트를 어떻기 처리해야 할지에 대해서만을 알고있다.

예를 들어보자. 당신의 앱이 동기적으로 통신하는 2개의 네트워크 요청을 백그라운드에서 API 호출 하였다고 해보자. B라는 요청은 A요청의 응답 결과를 이용해야하므로 A요청이 끝날때까지 B요청을 해서는 안된다. 이것은 Presenter에서 Interactor로 호출하는 메소드로부터 작업이 시작된다.


여기서 반드시 기억해야할 사실은 Interactor가 직접 네트워크를 타지는 않는다는 것이다. 사실 Presenter는 실제로 네트워크 요청이 있는지 없는지 조차도 모른다. 아는것이라곤 Data Manager(나중에 이 개념에 대해 이야기 할 것이다)가 Entity 형태로 데이터를 넘겨주는 정도이다. 이것을 기억하고 위에서 호출된 Interactor 메소드는 아래와 같이 생겼을 수 있다.


Interactor는 Data Manager에 메소드를 호출하고 콜백을 받아 결과를 Presenter에게 돌려준다. 여기서 Interactor는 'performMyTask'가 'fetchFooWithCallback:'과 'fetchBarWithFoo:callback:'이 호출된다는것을 알고있고, 'fetchFooWithCallback:'의 콜백 블럭(block)에서 호출된다는 것까지 알고있다. 이 경우 Interactor는 앱의 "비즈니스 로직"을 다룬다고 할 수 있겠다.

Data Manager


Data Manager의 책임
  • 데이터를 검색
  • 데티러를 저장(부가적인)
Data Manager는 어디에서 데이터를 검색할 수 있는지, 저장할 수 있는지 없는지 알고있다.

데이터를 검색
Interactor 예제에서 보았듯 Data Manager에게 쿼리를 날리고 필요한 Entity들을 받아낼 수 있다. 우리는 그 데이터가 어디서부터 오는지 몰라도 된다. Data Manager가 알아서 관리해주기 때문이다.

Data Manager는 어디서부터 특정 데이터를 검색할지나 어디서 특정 리퀘스트를 수행하는지 정확하게 안다. 예를들어 Interactor는 Data Manager에게 사용자 객체를 요청할 수 있다:


만일 이 앱이 백엔드 서버 동작을 사용하고 있다면, 사용자 데이터를 검색할 수 있는 네트워크 요청을 누가 가지고 있는지 Data Manager는 알고 있을 것이다. 이 앱이 서버를 사용하지 않는다면 Data Manager는 네트워크 대신 로컬 persistent store로부터 데이터를 검색할 것이다. 네트워크 요청이 있는 경우, Data Manager는 Service 객체와 함께 쿼리를 날린다.


Service는 좀 있다가 설명하고, 여기서의 키포인트는 특정 정보를 검색하기 위해 어떤 Service를 호출해아하는지 알고 있다는 것이다. 이제 정보를 받으면 그 정보를 Interactor에게 넘긴다.

데이터를 저장
Persisting 데이터는 Data Manager의 관심사이기도하다. 서버로부터 받은 데이터를 패치(갱신)하기 전에 잠시 들고 있는 목적으로 Data Manager는 언제 데이터를 저장하는지 알고 있어야한다. 주 요점은 어떤 다른 클래스도 어떤 데이터가 persistent한지 모른다. 왜냐하면 Data Manager가 추상화를 했기 때문이다. 이 예제는 위 코드를 수정한 코드이다.


이제 하나하나 파헤쳐보자.
  • Data Manager는 먼저 'User ID'를 넘겨줌으로써 일치하는 해당 유저가 있는지 확인한다.
  • 만일 유저를 찾으면 Interactor에게 알리고 메소드는 return된다.
  • 만일 유저를 못 찾으면 Data Manager는 백엔드에서 유저를 찾아보고 서버를 이용하여 처리한다.
  • 서버로부터 유저 정보를 받으면 유저는 첫 persistent가 되고 interactor에게 알린다.
persisting 데이터는 그것이 소유하는 넓은 범위를 말하며, 다양한 형태로 구현될 수 있다. 여기서의 예제는 단순히 가르쳐주기 위한 스키마이지만 Data Manager에서의 모든 추상화 아이디어는 VIPER의 키포인트를 담는다.

Service

Service의 책임
  • 특정 Entity들을 위해 서버로 네트워크 요청을 날린다.

Service 오브젝트는 VIPER의 필수요소는 아니다. 그러나 이것은 굉장히 유용하다.

특정 Entity들을 위해 서버로 네트워크 요청 날리기
우리는 네트워크 요청의 DRY(Don't Repeat Yourself)에 Service가 좋은 역할을 할 것이라는 것을 알아냈다. 이 생각은 한개의 Service는 한개의 Entity 타입을 다루고, 이 Service는 Entity 타입의 다양한 수행들을 위해 어떤 네트워크 요청이 필요한지 알고있다.

예를들어, 사용자 Entity를 위한 Service가 있을 수 있다. 이 헤더파일은 이 클래스를 위한 것이다.


위 Service는 어떻게 유저를 생성하고, 로그인하고, 유저 정보를 갱신하는지 알고있다. ID를 주어서 유저 오브젝트를 갱신하는 것은 앱에 다양한 곳에서 사용할 수 있고, 이것은 Service 오브젝트의 코드가 DRY를 지켜가며 다룰 수 있다는 뜻이기도 하다.

Entity


Entity의 책임
  • 데이터를 표현한다.
Entity는 꽤 직관적이고 여러분이 예상하는 바와 비슷하다. 이것들은 데이터의 타입을 정의하고 다른 클래스들 사이에서 넘겨지면서 "payload" 역할을 한다. 예를들어 Data Manager는 한 Entity를 Interactor에게 반환하고, 다음 이것이 Presenter에게 반환되어, 마지막으로 View가 그것을 받아 화면에 표시한다.

Router/Wireframe

Wireframe의 임무
  • 모든 다른 클래스 객체들을 초기화
  • 앱에서 다른 View로의 경로를 다루기
Router/Wireframe은 앱에서 다른 VIPER 컴포넌트 모두를 하나로 합쳐주고, 한 View에서 다른 View로 navigating하는것을 도와준다.

모든 다른 클래스 객체를 초기화
여러분은 아마 VIPER 클래스들이 어떻게 초기화되는지, 어떻게 서로 소통하는지 궁금할 것이다. 그 일은 Router/Wireframe이 할 것이다.

VIPER에서는 한 "stack"이 View, Presenter, Interactor, Data Manager, Service, Entity로 구성된다. 이 "stack"은 모듈로서 표현될 수 있는 VIPER이다. 한 VIPER 모듈은 하나의 유스케이스와 일치한다. 한 유스케이스는 당신의 앱이 유저를 위해 실행할 수 있는 어떤 기능이다.

예를들어 많은 앱들을 위한 일반적인 한 유스케이스로는 계정을 이용한 로그인 허가이다. 여기서 앱의 "로그인"화면을 위해 특정 VIPER 모듈을 만들 수 있다. 이 모듈의 기능이다.
  • View는 로그인 화면을 표시한다.
  • Presenter는 특정 유저이름, 패스워드를 받아 로그인 요청을 날리는 그런 이벤트들을 다룬다.
  • Interactor는 유저 로그인 시도와 같은 이벤트를 위해 Data Manager에게 어떤 ㅁ[소드가 필요한지 알고있다.
  • Data Manager는 어떤 Service가 검색에 사용되는지, 어떤 Service가 서버로 정보를 보낼 수 있는지 알고있다.
  • Service는 실제 요청을 보내기 위해 HTTP URL을 알고있다.
  • Entity는 서버로부터 받은 응답으로써 앱에 적용가능한 정보로 변환된 형태이다.
여러분도 인지했겠지만 View, Presenter, Interactor, Data Manager들은 모듈에 안에서 굉장히 한정적으로 사용되는 것이다. 이것들이 오직 로그인과 관련하여만 어떻게 다루는지 안다. 반면 Service와 Entity는 앱 전반에 걸쳐 일반적으로 쓰이는 것이다. 이 경우 Service는 유저와 관련하여 당신 서버에 어떻게 요청하고 끝나는지 알고 있을 것이다. Service는 로그인, 회원가입 혹은 단순히 유저 정보를 검색하는 기능들을 포함한 것이다. 그리고 이 경우 Entity는 앱의 다양한 곳에서 사용될 수 있을 것이다.

이제 우리는 VIPER "stack" 혹은 모듈이 무엇인지 설명했고, Wireframe이 어떤 역할을 하는지 설명하겠다. Wireframe은 각 VIPER 컴포넌트들을 인스턴스화(객체화) 시켜주고 서로 소통할 수 있게 해준다. Wireframe은 View와 Presenter를 서로 참고하게 해주고 Presenter와 Interactor를 서로 참조하게 해주는등의 역할을 한다. Wireframe을 객체로 만든다는 것은 VIPER 모듈 전체를 객체화 한다는 것과 같은 의미이다.

앱에서 다른 View로 라우팅하기
Wireframe의 또다른 별명은 Router이다. 이것은 VIPER의 R을 의미하기도 한다. Wireframe은 요청이 들어올때 어디로 navigate할지 어떤 다른 모듈로 present 할지 알고 있다. 이 말은 즉 어떤 Wireframe은 다른 Wireframe을 참조한다는 뜻이다.

예를들어 당신앱의 첫 화면이 유저 계정을 요구하는 화면이라 생각해보자. 이 화면은 "회원가입"버튼과 "로그인"버튼이 있을 것이다. 그리고 또한 이 화면은 한 모듈이다; 이것을 회원가입 prompt라 부르다. 이 경우 실제 로그인을 하기 전에 유저들은 몇몇 옵션들을 볼 수 있다. 유저가 "로그인"버튼을 누른 경우 나타나는 일반적인 흐름이다.
  • View는 유저가 로그인을 요청했다고 Presenter에게 알린다.
  • Presenter는 이 요청을 알아차리고 모듈의 각 기능을 수행후 Wireframe에게 알린다.
이제 회원가입 Wireframe이 로그인 Wireframe에 의해 객체가 만들어지고, 역시 모든 VIPER 컴포넌트들이 새로 객체화된다. 그 다음 로그인 View에서 회원가입 View로 넘어갈 것이다.

VIPER의 이점
VIPER를 사용하고부터 많은 방법들로부터 이점들을 찾았다. 우리 앱에 VIPER를 적용시키면서 좋았던 점들을 다시 확인해보자
  • 재사용에 용이함
  • 동업에 적합
  • 일을 잘 나눌 수 있음
  • 테스트하기 쉬움

재사용에 용이함
앱에서 새 기능을 넣는다는 것은 굉장히 유용한 점인다, 이것은 이전 코드에 연관되어 새 코드가 어디에 들어갈지 알고 있다는 것이다. VIPER는 각 컴포넌트가 명확한 책임을 가진다는 점에서 굉장히 이상적이다. 이 점은 어떤것 안에서 엉키지 않고 새 코드를 어디에 넣을지 쉽게 결정되게 해준다.

새 기능을 추가할 때 그것들이 매번 쓰는 루틴이라 느껴진다. 왜냐하면 각 부분들의 코드가 어디에 있는지 알고 있고, 그냥 끼워넣으면 끝나는 문제이기 때문이다.

코드가 어디에 있어야할지 불분명하거나 임의로 판단(Judgement Call)해야할 경우 우리는 VIPER 상황에서 동작시킨다. 예를들어 뭔가 수행되기 전에 유저가 여러 아이템을 골라야 한다고 해보자. 이때 현재 View, Presenter 등등 상태를 유지한채 작업해야할까? 당신의 감각으로 어떤것을 만들어야하는지 한번만 이해하면 나중에는 쉽게 이 문제를 해결할 수 있을 것이다. 왜냐하면 이미 해결해보았던 경험이 있기 때문이다. 

동업에 적합
VIPER는 팀에서 작업하기 쉽게 만들어준다. 유스케이스가 서로 다른 모듈별로 나뉘어져있으므로 다른 사람에게 코드를 넘겨받아 작업할 필요가 없다. 그러므로 항상 한 기능을 위한 모든 코드는 그것의 모듈에 구분되있다.

예제에서 각 VIPER 컴포넌트들은 인터페이스를 통해 다른 컴포넌트와 연결된다. Objective-C에서는 이것은 단지 규약일 뿐이다. 좋은 점은 당신이 두 클래스간에 인터페이스를 정의하면 사람들이 이 클래스를 개별적으로 작업할 수 있다는 점이다. 우리는 처음에 View-Presenter의 인터페이스를 정의했고 한사람이 UI만 작업하고 다른 한 사람이 나머지 VIPER stack의 부분인 뒷단(backend)를 작업했다ㅏ.

일을 잘 나눌 수 있음
명확하게 정의된 "하나의 책임 원칙"인 각 VIPER 모듈이기에, VIPER는 클래스 사이에서 자연스럽게 분리된다. 이전 2가지 이점(재사용에 용이, 동업에 적합)과 관련이 깊다고 할 수 있겠다.

테스트하기 쉬움
앞에서 말한 "단일 책임 원칙"으로 쪼게어진 컴포넌트들을 가짐에 따라, 테스트(spec)하기 쉽게 되었다. 이것은 다른 의존성을 잘라내어 특정 기능을 테스트 가능하게 해준다. 예를들어 Interactor로직을 테스트 하고 싶을때는 주변에 붙어있는 Presenter와 Data Manager를 잘라내어 의존성을 제거하고 테스트를 위한 코드를 작성하여 컴파일 할 수 있다. 그러면 당신의 테스트들은 Interactor를 테스트하기위해서만 동작할것이며, Presenter와 Data Manager로부터 분리되어 테스트 가능하게 된다.

결론
전반적으로, VIPER로 전향한 것은 굉장히 유용했다는 것을 느꼈고, 위에서 언급한 이유들처럼 우리에게 많은 이드벤테이지가 있었다. 물론 우리가 원하는 VIPER를 만들면서 많은 난관을 마주쳤지만, 사실 이것은 어떠한 아키텍처를 골랐더라해도 겪는 문제일거라 생각된다.

나는 VIPER를 시도해볼 것을 추천하는 바이고 또한 다른 블로그 포스트도 보길 추천한다. 이 글이 도움이 되었다면 당신이 한 번 VIPER를 시도해보기에 관심이 생겼을 것이라 생각된다.


추가적인 읽을거리




이번 시간에는 VIPER와 관련된 우리팀의 이야기를 들려주고 경험을 공유하고 싶다. 특히 우리가 특정 상황에서 어떻게 VIPER적으로 다루는지, 우리들의 추천은 어떤지 이야기 할 것이다. 또한 당신의 경험을 코멘트 해주길 바란다(원문 링크에 들어가서 코멘트 해주시면 됩니다).

이 글의 목표는 VIPER의 규칙에 대해 이야기하거나 VIPER의 모든 컴포넌트들을 설명하려는게 아니다. 이러한 이야기는 이미 더 좋은 글들이 많다.  

이 글에서는 우리 팀이 프로젝트를 하면서 무얼 배웠고, 이 아키텍처를 사용하는 동안 어떤 것들이 큰 도전이었는지 알려 주고 싶다. 우리는 시작할 때 많은 궁금증들이 있었다. 작년에 큰 프로젝트를 시작하였고 이야기는 여기서부터 시작되었다. (컨퍼런스 관련 앱인데 의제나 참석자의 목록, 발표자의 목록, 뉴스의 목록 등으로 구성된 앱이다)

VIPER를 타고 달려나갈 준비가 되었는가? 

VIPER CarVIPER Car


왜 우리는 VIPER를 골랐을까?


Note : 만약 새 프로젝트에 적용시키길 적절한 아키텍처를 찾고있다면 이 사이트를 한 번 들어가보아라. 우리의 경우 최종적으로 VIPER를 선택했다.

  • VIPER 아키텍처는 프로젝트 초기에 요구사항이 잘 정의되어 있다면 적합하다고 할 수 있다. 운좋게 우리가 그러했다. 만약 당신의 화면정의나 비즈니스 로직이 프로덕트 오너에 의해 바뀌기가 쉽다면, VIPER는 별로 좋지 않은 솔루션일 수 있다. 하나의 작은 변화에도 당신의 모든 모듈(View, Presenter, Interactor...)을 손봐야 하는 수가 있다. 이러한 대규모의 재설계는 엄청난 시간낭비이고 차라리 새로운 VIPER 모듈을 만드는게 나을지도 모른다.
  • 우리 프로젝트는 상당히 규모가 컸다. 하나의 모듈을 세팅함으로써, 파일들을 생성하고 수많은 반복적인 코드를 만들어낸다. 모든 VIPER 컴포넌트로부터 데이터를 주고 받는일이 잦은데, 한 View에서 API 관리자로 데이터를 넘겨주고, 다시 데이터를 View에 돌려준다. 데이터를 자꾸 옮겨 다녀야하기 때문에 이게 왜 작은 프로젝트에는 적합하지 못한지 보여주는 예이다.
  • (몇몇 예외를 제외한) VIPER는 각 요소마다 기능 정의가 아주 명확하다. 덕분에 파일의 코드 양을 줄여주고, 하나의 기능 컴포넌트에 따라 옳바르게 모듈이 나눠져있을 것이다. 추가적으로 VIPER 프로젝트는 모든 개발자에게 비슷한 관습을 만들어주기 때문에 구조가 잘 잡힌다. 새로운 개발자가 팀에 합류한다해도 빠르게 그 VIPER에 적응하게 될것이고, 새로운 개발자가 원래 프로젝트의 구조를 바꾸기는 쉽지 않을 것이다.
  • 3명의 개발자가 있는 팀에서 일한다면, 모두가 하나의 모듈을 개발할 수 있다. 쉽게 쪼게어 개발할 수 있다는 뜻이다.
  • 우리 프로젝트는 시작단계부터 화면정의와 기능정의가 잘 되있었기 때문에 VIPER 모듈로 만드는 것이 어렵지 않았다.
  • VIPER 컴포넌트의 기본은 한 모듈에 있는 모든것들이 굉장히 잘 나눠져있다. 따라서 유닛 테스트 하기 좋은 조건이다. 이 글을 보면 VIPER에서 TDD 이야기를 들을 수 있다.
  • 마지막으로 우리팀은 새로운 아키텍처를 시도해보고 싶었다!!!


MassiveMassive

시작은 MVC로 했지만, 결국 Massive(덩어리의)VC로 끝나버렸다.

프로젝트 구조, 폴더, VIPER 모듈들

혹시 모든 VIPER 모듈의 컴포넌트들을 외우고 있는가? 우리는 이 웹사이트를 기반으로 컴포넌트를 정의했고, 여기서는 Services라 부르는 컴포넌트를 사용했다. 당신은 Services라는 섹션에서 더 많은 정보를 얻을 수 있을 것이다. 


VIPER 다이어그램VIPER 다이어그램

프로젝트 파일에 어떻게 이것들을 적용시킬까? 모든 컴포넌트는 각 폴더와 클래스로 적용될 수 있다.

다음 질문으로 VIPER 모델로서 어떤것이 제격일까? 기본적으로 가장 쉬운 접근법은 한 화면 단위로 VIPER 모델을 만드는 것이다. 아래 예를 보자.
  • Login Screen (로그인 화면) -> Login Module
  • Participant List (참여자 목록) ->  Participant List Module
흠 모든 것을 수작업으로 하나하나 만들어야 할까? 다행히도 자동생성기를 사용하면 된다.

VIPER 모듈 자동생성기(Generator)
당신의 앱을 정말 VIPER 아키텍처 기반으로 만들고 싶다면 손으로 하나한하 처넣을 생각은 안해도 된다. 그것은 재앙이다! 새 모듈을 자동으로 생성해주는 프로세스가 필요하다. 먼저 VIPER 자동생성기 프로그램을 아래 링크에서 다운받을 수 있다.

우리 프로젝트에서는 첫번째 VIPER gen을 선택하여 우리 목적에 맞게 커스텀하여 사용했다. 예를들어, Interactor와 Presenter를 위한 테스트 파일을 추가했다. 커스텀된 vipergen툴은 아래 링크에서 사용할 수 있다.

이 커스텀 과정에서 루비로 약간이 수정이 필요했고, 단지 자동으로 모든 유닛테스트 파일에 Swift 모듈 이름을 추가해주는 기능이다.

아마 당신은 당신만의 템플릿이 필요할 것이다. 그러려먼 이미 있는 기본 템플릿에서 폴더를 복사하고 수정해가면서 간단하게 만들 수 있을 것이다. 이 저장소에 들어가면 새 템플릿을 어떻게 추가하는지 배울 수 있다. github를 쓸 수 있는 사람이라면 쉽게 커스텀이 가능할 것이다. 

우리는 사실 다른 솔루션을 많이 사용해보진 않았으나, Generamba는 CLI를 셋업하기 좋게 제공되는 툴처럼 보인다. 여러분은 각자 상황에 맞게 가능한 많은 솔루션을 체크해보고 사용하는걸 추천한다.

VIPER 모듈들끼리 서로 정보 보내기
시작할때부터 우리는 VIPER 모듈 사이에 데이터를 어떻게 다루는지 생각해보았는데, 명확한 답이 보이지 않았다. 아래 토픽들은 프로젝트 시작할 때 읽으면 굉장히 도움이 될 것이다.

최종적으로 우리는 "한 모듈"에서 "다른 한 모듈에있는 Presenter"에 정보를 보내기로 결정했다. (이것이 최대한 VIPER 컴포넌트를 망가뜨리지 않고 사용할 수 있는 가장 좋은 방법이라 생각했다.)

Passing DataPassing Data


코드에선 어떤걸 의미할까?

SpeakerDetails 모듈은 SessionList wireframe에서 불려진 클래스 메소드를 기반으로 초기화된다. 그러므로 SpeakerDetails의 Presenter 메소드인 위 메소드는 유저가 어떤 세션을 선택했는지 알고 있어야 한다.


VIPER, Entity와 Core Data
우선 우리는 Core Data Stack을 만들기로 결정했다. 왜 이 작업은 외부 라이브러리를 사용하지 않았냐고 물어본다면, 우리는 Persistence Store의 컨트롤을 자유자재로 하고 싶었기 때문이다.

우리의 Core Data Stack은 Object Context가 두가지가 있다. 하나는 메인 쓰레드용이고 하나는 백그라운드 쓰레드용이다. 둘 다 같은 Persistent Store Coordinator에 연결되어, 각 context는 서로 독립적으로 동작한다. 저장이 완료되었다는 신호(did-save notification)와 함께 변경사항들이 맞바꿔진다. 

CoreData SchemaCoreData Schema

Note : 이 아이디어는 Advanced Core Data라는 책으로부터 얻었는데, Core Data Stack을 많은 선택지와 함께 어떻게 세팅할 수 있는지 알려준다. 우리는 강력히 이 책을 추천한다.


그러나 Entity는 어떨까?

Entity들은 CoreData의 NSManagedObject 인스턴스가 아닌 VIPER 컴포넌트에 의해 주고 받아진다. Managed Object는 local manager들 에서만 접근이 가능하다. 이것들은 Entity로 바꾸거나 Interactor로 보낸다. 

CoreData Convert To EntityCoreData Convert To Entity

왜 NSManagedObject들을 보내지 않을까? 그 이유는 우리 앱은 데이터 모델과 데이터 레이어가 분리되있기 때문이다. 이 방법에서는 우리는 CoreData를 data store layer로 따로 빼어 놓았다.


어떤 데이터 타입으로 Entity를 표현할까? 우리는 모두 구조체(Struct)로 만들었다. 


위 구조체는 알기쉽고, 변경불가능하며 쓰레드-세이브(Thread-Safe)하여 완벽하다 😃


의존성 주입(Dependency Injection)

VIPER 아키텍처를 사용하는 것은 의존성 주입(dependency injection)을 적용해볼 수 있는 기회를 제공한다. 예를들어 이 local manager 예제를 살펴보자. 



VIPER 아키텍처를 사용할 때 모든 요소마다 DI를 사용하는 것은 좋은 습관이다. 우리는 유닛테스트라는 섹션에서 몇 예제와 함께 테스트할때 어떻게 이런 접근이 우리에게 도움이 되는지 보여줄 것이다.
실제 Wireframe이 무엇인가?
우리의 경우 wireframe은 두가지 기능을 가진다.

  • 각 VIPER 컴포넌트들의 인스턴스들을 초기화하고 그들을 연결해준다.
  • 두번째 기능은 다른 모듈에 navigate와 present 해준다.

이게 다다.

유닛 테스트
우리는 이전에 유닛테스트에 경험이 많이 없었다. 이 프로젝트에서 유닛테스트의 첫발을 내딛었는데, Interactor와 Presenter를 테스트하는 것 부터 시작했다. 그 이유는 Interactor에서 메인 비즈니스 로직을 가지고 있고 Presenter는 데이터를 보여주기 직전에 준비하는 작업을 하기 때문이다. 이 컴포넌트들은 다른 것들에 비해 좀 더 중요해 보였으나 오로지 우리 주관적인 의견임을 기억해주기 바란다.

유닛 테스트에서 사용된 라이브러리들이다.

VIPER에서 한 모듈의 모든 요소들은 각 기능별 유닛 테스트하기 적합하게 만들어져 있으며 이것들은 엄격히 분리되어 있다. 

Unit Test MockUnit Test Mock

위에서 볼 수 있듯이 컴포넌트들을 분리함으로써 우리는 Interactor의 테스트에만 집중할 수 있다. Interactor와 연결된 다른 요소는 단지 테스트를 위해 임의로 만든 목(mock)이다.



Services
Services가 무엇일까? 이것은 엄격한 VIPER 컴포넌트로부터 약간 분리되어있으며, 독립적인 컴포넌트라 할 수 있다. Service는 여기에서 언급되었으며, 이것이 반드시 필요한 것은 아니나 굉장히 유용한데 특히 높은 결합력을 만들어준다. 한 Service는 여러 모듈에서 사용될 수 있다. 
여기 그 기능의 예시이다.
  • 캘린더를 다루는 것 (Calendar Service)
  • 연락처를 다루는 것 (AddressBook Service)
  • 키체인 관리 (Keychain Service)
  • 사람들 서비스 (Person Service) - 사진과 같이 유저의 데이터 다운로딩을 관리한다. 이 경우 Service는 네트워크 요청을 위한 apiClient 인스턴스가 필요하다.

Service 동작



백엔드로부터 변화(갱신)들을 듣는(listening)것은 어떻게 다뤄지는가? 

우리 앱에서는 자동으로 갱신되는 두개의 목록이 있다. 우리의 Synchronized Service 는 백그라운드에서 JSON responses를 다운받고 CoreData에 넣는 무거운 작업을 한다. 이 작업은 매 1분마다 하게된다.


notification을 보내기 위해 파라미터를 받는 save 메소드를 가진다.

갱신 플로우에서 다음 컴포넌트는 interactor 이다.


dataUpdated(notification:NSNotification)는 interactor로부터 override 되었다. 당신이 예상한대로 표준 VIPER 플로우가 되었다. interactor는 notification을 받은 후에 데이터를 위해 local data manager에게 그것을 물어본다. 그러면 새로운 데이터르 미리 프로세스하기 위해 presenter에 보낸다. 그리고 presenter는 view에 보내어 화면에 표시한다. 됐다!


VIPER 모듈 컴포넌트들 사이에 API들은 각 프로토콜 파일에 정의되므로 항상 양방향 소통이 가능하다. 끝나는 시점에 작업을 하기 위해 클로저(closure)를 사용하는 것은 interactor에서 presenter로 데이터를 넣는 중에 블락될 수 있다.

오히려 옛날의 delegate 패턴이 우리 업데이트 메커니즘에 더 잘 동작한다. presenter가 interactor에 데이터를 얻기 위해 물어볼 뿐만 아니라, 모든 업데이트 프로세스를 초기화 할 수 있다.

보기에 깔끔하고 딴딴해보이지 않는가?


그래서 언제 VIPER를 사용하고, 언제 사용하지 말아야할까?
항상 그렇듯 대답은 "상황에 따라 다르다”.

아래 다이어그램이 이 중요한 질문에 답변이 될 수 있길 바란다.

ViperOrNotViperOrNot


마지막 요약
VIPER 사용을 시작하는 것은 크나큰 도전일 수 있다. 특히 이 아키텍처를 처음 적용시키는 경우는 더더욱 그렇다. 우리는 여러분이 이  글을 읽고 많은 의문들이 사라졌기를 바란다.

우리의 경우 git flow로 pull request와 함께 연습해보았다. 모든 개발자들이 저장소에 동료가 어떤 것을 push 했는지 조심히 관찰할 수 있다면, 이것은 굉장히 유용했다. 만약 대부분의 사람들이 서로 이야기나 관찰 없이 그들만의 VIPER 버전을 만들어버리면 이것은 재앙이다. 그 즉시 바로 모여 브레인스토밍하고 모두가 사용할 수 있는 새로운 솔루션을 다같이 찾아봐야한다.

VIPER는 앱을 어떻게 만들지 개괄적인 결정을 한다. 우리는 당신에게 오픈 마인드를 가지고, 각 컴포넌트를 커스터마이징하고 최적화하는 것을 멈추지 마라고 조언하고싶다.

VIPER는 지속적인 개선이 필요하며, 우리는 새 프로젝트가 이전 처음 프로젝트보다 더 나은 경험을 할 수 있을거라 기대한다.

특히 프로젝트 첫 시작부분인 당신이 프로젝트 구조를 세팅하는 시점에서 아키텍처를 바꾸지 않는다면, 한 걸음 걸음이 악몽일 것이다. 아키텍처에서 하나의 실수가 더 많은 실수를 유발하게 할 수 있다. 이것이 수 많은 힘겨운 일거리가 만들어지는 이유이다.

마지막으로 VIPER는 유닛테스트에 경험이 없어도 이것을 구현하기 쉽도록 해준다. VIPER에게 감사하다.

흥미로운 자료들

iOS 아키텍처 관련 번역글
 


MVC, MVP, MVVM, VIPER에대해 확실하게 잡기

원문https://medium.com/ios-os-x-development/ios-architecture-patterns-ecba4c38de52#.wtcp3gqzw

UPD: NSLondon에대해 내가 발표한 슬라이드 자료가 이 링크 있다.

iOS에서 MVC 사용한다는게 다소 이상하게 느껴질 있다. 당신은 MV모데VM으로 바꾸려고 생각해본 적이 있는가? VIPER 적용시켜볼 생각을 적은 있으나, 그게 의미있는 것인지 확신이 들지 않는가?

글을 읽어 내려가면 것들에 대한 답을 찾을 있을 것이다. 또한 자유롭게 댓글로 의견을 제기할 있다.

당신은 iOS 환경에서 아키텍처 패턴에 대한 지식을 정리하고 싶을 것이다. 우리는 유명한 것들을 골라 한번 보고, 이론과 비교한 , 작은 예제들과 함께 연습해 것이다. 아래 링크는 당신이 특별히 관심있는 것을 연습할 있다.

디자인 패턴을 마스터하는것은 중독될 있으므로 조심해야한다: 전보다 많은 질문들이 생겨날 것이기 때문에.

- 누가 네트워크 리퀘스트를 소유하여야하나: 모델이냐 컨트롤러냐?
- 새 뷰의
어떻게 모델을 넘겨주나?
- 누가 새로 생긴 VIPER 모듈을 생성해야하나: Router Presenter?

아키텍처를 고르는데 신중해야하는가?

당신이 만약 개발을 하다가 디버깅을 해야하는데 엄청난 양의 클래스와 엄청난 양의 다른 것을 비교해야 하며, 이게 아키텍처가 없는 상황이라면, 당신 클래스의 어떠한 버그를 찾지도 고치지도 못하는 상황을 맞이하게 것이다. 우리는 클래스의 모든 속성을 머릿속에 담아두고 있을 없다. 만약 그짓을 하다보면 중요한 세부적인 요소를 놓힐 수가 있다. 만약 개발하면서 이런 경험을 이미 해보았다면 아래와 같은 것을 겪어봤을 것이다.

  • 클래스가 UIViewController 자식클래스이다.
  • 당신의 데이터들이 UIViewController에서 바로 저장된다.
  • UIView들이 거의 아무 일도 하지않는다.
  • Model 데이터 구조이다.
  • 유닛 테스트로 아무것도 하지 않는다.

그래도 애플의 가이드라인이나 애플의 MVC(링크) 따랐다해도 이러한 상황은 생길 있으니 너무 낙담하지는 마라. 애플의 MVC 뭔가 잘못되었고, 우리는 그걸 바로잡을 것이다.

좋은 아키텍처의 특징 정의해보자:

  • 엄격한 룰에 따라 개체들간의 책임 분리(Distribution) 균형있게 해야한다.
  • 첫번째 말한 특징으로부터 나올 있는 테스트들이 가능(Testability)해야한다. (그리고 걱정마라: 적절한 아키텍처를 고른다면 어렵지 않을것이다.)
  • 사용하기 편해야(Ease of use)하고 유지보수하기 쉬워야한다.

분리해야하나?
분배는 우리가 이게 어떻게 동작하는지 알아낼려고 노력하는 동안 우리의 뇌에서 균등하게 생각하도록 해준다. 만약 당신이 천재라 생각되면 그냥 하던대로 해라. 그러나 능력은 선형적으로 커지니 않을 뿐더러 광장히 빨리 한계에 도달해버린다. 그러므로 가장 빨리 복잡한 것을 극복하는 방법은 하나의 책임 단위 수많은 개체들의 책임을 쪼개는 것이다.

테스트 가능해야하나?
이미 유닛테스트에대한 중요성을 알고 있는 사람에게 던지는 질문이 아니라, 기능을 추가한 일때나, 클래스의 몇몇 복잡성을 리팩토링을 하기 위해서 테스트에 실패하는 사람들이 하는 의문이기도하다. 이것은 테스트가 런타임 내에서의 이슈를 찾는데 도와주며, 반대로 실유저에게 이슈가 발생한다면 그걸 고친 앱을 다시 실유저가 다시 사용하기까지 일주일씩이나 걸린다.

사용하기 쉬워야하나?
가장 좋은 코드가 뭔지는 한번 언급해 가치가 있다: 하나도 작성하지 않은 코드이다. 따라서 적은 양의 코드는 버그가 적다. 게으른 개발자 말을 빌려 적은 코드를 작성 하기를 갈망하며 이것은 코드를 설명해야하면 안된다. 또한 당신이 눈을 감고 허우적대며 유지보수하는 솔루션을 원치도 않을 것이다.

필수 MV(X)

요즘은 아키텍처 설계를 할때 수많은 선택지가 있다:

위에서 세개(MVC, MVP, MVVM) 아래 3 카테고리중 하나는 들어가있다:

  • Models데이터나 데이터 접근 레이어(Person 클래스나 PersonDataProvider 클래스와 같이 데이터를 다루고있는) 소유를 책임지는 부분
  • Views레이어에 표현되있는 것을 책임지는 부분(GUI), iOS 환경에서는 'UI' 접두로 붙는다(역자주: UILabel, UIView 등등..).
  • Controller/Presenter/ViewModelModel View 붙여준다. 보통 유저가 View에서 어떤 액션을 취할때 Model 변경하거나 Model 변경되었을 , View 갱신하는 책임을 가지는 부분

개체들을 나눌때 이점:

  • 이전보다 이해할 있다(이미 알고 있다 하더라도).
  • 재사용 가능하다(대부분 View Model 적용 가능하다).
  • 독립적으로 테스트 가능하다.

어서 MC(X) 패턴을 시작하고 나중에는 VIPER까지 해보도록 하자.

MVC

이전에는 어떻게 사용해왔느냐

애플의 MVC 논하기 전에 전통적인 MVC 어떻게 사용되었는지 보자.

Traditional MVCTraditional MVC

경우는 View 범위가 정확하지 않다. Model 변경 바뀌고나서 Controller에의해 한번 랜더링(rendering) 된다. 웹페이지에서 다른 페이지로 있는 링크를 누른 , 다시 로딩되는 것을 생각해봐라. iOS 앱에서 전통적인 MVC 구현하는것은 가능할지라도 구조적인 문제때문에 효과적으로 처리할 없으며 당신 앱이 그러기도 원치 않는다.— 모든 개체가 둘씩 묶여있고, 개체는 다른 두개에 대해 알고있다. 이것은 각기 그들이 재사용성을 심각하게 줄여버린다. 이러한 이유로 우리는 흔히 쓰는 MVC 작성하는 또한 스킵 하겠다.

전통적인 MVC 최신 iOS 개발에 적합해 보이지 않는다


Apple’s MVC

기대한것..

Cocoa MVCCocoa MVC

원래 Controller Model View 연결시켜주는 역할을 하므로 서로에 대해 알필요가 없다. 그중에 가장 재사용 불가능한 것이 Controller이며, 우리도 그걸 알고있다. 따라서 우리는 모든 특이한 로직을 Model 아닌 Controller 넣어야한다.

이론적으로는 굉장이 전략적으로 보이지만 뭔가 문제가 있다. 당신은 MVC 컨트롤러 덩어리(Massive View Controller) 불리는걸 들은적이 있을지도 모른다. 나아가 View Controller offloading iOS 개발자들에게 중요한 토픽이다. 애플은 전통적인 MVC 조금 개선하여 사용하여서 이런 일이 일어나버린건가?

Apple’s MVC

실체는..

Realistic Cocoa MVCRealistic Cocoa MVC

Cocoa MVC View Controller 덩어리 작성하도록 만들어버린다. 이유는 View들의 라이프 사이클 안에서 뒤엉키는데 그것들을 분리해내기가 어렵기 때문이라고 말한다. 너가 Model*비지니스 로직이나 데이터 변환같은 것을 없애는 능력을 가졌을 지라도 대부분의 View에서 반응하면 액션을 Controller로 보내게 될것이다. 뷰 컨트롤러는 결국 모든 것의 델리게이트(delegate)나 데이터소스(data source)가 될테고, 종종 네트워크 요청과같은 처리도 하고 있을지 모른다. 

이런 종류의 코드를 얼마나 많이 보았는가:

Model 함께 직접적으로 구현된 View cell MVC 가이드라인을 위반한다. 그러나 항상 그렇게 사용하며 사람들은 이게 문제가 아니라고 느낄때가 많다. 좀더 MVC 따르고자 한다면 cell Controller에서 구성하고 View 안에 Model 거치지 않아햔다. 그러나 그렇게해버리면 Controller 커져버리게 될것이다.

Cocoa MVC View Controller 덩어리의 이유이기도하다.

문제는 유닛 테스트(여러분 프로젝트에 있기를 바란다)에까지 나타날 거라는걸 확신할수 없다. 당신의 View Controller View 붙어있고, 이렇게하면 그들의 View 라이프 사이클이나 테스트를 위한 View 만들기가 어려워지기 때문에 테스트가 힘들어진다. 반면 View Controller 코드를 작성하고 있으면 당신 비지니스 로직은 가능한 View 레이아웃 코드로부터 분리될것이다.

간단한 예제를 보자:

MVC 분리하면 현재 View Controller안에서 동작되게 있다.

테스트하기 좋아보이지는 않다. 우리는 greeting 생성을 GreetingModel 클래스에 옮겨 넣을 있다. 그러나 GreetingViewController안에서 UIView 연관되어있는 메소드(viewDidLoad, didTapButton) 호출하지 않은체 상연 로직(예제에는 로직이 많이 없지만) 테스트를 수가 없다.

사실, 로딩테스트는 디바이스를 바꿔가며(iPhone4S, iPad 등등으로) 확인해보는 것에대한 이점이 없다. 그래서 Unit Test target configuration에서 “Host Application” 지우고 시뮬레이터 없이 테스트 해보는것을 추천한다.

View Controller 사이의 상호작용은 Unit Test로써 테스트하기에 좋지 않다.

위에서 말한건, Cocoa MVC 사용하는것은 별로 좋지 않은 선택인것 같아 보인다는 것이다. 그러나 글의 서두에 언급했단 특징들의 용어를 정의했었다.

  • Distribution사실 뷰와 모델은 분리되 있지만, View Controller 붙어있다.
  • Testability거지같은(?)분리 때문에 아마 Model 테스트 가능할 것이다.
  • Ease of use다른 패턴에 비해 코드가 적게 든다. 추가로 많은 사람들이 친숙하게 사용하기도하며 경험해보지 못했던 개발자도 쉽게 접근할 있다.

Cocoa MVC 아키텍처 쪽에 시간을 투자할 시간이 별로 없을때 선택하는 패턴이며, 작은 프로젝트에는 지나친 유지보수 비용이 들어간다는 것을 느낄 있을 것이다.

Cocoa MVC 개발 속도면에서는 최고의 아키텍처 패턴이다.


MVP

전달될거라 약속한 Cocoa MVC(Cocoa MVC’s promises delivered)

Passive View variant of MVPPassive View variant of MVP

사진이 애플의 MVC 굉장히 비슷하지 않는가? 이것의 이름은 MVP(Passive View Variant)이다. 그럼 애플의 MVC MVP 같다는 걸까? 그렇지 않다. MVC에서는 View Controller 서로 붙어있지만 MVP에서 중간다리 역할을 하는 Presenter View Controller의 라이프 사이클에 아무런 영향을 끼치지도 않으며, View 쉽게 테스트가능한 복사본(moked) 만들 있다. 그러므로 Presenter에는 레이아웃 관련 코드가 없고 오직 View 데이터와 상태를 갱신하는 역할만 가진다.

만약 UIViewController View라고 말했으면 어떨까.

사실 MVP 입장에서는, UIViewController 자식클래스에 Presenter 아닌 View들이 있다. 이러한 구분은 좋은 테스트 용이함을 제공하지만, 수작업의 데이터나 이벤트 **바인딩 따로 만들어야하기때문에 개발 속도에대한 비용도 따라 온다. 아래 예제에서 확인할 있다:

Important note regarding assembly(중요 요약 모음)

MVP 세개의 다른 레이어를 가짐으로써 이런 문제 집합이 처음으로 나타난 패턴이다. 그러므로 뷰가 Model에대해 알기를 원치 않기 때문에, 현재 View Controller(View 것이다) 모아서 동작시키는건 옳지 않으므로 다른곳에서 동작시켜야한다. 예를들어, 우리는 앱에서 범용적인 모아서 수행하거나 View-to-View 보여주기위한 Router 돌릴 있다. 이슈는 MVP 뿐만아니라 아래 모든 패턴들에게도 나타나는 문제이기도하다.

이제 MVP 특징 보자.

  • DistributionPresenter Model 책임을 거의 분리했고 View 빈껍데기가 셈이다( 예제에서는 Model 빈껍데기 같았지만..)
  • Testability최고로 좋다. View 재사용가능 덕분에 대부분의 비지니스 로직을 테스트 있다.
  • Easy of use위에서 비현실적인 예제에서는 MVC에비해 코드의 양이 2배정도 많이 들지만 MVP 아이디어는 굉장히 명료하다.

iOS에서 MVP 테스트하기엔 좋지만 코드가 길어진다.


MVP

With Bindings and Hooters

MVP 다른 버전(MVP Supervising Controller) 있다. 이러한 다양한 MVP들은 Presenter(Supervising Controller) View로부터 액션을 처리하고 View 적합하게 변경하는 동안 View Model 직접 바인딩을 포함한다(?).

Supervising Presenter variant of the MVPSupervising Presenter variant of the MVP

그러나 우리가 이미 이전에 배웠듯, 막연하게 책임을 나누는건 좋지않은데다, View Model 합쳐버린다. 이것은 Cocoa 데스크탑 개발에서 어떻게 동작하는지와 비슷하다.

전통적인 MVC와같이, 결함이 있는 아키텍쳐의 예제를 찾기 힘들었다.

MVVM

마지막이자 MV(X) 종류의 최고 종류

MVVM은 최근에 나온 MV(X) 종류이다. 그러므로 이전의 MV(X) 문제들을 해결하여 나오기를 기대해보자.

이론적으로는 Model-View-ViewModel이 굉장히 좋아보인다. ViewModel은 이미 우리에게 친숙할테고, View Model 이라불리는 중계자 또한 마찬가지일 것이다.

MVVMMVVM

MVP 비슷하다:

- MVVM View Controller View라고 일컫는다.
- View Model 서로 연결 되어있지 않다.

추가로 MVP Supervising버전에서 처럼 binding 있다; 그러나 여기서는 View Model 관계가 아닌 View View Model 사이의 관계이다.

그래서 실제 iOS에서 View Model 뭘 의미할까? 그것은 기본적으로 UIKit인데 그로부터 View 독립된 표현이거나 상태이다. View ModelModel에서 변경을 호출하고 Model 자체를 갱신한다. 따라서 View나 View Model 사이에서 바인딩을 하며, 적절히 처음것이 갱신된다.

Bindings(바인딩)

MVP 파트에서 간당하게 언급한적이 있다. 그러나 여기서 좀 더 이야기 해보자. 바인딩은 OS X 개발을 위한 박스(역자주: 프레임워크나 툴을 말하는듯 합니다)에서 나왔으나 iOS 툴박스에서는 보지못한다. 물론 KVO나 notification을 가지고 있긴 하지만 그것이 바인딩만큼 편리하지는 않다.

그러므로 

- 바인딩 기반 라이브러리인 KVO에는 RZDataBinding 혹은 SwiftBond 이런게 있다.
- The full scale functional reactive programming beasts like ReactiveCocoa, RxSwift or PromiseKit. (번역하지 못했습니다ㅠ)

사실 요즘엔 MVVM을 들으면 바로 ReactiveCocoa를 말하기도하며, 반대도 그렇다(역자주: 뭐라고??????). 비록 간단한 바인딩으로 MVVM을 만드는게 가능하기는 하나 ReactiveCocoa (혹은 siblings)으로는 최고의 MVVM을 만들수 있게 해준다.

Reactive 프레임워크에는 쓰디쓴 진실이 하나 있다: 큰 책임엔 큰 에너지가 필요하다. Reactive를 사용하게되면 굉장히 혼잡해지기 쉬워진다. 다른말로 설명하자면, 문제가 하나 생기면 앱을 디버깅하는데 시간이 굉장히 많이 걸리며, 아래와 같은 콜 스택을 보게 될것이다.

Reactive DebuggingReactive Debugging

우리의 예제에서는 FRF 프레임워크나 KVO까지도 배보다 배꼽이 식이다. 대신에 showGreeting 메소드를 이용하여 갱신하기 위한 View Model 명백하게 물어 것이고 greetingDidChange 콜백 함수를 위해 작은 프로퍼티를 사용할것이다.

이제 돌아와서 특징들을 나열해보겠다:

  • Distribution우리의 작은 예제에서는 명료하게 나타나지 않았지만, 사실 MVVM View MVP View보다 책임이 많다. 왜냐면 두번째 것이 Presenter 포워드(forward)하고 자신를 갱신 하지는 않은 그 때, 바인딩을 세팅함으로써 View Model에서 처음 것의 상태를 갱신한다.
  • TestabilityView Model View에대해 전혀 모르며, 이것이 테스트하기 쉽게 해준다. View 또한 테스트 가능하지만 UIKit 의존이면 그러고 싶지 않게 원하게 될것이다.
  • Easy of use우리 예제에서는 MVP 비슷한 양의 코드나 나왔으나 View에서 Presenter으로 모든 이벤트를 포워드하고 View 갱신하는 실제 앱에선 바인딩을 사용했다면 MVVM 코드 양이 적을 이다.


MVVM 앞에서 말한 장점들을 합쳐놓은것 같아서 굉장히 매력적이다. 그리고 View입장에서 바인딩을 하기 때문에 View 갱신하는데 추가적인 코드를 필요로 하지도 않는다. 그럼에도불구하고 테스트에도 굉장히 좋은 수준이다. (역자주: 완전 극찬이군요)


VIPER

iOS 설계에 레고 조립 경험을 적용하다

VIPER 마지막 지원자다. 이것이 특별히 흥미로운 이유는 MV(X) 카테고리로 부터 나온 녀석이 아니기 때문이다.

이제부터 당신은 책임의 단위가 매우 좋다고 인정하게 될것이다. VIPER 분리된 책임이라는 아이디어에서 생겨난 다른 iteration 만드며, 이번 시간에는 다섯 레이어를 것이다.

VIPERVIPER

  • Interactor데이터 개체나 네트워킹과 연관되어있는 비지니스 로직을 가지고, 서버로부터 그들을 받아오거나 개체 인스턴스를 만드는것을 좋아한다. 이러한 목적으로을 위해서 당신은 VIPER 모듈의 일부로써 몇몇 Services Managers 사용해야 것이나, 다소 외부 의존도가 있을것이다.
  • Presenter—Interactor에서 발생되고 비지니스 로직과 관련있는 (그러나 UIKit과는 관련없는) UI 가진다.
  • Entities일반적인 데이터 객체이다. (데이터 접근 레이어(data access layer) Interactor 책임이기 때문에 Entities 아니다.)
  • Router—VIPER 모듈 사이의 연결고리(seques) 책임을 가진다.

기본적으로 VIPER 모듈은 스크린(screen)이나 당신 어플리케이션의 모든 ***사용자 스토리(user story) 있다인증을 생각해보면 스크린이나 여러개가 하나에 연관되어 있을 있다. 얼마나 작은 “LEGO” 블럭어여야 할까?—전적으로 당신에게 달려있다.

MV(X) 종류와 비교하면, 우리는 책임의 분리가 다르다는걸 확인할 있다:

  • Model(data interation) 로직은 데이터 구조로써 Entities 함께 Interactor 이동된다.
  • 오직 Controller/Presenter/ViewModel Presenter 이동하는 UI 표시 책임을 갖지만, 데이터를 변경할 능력은 없다.
  • VIPER 명시적으로 Router에의해 결정된 네비게이션 책임을 해결한 패턴이다

iOS 어플리케이션 입장에서는 각기 방법으로 라우팅 하는게 도전이라고 수있다. MV(X) 패턴들은 이러한 이슈가 발생하지 않는다.

토픽이 MV(X) 패턴을 반영하지 못했으므로, 예제 또한 라우팅이나 모듈간의 interaction 반영하지 않았다

이제 다시 돌아와 특징들을 살펴보자:

  • Distribution틀림없이 VIPER 책임 분배의 최고봉이다.
  • Testability분리가 잘되있는만큼 테스트에도 좋다.
  • Easy of use마지막으로 여러분이 이미 추측한것처럼 두배 정도의 유지보수 비용이 들것이다. 매우 작은 책임을 위해 수많은 클래스 인터페이스를 작성해야하는 점이다.

그래서 레고는 뭐였나?

VIPER 사용하는 동안 레고로 엠파이어 스테이트 빌딩(위키:엠파이어 스테이트 빌딩은 1931년부터 1972년까지 세계 최고층 건물이었다.) 쌓는 기분이 들것이자, 이것이 유일한 문제이기도하다. 아마 당신 앱에 VIPER 적용시키기에 이를수도 있고 좀더 간편한것으로 고려해도 좋다. 몇몇 사람들은 이걸 아예 무시하고 대포에다가 화살을 쏘아대는 경우도 있다. 지금은 비록 엄청나게 높은 유지보수 비용이 들지만, 그들이 미래에는 그들의 앱에 VIPER 필요할지도 모른다는걸 알고있을거라 생각한다. 만일 당신도 생각이 같다면 Generamba(VIPER 골격을 제공해주는 ) 한번 사용해보길 바란다. 개인적으로는 이건 새총 대신에 자동 대포 조준 시스템을 사용하는 느낌이긴하다.


결론

우리는 몇몇 다른 아키텍처 패턴을 살펴보았고, 무엇이 당신을 괴롭히는지 찾아냈기를 바란다. 그러나 여기에 완벽한 해답은 없고 아키텍처를 선택하는게 당신의 특별한 상황에서 문제의 비중을 등가교환하게 된다는걸 알게되었음을 의심하지 않는다

그러므로 앱에 다른 아키텍처를 섞어 사용하는것은 자연스러운 일이다. 예를들어 MVC 시작했지만 어떤 화면에서만 MVC 관리하기 어려워지는 상황이 생기면 부분만 MVVM으로 바꿀 있다. 이런 아키텍처들은 서로 공존할 있기때문에, 다른 화면이 MVC 골격으로 동작하면 바꿀 필요가 없다



Make everything as simple as possible, but not simpler 
이론은 가능한 간단해야하지만지나치게 간단해서는 안된다
— Albert Einstein




*비지니스 로직 (business logic)

**바인딩 (binding)

***사용자 스토리 (user story)



iOS 아키텍처 관련 번역글


+ Recent posts