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를 시도해보기에 관심이 생겼을 것이라 생각된다.
추가적인 읽을거리
iOS 아키텍처 관련 번역글
'Swift와 iOS > 아키텍처' 카테고리의 다른 글
(번역)Real World Flux Architecture on iOS (0) | 2016.10.01 |
---|---|
(번역)Implementing MVVM in iOS with RxSwift (0) | 2016.10.01 |
(번역)Architecting iOS Apps with VIPER objc.io (0) | 2016.08.27 |
(번역) 언제 VIPER를 사용할까? (0) | 2016.07.09 |
(번역) iOS 아키텍쳐 패턴들 (스압주의) (8) | 2016.02.15 |
WRITTEN BY
- tucan.dev
개인 iOS 개발, tucan9389
,