제목: Protocols and MVVM in Swift to avoid repetition

우리가 Viable을 최신 iOS 앱의 토대를 만들어갈때, 이전 iOS 앱으로부터 배우려 했다. 우리는 2가지 목표를 정했다.
  • Massive View Controller(MVC를 비꼬는 약자) 증후군 피하기
  • 가능한 적은 중복
초기에 디자인팀이 만든 Viable 화면에는 수많은 비슷한 화면이었다. 아래에 간단하게만든 예시를 한번 보자. 두 화면은 모두 상단에 UILabel이 있고 검색 결과를 보여주는 UITableView가 있다. 각각의 결과에대한 UITableViewCell도 매우 비슷했다. 이들은 다소 레이아웃을 공유했고 데이터만 달랐다.


Viable은 화면에 표시되는 6가지 타입의 데이터가 있었으며, 각 타입마다 새로운 뷰 컨트롤러를 만들어서 코드 중복이 많았다. 그리하여 우리는 6개의 데이터 타입을 모두 표시할 수 있는 SearchResultsViewcController를 만들었다.
데이터 타입에따라 다르게 렌더링하기위해 제일 처음 떠오른 방법으로는, tableView:cellForRowAtIndexPath:에 거대한 if/else문이었는데, 코드 규모가 잘 정연되지 못했고 결국 길고 못난 메소드가 되버렸다.

MVVM와 프로토콜을 사용하여 해결하기
테일러 구이던(Taylor Guidon)은 MVVM(Model-View-ViewModel) 패턴에대한 입문의 글을 포스팅했는데, 여기서 확인할 수 있다. 이 글은 그 요약 버전인데, 데모 프로젝트에 적용한 것을 깃헙에서 확인할 수 있다.

모델(Models)
모델 그룹에서의 모델은 데이터를 담고 있는다. 우리는 DomainModelProductModel을 가지는데, 둘 다 구조체이다. DomainModel은 이름(name)과 그 상태 도메인을 가질것이고, ProductModel은 제품이름(product name), 제품평점(product rating), 제품로고(product logo), 제품가격(product price)을 가진다.
struct Product {
    var name: String

    var rating: Double

    var price: Double?
}

뷰모델(View Models)
모든 데이터 모델은 해당되는 뷰모델을 가진다. 그 말은, 우리 예제에서는 DomainViewModelProductViewModel을 가진다는 뜻이다. 뷰모델은 모델로부터 데이터를 받아서 사용자에게 보여주기전에 뷰에 적용시킨다. 예를들어 ProductViewModel4.99 가격의 부동소수점을 받아서 $4.99라 읽히는 문자열로 변형한다.
class ProductViewModel: CellRepresentable {
    var product: Product

    var rowHeight: CGFloat = 80

    var price: String {
        guard let price = product.price else {
            return "free"
        }

        return "$\(price)"
    }

    init(product: Product) {
        self.product = product
    }

    func cellInstance(_ tableView: UITableView, indexPath: IndexPath) -> UITableViewCell {
        // Dequeue a cell

        let cell = tableView.dequeueReusableCell(withIdentifier: "ProductCell", for: indexPath) as! ProductTableViewCell


        // Pass ourselves (the view model) to setup the cell

        cell.setup(vm: self)

        // Return the cell

        return cell
    }
}

뷰(Views)
우리 예제에서 뷰는 두가지 UITableViewCell이다. DomainTableViewCellProductTableViewCell를 가진다. 레이아웃은 앱의 스토리보드에 만들어놓았따. 두 클래스 모두 간단한데, 뷰모델을 인자로 받는 setup 메소드 하나만 가지고 있다. 뷰모델은 셀에 정보를 옮길때 사용되는데, 예를들자면 읽을 수 있는 가격($4.99)을 받아서 UILabel의 테스트 프로퍼티에 할당한다.
class ProductTableViewCell: UITableViewCell {
    func setup(vm: ProductViewModel) {
        self.textLabel?.text = vm.product.name
        self.detailTextLabel?.text = vm.price
    }
}

합쳐보기
3가지 큰 기둥을 만들었으니 합쳐보자. 뷰 컨트롤러와 뷰모델을 합치기위해 프로토콜을 사용할 것이다. 프로토콜은 이것을 따르는 클래스나 구조체가 어떤 변수와 메소드를 가질지 정의한다. 계약서를 생각해보자. 여러분이 X라는 프로토콜을 따르고 싶다면, 여기에 명시된 모든것을 구현해야한다. 간결하게 만들기위해 한 프로퍼티와 한 메소드만 넣어놨다. DomainViewModelProductViewModel 둘 다 이 프로토콜을 따른다.
protocol CellRepresentable {
    var rowHeight: CGFloat { get }
    func cellInstance(_ tableView: UITableView, indexPath: IndexPath) -> UITableViewCell

}
스위프트에서 프로토콜은 일급 객체(first class citizen)이므로 SearchResultsViewController 파일은 화면에 표시할때 필요한 뷰모델 배열을 가진다. [DomainViewModel]()이나 [ProductViewModel]()처럼 배열을 초기화하는것 대신, 프로토콜을 사용하여 뷰모델을 담아둘 수 있다. var data = [CellRepresentable](). DomainViewModelProductViewModelCellRepresentable을 따르기 때문에 배열은 둘 다 담아둘 수 있다.

이제 배열에 있는 모든 요소를 CellRepresentable을 따르게하여 UITableViewCell을 반환하는 cellInstance(_ tableView: UITableView, indexPath: IndexPath) 메소드를 가진다고 확신하게 만들자. 고맙게도 tableView:cellForRowAtIndexPath:cellInstance 메소드만 호출하면 된다.
extension SearchresultsViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return data.count
    }
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        return data[indexPath.row].cellInstance(tableView, indexPath: indexPath)
    }
}

extension SearchresultsViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return data[indexPath.row].rowHeight
    }
}
이게 전부다. 우리는 다양한 셀의 다양한 열 높이로 표시해주는 작은 뷰컨트롤러를 가지게 되었다! ISL의 깃헙 페이지에서 데모 프로젝트를 확인해볼 수 있다. 제안이나 질문이 있다면 주저하지말고 @thomasdegry에 트윗해달라.



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

으로 보내주시면 됩니다.


WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,
제목: 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



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

으로 보내주시면 됩니다.


WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,
제목: Service-oriented AppDelegate


보통 앱델리게이트(AppDelegate)는 거대한 클래스이다. 이것은 여러분의 앱에 대해 너무 많이 알고 있고 점점 더 커지게 된다. 이 글에서는 깔끔한 플러그인 기반 아키텍처를 만들어서 그 세부 기능으로부터 어떻게 분리해낼 수 있는지 보여줄 것이다.

우리는 올바르지 않은 방법으로 문제를 직면하고 있다. 당신의 앱델리게이트를 어떻게 관리하고 있는지 생각해보자. 내 생각엔 굉장히 거대하거나, 아니면 적어도 거기서 코딩한 것들에 대해 자신감이 있지는 않을 것 같아 보인다.

앱델리게이트의 문제는 바로 앱델리게이트가 여러분의 앱에대해 너무 많이 알고 있다는 점이다. 이것은 여러분의 의존성들을 알고있는데, 어떻게 초기화하는지, 어떻게 푸시 노티피케이션을 파싱하는지 등 많은 것을 알고 있다.

요약을 해보자면
ApplicationSercvices를 만들어낼 수 있다. 이것들은 AppDelegate 라이프사이클을 공유하고 해당 단계에 작업을 실행하는 오브젝트이다. 여러분의 앱델리게이트는 피관찰자(Observable)이고 여러분의 서비스들은 관찰자(Observer)이다. 이런 접근법은 그 서비스들로부터 앱델리게이트를 분리시켜주고 단일 책임 오브젝트를 생성해준다.

물론 많은 양의 코드를 작성하게 되므로 이 문제를 다루기위해 CocoaPod을 만들었다. PluggableAppDelegate라 부르며 여기에서 확인할 수 있다: http://github.com/fm091/PluggableApplicationDelegate

서비스를 생성해서 AppDelegate에 등록만 하면 된다. 그 결과는 여러분이 봐왔던 것 중에 가장 작고 깔끔한 AppDelegate가 될것이다.

일반적인 앱델리게이트
일반적으로, 앱델리게이트는 많은 역할을 가지고 있다.
  1. 여러분 앱의 모든 의존성을 초기화한다.
  2. 전역의 UIAppearance 값들을 초기설정한다.
  3. 푸시 노티피케이션을 다룬다.
  4. 푸시 노티피케이션을 등록한다.

그밖에 더 있을 것이다.

여기서 문제는 여러분의 앱델리게이트가 정확하게 모든 컴포넌트가 어떻게 동작하는지, 그리고 어떻게 인스턴스화하는지 알고 있기 때문에 생긴 것이다. 또한 이것은 여러분의 앱이 어떻게 생겼는지도 알 수도 있다. 그리고 푸시 노티피케이션 포멧도 알고있다. 결과적으로 생각했던것보다 너무 커져버린다.

다른 접근법
ApplicationService라 부르는 그 고유의 컴포넌트에 여러분의 의존성들을 캡슐화하여 들고 있다고 생각해보자.

모든 ApplicationService는 하위-앱델리게이트(sub-AppDelegate)이다. 이것은 앱델리게이트의 라이프사이클을 공유하고, 요청된 이벤트에서 행동을 취한다.

여러분 AppDelegate는 이벤트의 발생를 관찰하는 관찰자(observable)이 되어, 어떤 일이 일어나면 서비스에게 알려주기만 하면 된다.



어플리케이션 서비스들
어플리케이션 서비스들은 AppDelegate 라이프사이클을 공유하는 오브젝트들이다. ApplicationService는 그냥 태그한 프로토콜이다. 만약 PluggableApplicationDelegate를 사용한다면 이렇게 생겼을 것이다.
public protocol ApplicationService: UIApplicationDelegate {}
위에서 본 것처럼, UIApplicationDelegate만이지만, 더 전달력있는 이름이다.

이것을 구현할때, AppDelegate를 구현하듯이 하면 된다. 그러나 여기서 다른점은 단일 책임 요소를 코딩한다는 점이다.

아래에 구현에대한 예시가 있다.
import Foundation
import PluggableApplicationDelegate

final class LoggerApplicationService: NSObject, ApplicationService {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool {
        print("It has started!") return true
    }
    func applicationDidEnterBackground(_ application: UIApplication) {
        print("It has entered background”)
    }
}

새로운 앱델리게이트
PluggableApplicationDelegate를 상속받고 이것을 서비스에 등록함으로서, 이제 여러분의 앱델리게이트는 더 작아졌다.

아래를 확인해보자
import UIKit 
import PluggableApplicationDelegate 

@UIApplicationMain 
class AppDelegate: PluggableApplicationDelegate { 

    override var services: [ApplicationService] { 
        return [ LoggerApplicationService() ] 
    } 
}

PluggableApplicationDelegate는 너무 크다. 뭔가 일어났을때만 그 서비스에 알려준다. 그것에대한 코드를 여기서 확인할 수 있을 것이다.

의존성과 기능을 점점 더 앱에 추가하더래도 앱델리게이트는 아래처럼 깨끗하게 유지될 것이다.
import UIKit 
import PluggableApplicationDelegate 

@UIApplicationMain 
class AppDelegate: PluggableApplicationDelegate { 

    override var services: [ApplicationService] { 
        return [ 
            PushNotificationsService(), 
            AnalyticsService(), 
            CustomAppearanceService(),
            FirebaseService(), 
            FacebookSDKService() 
        ] 
    } 
}

결론
이게 다다. 여러분의 앱델리게이트를 분리시키는 간단한 방법이며, 그것이 커다란 덩어리로 되지 않게 막아준다.

내 깃헙 저장소를 꼭 확인해보기 바란다:

내가 도움이 됐던 만큼 여러분에게도 도움이 되길 바라며, 이것에대한 피드백을 꼭 받고싶다. 아래 댓글로 남겨달라

고맙다!


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

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,
제목 : Building iOS App with VIPER Architecture



이 글은 완전히 iOS VIPER 아키텍처에 관한 내용이다. 우리는 3가지 포인트를 통해 이야기해 나갈 예정이다.
  • VIPER 아키텍처란?
  • VIPER 아키텍처로 iOS 앱 만들기
  • VIPER 아키텍처의 이점들
이 표준 아키텍처는 재사용성과 테스트용이함에대해 코드를 분리시켜주는 중요한 역할을 한다. 이 아키텍처는 그 역할에 맞춰 앱 컴포넌트를 분리시키며, 이것은 seperation of concern이라 부른다.

이제 iOS를 위한 VIPER 아키텍처에대해 탐험해보자.

VIPER 아키텍처란?
VIPERView, Interactor, Presenter, Entity, Router로 구성되있다.

이 아키텍처는 단일책임원칙(링크)를 기반으로 하는데, 이것이 명확한 아키텍처로 만들어준다.
  • View : View의 책임은 사용자의 동작을 Presenter로 보내주고 Presenter가 요청하는 모든 것을 보여준다.
  • Intereactor : 이것은 비지니스 로직을 가지고있는 앱의 뼈대이다.
  • Presenter : 이것의 책임은 사용자 동작에 Interactor에서 데이터를 뽑아온 뒤, 그것을 보여주기 위해 View에 보낸다. 또한 네비게이션(화면이동)을 위해 router(혹은 wireframe)에게 물어보기도한다.
  • Entity : Interactor에서 사용하는 기본 오브젝트 모델이다.
  • Router : 이것은 어느 화면이 언제 나타날지에대한 정보를 담고 있는 네비게이션 로직이다. 보통 wireframe으로 쓰인다.

VIPER 아키텍처의 청사진


보통 VIPER 아키텍처는 큰 프로젝트에서 쓰인다. 그러나 이해를 돕기위해 이것을 위한 작은 앱을 하나 만들었다.

VIPER 아키텍처로 iOS 앱 만들기
나는 VIPER 아키텍처를 사용한 샘플 iOS 앱을 하나 만들었다.


이해를 돕기위해 프로젝트의 구조를 보자


샘플 앱의 스크린샷이다.



이 앱은 3가지 화면으로 구성된다.
  • 시작화면 : 일반적인 시작화면이다 따라서 더 설명할건 없다.
  • PostListView :화면  이 PostListView는 포스팅 목록을 가져와라고 Presenter에게 말한다. 그런 다음 Presenter는 관련 데이터를 위해 Interactor에게 접근한다. Intereactor는 로컬 데이터베이스에서 그 데이터의 사용 가능 여부를 확인해보고, 만약 데이터가 있으면 Presenter로 반환하고, Presenter는 View로 반환한다. 데이터가 로컬 데이터베이스에 없을경우 네트워크를 호출하여 데이터를 가져온 다음 Presenter에게 반환한다. 그리고 역시 이 데이터를 로컬 데이터베이스(CoreData)에 저장한다.
  • PostDetailView 화면 : 사용자가 PostViewList에 표시된 포스트를 클릭하면, PostListPresenter는 PostDetailView를 열어도 되는지 Router(PostListWireFrame)에게 물어본다. 선택된 포스팅의 세부사항이 이 화면에 나타난다.

그리고 이 프로젝트에서 대부분 클래스간의 의사소통은 정의된 프로토콜을 통해 일어난다.

이것을 완전히 이해하는데 가장 좋은 방법은 소스코드를 확인하고 구현해보는 방법일 것이다. 프로젝트를 깃으로 클론해서 빌드&런 해보자.

VIPER 아키텍처의 이점들
  • 재사용성과 테스트용이함을 위해 코드를 분리할 수 있다.
  • 그 역할에 맞춰 앱 컴포넌트를 분리할 수 있으며, 이것을 seperation of concern이라 부른다.
  • 새 기능을 추가하기 쉽다.
  • UI 로직이 비지니스 로직으로부터 떨어져있기 때문에 자연스럽게 테스트를 만들기 쉬워진다.

이게 다다. 즐거운 코딩하길 바란다 :)


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

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

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



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

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

안드로이드 프로젝트에서 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

WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

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 앱을 위한 정의된 라우터



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,
원문 :  https://medium.cobeisfresh.com/implementing-mvvm-in-ios-with-rxswift-updated-for-swift-2-51cc3ef7edb3#.jzmyljsky

iOS에서 MVVM를 적용시키는 수많은 글들이 존재하지만, 실제로 사용되는 MVVM는 어떻게 생겼는지, 사실상 어떻게 하는지에 초점이 맞춰져있는 글은 거의 없다. 이 글은 RxSwift를 사용하여 좀 더 실질적인 관점에서 MVVM를 살펴 볼 것이다.

ReactiveX는 시퀸스를 observable 함으로서 비동기와 이벤트 기반 프로그램으로 구성된 라이브러리이다.— reactivex.io

RxSwift는 ReactiveX의 Swift 버전이다. 이것은 리엑티브하게 프로그래밍 할 수 있게 도와주는 프레임워크다. 만약 이게 무슨 말인지 모르겠어도(아마 그럴것이다. 함수형 리엑티브 프로그래밍(FRP)은 최근에 각광받기 시작했다.) 멈추지 말고 한번 읽어보길 추천한다. 리엑티브는 여러분의 프로젝트를 더 간결하고, 유지보수 하기 쉽고, 다루기 쉽게 만들어 줄 것이다.

어떻게 iOS 컴포넌트들이 서로 소통할까?
RxSwift의 가장 큰 부분은 앱에서 서로 다른 컴포넌트 사이에서 간단하게 소통할 수 있다는 점이다. 예를들어 Model과 ViewController가 있다. MVC에서는 이들을 연결하기 매우 난잡함을 느낄 수 있었을 것이다.

ViewController에서 모든 outlet을 리셋시키기위해, 아마 model이 갱신될때 항상 Controller에서 updateUI() 함수를 호출해주어야 할 것이다. 이러한 흐름은 불필요한 갱신이나 이상한 버그들이 생기면서 Model과 ViewController 사이에 부조화가 일어나기 쉽다.

우리는 매 순간마다 Model의 옳바른 상태를 표시하는 View Controller가 필요하다. Model이 어떻든 Model 갱신되는 즉시 일치한 데이터를 보여주는 View Controller가 필요하다.

물론 바로 Model만 표시하는 대부분의 앱에서는 의미없는 고민이겠지만, 우리는 Model로부터 데이터를 뽑아와서 화면에 표시할 준비를 하는 과정이 필요하다. 이것이 왜 ViewModel 클래스를 소개하게 되었는지에 대한 이유이다. ViewModel은 화면에 표시할 모든 데이터를 준비한다.

그러나 조금 재미있는 부분이 있다: ViewModel은 ViewController에대해 아무것도 모른다는 사실이다. 절때 그 안에서 직접적으로 참조하거나 프로퍼티를 가지고 있지 않는다. 대신에 ViewController는 ViewModel의 모든 변화를 항상 Observe하고 있으며, ViewModel에서 변화가 일어나면 그것을 화면에 표시한다.

한 프로퍼티당 기반임을 기억하고 있자. 이 의미는 ViewModel 안에서 ViewController가 화면에 개별적으로 각 프로퍼티를 표시한다. 예를들어, 문자열과 이미지를 불러올때, 그 두가지를 다 불러올 때까지 기다리고 있는 것이 아니라, 불러와지는데로 바로바로 각 이미지를 화면에 표시할 수 있다.

ViewController는 화면에 표시하는 역할 뿐 아니라 유저의 입력을 받는 역할도 한다. 우리 ViewController는 단지 프록시(proxy)이고, 그 입력을 ViewController에서 따로 사용하지 않으므로 모든것을 ViewModel로 보내버리고 이것을 ViewModel이 알아서 처리할 것이다.

위 그림은 ViewController와 ViewModel 사이에 단방향 통신을 하는 방법이다. ViewController는 ViewModel을 보고 그것에게 말할 수 있지만, ViewModel은 ViewController가 무엇인지 전혀 모른다. 이 말은 앱에서 ViewController를 완전히 제거해도 모든 로직이 제대로 동작할 것이라는 뜻이다!

좋아보이지 않는가?! 그러나 어떻게 이게 가능할까?

RxSwift와 함께 MVVM
유저의 도시 입력에 따른 기상 예측을 표시해주는 간단한 날씨 앱을 만들어보자.

이 글은 RxSwift의 기본 지식을 가정하고 쓰였다. 만일 ReactiveX에 대해 전혀 모른다면, 마음가는대로 읽어도 상관없지만, ReactiveX 글을 읽어보길 추천한다.


우리는 도시 이름을 입력받기 위해 UITextFeild를 준비하고 현재 온도를 보여주기위해 UILabel을 준비했다.

Note: 이 앱에서는 OpenWeatherMap의 날씨 데이터를 사용했다.

도시의 이름과 날씨로 구성된 Weather 구조체가 우리의 Model이 될 것이다. 이 구조체는 받아온 값을 파싱한 뒤, 속성에 맞춰 만들어진 JSON 오브젝트로부터 만들어진다.


이제 public의 searchText 프로퍼티가 변경될 때, ViewModel이 새 Model을 요청해야한다. ViewController는 유저 입력을 보내기 위해 이 프로퍼티에 접근하게 된다.

searchText는 변수이다. 변수는 필수적으로 BehaviorSubject를 감싼다. 이것은 Observer할수도, Observable할수도 있다. 다시말해 그들이 다시 호출할 수 있는 항목을 그들에게 보낼 수 있다.

BehaviorSubject는 한번만 구독되야하기 때문에 유일한 존재이다. BehaviorSubject는 받았던 마지막 항목을 보낸다. MVVM에서는 이러한 방식이 필요하다. 앱의 라이프 사이클에 의존하며, 다른 클래스에서 Observable은 종종 그것들을 구독하기 전에 엘리먼트를 받기도 한다. ViewController가 ViewModel의 프로퍼티에 구독하면, 화면에 표시하기위해 마지막 항목이 무엇인지 보아야한다. 반대의 경우도 마찬가지이다.

이제 우리는 프로그래밍적으로 변하는 모든 UI 부분에 대해 ViewModel 안에 한 프로퍼티를 정의할 것이다.

ViewModel은 데이터를 출력할 수 있는 형태로 변환하는 역할을 맡고 있다. 이 경우 우리의 Model은 다른 Weather 객체의 Observe되어지는 한 순서이다. 위 프로퍼티(cityName, degrees)는 Weather Observable에 다른 맵핑이 일어날 것이다.

이 프로퍼티가 private로 선언된 이유를  기억하자. ViewController에는 비즈니스 로직에 대해 전혀 몰라야 하기 때문이다. ViewController는 화면에 표시하기 위한 데이터 밖에 모른다.

검색
이제 우리가 위에서 선언한 searchText 프로퍼티에 우리의 Model을 연결해보자.

우리는 searchText가 바뀔때마다 네트워크 요청을 만들 것이다. 그리고 우리의 Model은 그 요청을 구독하고 있을 것이다.

이 경우 searchText가 바뀔때마다 jsonRequest는 NSURLRequest와 통신하기 위해 스스로 갱신된다. 갱신마다 우리의 Model은 NSURLRequest로부터 어떤것을 받던지간에 세팅된다.

만약 JSON 요청중 에러가 나오면 그것을 출력하고 빈 값을 반환한다.

Note: rx_JSON() 메소드는 실제로 그 스스로 Observable 순서이다. 그러므로 jsonRequest는 Observable의 Observable이다. jsonRequest가 가장 최신의 것을 리턴하기 위함이 마지막에 .switchLatest()를 사용하는지에대한 이유이다. 또한 요청을 당신이 그것에 구독하기 전까지 패치되지 않을 것이라는 것을 기억해두자.

.shareReplayWeather에 구독하는 모든 것들이 정확하게 같은 결과를 받았는지 확신하기 위함이다. 그렇지 않을 경우 각 구독은 날씨의 개별 객체를 호출할 것이고 요청이 중복으로 일어날 수 있기 때문이다.

이제 남은 것은 ViewController를 ViewModel에 연결하는 것이다. ViewModel의 Observable을 Controller의 outlet에 바인딩하여 연결할 수 있다.(We’ll do this by binding the PublishSubjects in the ViewModel to outlets in the Controller.)

사용자가 텍스트 필드에 친 값을 ViewModel이 알고 있어야함을 기억하자! ViewModel의 searchText 프로퍼티에 ViewController의 textField 값을 바인딩하여 위 일을 할 수 있다. 따라서 viewDidLoad()에 아래의 코드만 추가하면 된다:


이제 됐다! 우리 앱은 유저가 타이핑하는 동안 날씨 데이터를 갱신한다. 그리고 유저가 어떤 것을 볼지라도 화면 뒤의 앱 상태를 보게된다.

이 앱에서 좀 더 확장되고 주석이 달린 코드의 버전에 관심이 있다면 내 Github의 Weather 앱을 확인해보아라.


여기 당신이 흥미있어할 법한 더 많은 글들이 있다.


용어 정리

  1. Observable, Observer, Subscribe
    : 옵저버 디자인 패턴에서 사용하는 용어로, Observer는 구독(Subscribe)하는 오브젝트, Observable은 구독당하는 오브젝트를 말한다. Observer는 
    Observable에게 자기 자신을 넘겨줘서 Observable에서 이벤트가 발생할때 Observer에 있는 메소드를 호출해줌으로서 구독할 수 있다. 이 글에선 Observer나 Observable에 적합한 한글번역을 찾지 못해, 그대로 표기하였다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,


유닛 테스트에서 가장 힘든 시점은 시작 시점이다. 그 이유는 모든 아키텍처가 유닛 테스트 되지 않기 때문이다. 만약 유닛 테스트를 하고자 한다면(Part1(링크)에서 왜 해야하는지 설명했다.) 앱의 아키텍처를 주의깊게 만들어야 한다.


이 아키텍처에 관해 좀 더 세부적으로 들어가기 전에 먼저 한가지 키포인트를 강조하고 싶다:

우리 아키텍처를 보다 더 테스트하기 쉽게 만드는 과정은, 코드를 다른 방면으로도 더 낫게 만들어 줄 것이다. 보통 테스트 가능한 설계는 좋은 소프트웨어 설계와 직결된다.

테스트 가능한 아키텍처로 바꿀 때, 우리는 앱 컴포넌트들을 더욱더 독립적으로 만들어야 하는데 특히 외부 라이브러리로부터 분리해야한다. 이렇게 바꾸면 더 다루기 쉬워지고, 이 점은 소프트웨어 개발의 불변의 법칙이다. 또한 우리는 앱을 만들때 시작 시점에 어떻게 해야하는지, 긴 개발기간동안 어떻게 시간을 단축시킬 수 있는지에대해 주목해볼 것이다.

이제 이 일을 정확히 어떻게 하는지 보자

ViewController와 함께 다루기
모의 객체로 ViewController를 본따는 일은 쉽지 않다. ViewController를 테스트 하는것도 쉽지 않은 일이다. 따라서 무턱대고 작업해서는 안된다.

음... 뭐라고?

우리는 우리에 맞는 MVVM 아키텍처를 사용할 것이다. MVVM에 대한 글이 수도 없이 많기 때문에(아래 링크 참고) 이 부분에 대해 깊이 들어가지는 않겠지만 기본 원칙은 다음과 같다. 모든 로직을 ViewController 바깥에 두어 UIKit과 로직이 섞이지 않게 한다. 이 로직에는 모든 모델의 변화, 다른 서비스를 호출, 상태 변화, 예외 처리 등이 있다. 이 모든 로직은 ViewModel에 담겨있다. 모든 ViewController는 ViewModel로부터 데이터를 바인딩하고 유저 인터렉션에 그것을 보내며, 유저 인터렉션은 애니메이션이나 뷰를 준비하는 화면 표시의 코드이다. 아래에 MVVM에관한 글이 더 있다:


MVVM에는 각기 다른 관점들이 존재한다. 어떤 사람들은 ViewModel이 모델과 한께 초기화된 값 타입이여야한다고 하기도 하지만 우리는 조금 다르다. 우리 아키텍처에서의 ViewModel은 UI 상태를 가지고 있고, 다른 서비스를 호출하며, ViewController에 보여줄 가공되지 않은 데이터를 제공하는 역하을 한다.

이렇게 함으로서 ViewController는 굉장히 가볍고 간단한 UIKit 관련 레이어가 되므로 대부분의 앱 로직을 쉽게 테스트할 수 있다.

(Note: ViewController를 테스트 할 수도 있지만 솔찍한 내 의견은 MVVM이 MVC보다 더 나은 아키텍처라고 생각한다.)

의존성 주입(Dependency Injection)
유닛 테스트에서 각 '유닛'은 완전히 분리되어 있기 때문에 앱의 각 컴포넌트를 독립시킬 필요가 있다. 우리가 테스트 하고 있는 각 클래스에 '약한' 의존성을 제공할 방법이 필요하고, 의존성때문에 특정 테스트가 실패하는 것은 아닌지 알아야한다.(This means that we need a way to provide “sterile” dependencies to each class we are testing, to know for certain tests won’t fail because of the dependencies.)

의존성 주입이란 그냥 한 클래스에 외부적인 의존성을 제공한다는 의미의 그럴싸한 표현이다. 클래스가 자기 스스로 의존성이 생길 수는 없다. 다른 클래스를 호출하고자 한다면 그 객체를 초기화때 파라미터로 받아두어야한다. (이것을 constructor injection이라 부른다)

또 다른 한가지 양상은 모든 의존성은 프로토콜 객체로서 선언된다는 것이다. 이러한 방법으로 그 클래스가 필요로하는 오브젝트를 메소드에 담아 우리가 원하는 클래스/구조체의 객체를 초기화할때 쉽게 전달할 수 있다.


이렇게하면 우리의 통제된 의존성을 클래스에 제공할 수 있게 해주고, 나머지 앱 부분으로부터 완전히 독립적이게 만들어준다. 또한 모의 객체 의존성과 함께 깔끔하고 작은 일을 할 수 있게 해준다.

이런식으로 코드를 작성하면 꼭 테스트 뿐만 아니라 코드를 분리시킬 수 있다는 점에서 좋은 방법이다. 그 클래스는 구체적인 구현에 의존하지 않고 그냥 프로토콜로서 들고 있는 것이므로 다른 클래스의 구현을 변경하더라해도 그 클래스는 건드리지 않아도 된다.

외부 프레임워크
만약 클래스가 외부 프레임워크에 의존하고 있으면 어떻게 될까? 우리 클래스가 NSURLRequest나 CoreData에 의존하고 있으면 어떨까? 아마 꼬일 것이다.

우리는 외부 프레임워크를 감싸는 helper를 만들 것이다. 기본적으로 helper는 감싸고 있는 외부 프레임워크의 무엇이든 불러오고, 프레임워크에 함수 호출을 전하며, 외부 프레임워크에 의존없이 우리 코드베이스에서 사용할 수 있는 형태의 결과로 변형하는 역할을 한다. 한가지 일반적인 규칙은, helper는 한 프레임워크당 하나씩 불러온다.

helper 안의 로직은 가능한 작아야한다. ViewController처럼 테스트 하기 쉽지 않기 때문이다. 그리고 그 프레임워크의 같은 기능을 재정의하는것이 아니라 helper를 코딩 기준과 필요한 것에 맞추어야한다.

다른 의존성에도 같은 원리를 적용시켜 모든 클래스는 세부적인 구현을 하는게 아니라 helper에 맞춘 프로토콜의 객체를 받는다.

이렇게하면 커다란 이점이 있다. 예를들어 당신이 CoreData가 너무 복잡해서 Realm으로 바꾸려 할때, 오직 한 클래스만 고치면 된다.

그러면 자신의 의존성을 제공하지 않는 클래스는 무엇이 있을까? 바로 서비스 팩토리이다.

서비스 팩토리(Service Factory)
서비스 팩토리는 앱 전체에 의존성을 만드는 변수를 get-only로 모아놓은 집합체이다. 예를들어 ViewModel은 APIServiceProtocol 타입에 의존성을 가지는데, 그것이 ViewModel로서 컨스트럭트 될 것이다. (apiService: ServiceFactory.apiService)

클래스가 다른 것을 호출하는 장소는 한 곳에 모여있어야 한다. 이것은 마치 통제실 같은 느낌을 주며, 어떤 개발자가 보아도 한번에 이 클래스가 어떻게 돌아가는지 알 수 있어야 한다.

또한 의존성 구현을 맞바꾸는 유일한 장소이기도 하다. 예를 들어보자면 싱글톤에서 일반 객체로 바꾸는데 5초정도 걸린다. 그렇게 하기 너무 크다면 한 클래스를 두 부분으로 쪼개어 시간을 절약할 수 있다.

그리고 여러분의 클래스에 더미나 목(mock) 의존성을 제공하여 실행때 인자레 의존하므로 UI 테스트나 디버깅에 유용하다. UI 테스트시 네트워크로부터 독립적이거나 백엔드에서의 기능은 여전히 잘 동작하고 있을 것이다.

좋다. 이제 우리 앱이 실제 구현과 모의 객체를 맞바꿀 수 있다. 그러나 이것을 어떻게 할 수 있을까?

모의 객체(Mocks)
모의 객체는 여러분이 테스트하고 있는 클래스의 의존성으로서 같은 프로토콜을 따르는 클래스 혹은 구조체이다. 모의 객체는 클래스에 유닛테스트 할 수 있게 해준다.모의 객체는 모통 no-op 메소드나 유닛 테스트에 유용한 작은 기능들을 가지고 있다.

일반적인 역할은 모의 객체가 테스트할 타겟 속으로 들어간다는 점이다. 이렇게 하면 모의 객체에 구현된 코드들이 우리 앱을 더럽히지 않을 수 있다.

당신은 모의 객체 의존성만 생각해선 안된다. 완전한 테스트를 위해 종종 모의 객체 델리게이트 오브젝트를 만들어야한다. 정확한 원칙은 이것이지만 노력하고자하는 델리게이트 메소드를 검증해야 한다는 점을 잊어선 안된다.

Swift에서는 제한된 런타임 접근 때문에 아직 안드로이드용 Mockito나 Objective-C용 OCMock과 같은 좋은 모의객체 프레임워크가 나오지 않았다. 따라서 직접 모의객체를 만들어보자.

지루한 작업일 수 있으나 모의객체 프레임워크를 쓰는 것 보다 더 자유롭게 작업할 수 있을것이다. 대부분 모의객체가 보편적으로 다음과같은 설계를 가진다는 것을 알아냈다:
  1. 당신이 모의로 할 프로토콜을 구현한다.
  2. 각 메소드는 모의객체 안에 methodDidGetCalled 라는 불리언 프로퍼티를 가지고 있어야하고 requestedParameterX 프로퍼티는 옵셔널하게 가지고 있는다. 이 메소드 구현은 보통 마지막에 이 프로퍼티를 설정하는 것이다. 여러분은 테스트한 클래스에 그 메소드가 옳바른 파라미터와 함께 호출 되었는지 나중에 확인해 볼 수 있다.
  3. 만약 메소드가 리턴값이나 완료 핸들러의 뭉치를 가진다면 구조체에 methodXShouldFail이라는 불리언 프로퍼티가 있다. 이 메소드 구현은 불리언을 체크하고 성공하든 못했든 결과를 반환한다. 이렇게하면 테스트한 클래스에 실패가 생겼을 때 다루기 유용해진다.

테스트 작성하기
앱을 옳바르게 준비하고, 무엇을 테스트할지 안다면 이번에는 꽤 같단하게 끝날 것이다. 이번에는 단지 테스트의 동작을 확인한다.

테스트를 도와주는 수많은 라이브러리가 존재한다. 먼저 애플의 XCTest이다. Xcode와 연동하여 사용할 수 있고, 타이핑할 것이 좀 많기는 하나 꽤 좋은 테스트 라이브러리이다. 그리고 써드파티 라이브러리인 QuickNimble이 있다. 우리는 Nimble과 함께 XCTest를 사용한다.

다른 메소드를 호출하는 메소드를 위해, 그 메소드가 호출되었는지, 옳바른 파라미터를 전달했는지 체크한다.

당신의 클래스가 델리게이트에게 알리는지 혹은 옳바른 콜백을 호출하는지 확인한다.

이제 당신이 할 수 있는 모든 것을 테스트 할때까지 두 세번정도 이 일을 하면 된다.

이 두 포스트를 통해 왜 테스트를 해야하는지 아는데 도움을 주고, 유닛테스트를 어떻게 시작하는지에대한 가이드가 되면 좋겠다. 즐거운 테스팅하길 바란다!








WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

옮긴이 : 종종 브라우저로 원문 링크에 들어가지지 않는 현상이 생깁니다. 이럴때는 Medium 앱으로 Architecture of Node.js Internal Codebase를 검색하여 들어가시면 됩니다.


첫째로 JavaScript라는 단어에 대해...

StackOverflow 공동 창시자인 Jeff Atwood가 Coding Horror 프로그래밍 블로그에 글을 올렸다.
"JavaScript로 작성될 수 있는 모든 앱들은 결국 JavaScript로 작성될 것이다."

JavaScript는 지난 몇년간 가장 인기있는 프로그래밍 언어 중 하나로 크게 확장되었다. 실제로 2016년 SO 개발자 조사에 따르면, StackOverflow에서 가장 인기있고 가장 기술력있는 언어로 1등을 달성하고, 다른 결과들도 좋은 기록을 세웠다.

Node.js는 바이너리 데이터 조작, 파일 시스템 I/O 오퍼레이션, 데이터베이스 접근, 컴퓨터 네트워킹 등과 같은 중요한 서버사이드 기능을 담은 기초(foundation)로서 JavaScript 환경의 서버사이드이다. 이것은 Django(Python), Laravel(PHP), RoR(Ruby) 등과 같은 현존하는 시도되고 테스트된 프레임워크 사이에서 특출난 특징을 가진다. Node.js는 PayPal, Tinder, Medium, LinkedIn, Netflix와 같은 기술 리더 회사들이 사용한다는 것이다. 몇몇은 Node.js가 1.0버전이 되기도 전에 이미 사용하고 있었다.

나는 최근에 StackOverflow에서 Node.js 내부 코드베이스의 아키텍처에 관한 질문에 답변을 달았었다. 그것이 이 글을 쓰도록 영감을 불어넣어 주었다.


사실 공식적인 문서에서는 Node.js가 무엇인지 설명하기에 크게 도움이 되지 않는다.


"JavaScript 런타임은 크롬의 V8 JavaScript 엔진으로 만들어졌다. Node.js는 event-driven, non-blocking I/O 모델이고..."

이 말을 이해하고 실제 숨어있는 힘을 이해하기 위해 Node.js 요소들을 쪼개어보고, 몇몇 중요 용어들을 상세히  설명 할 것이다. 다음 어떻게 다른 조각들이 서로 소통하여 Node.js를 강력하게 만들 수 있는지 설명할 것이다.

Node.js 아키텍처 (High-Level to Low-Level)

요소들/종속물(COMPONENTS/DEPENDENCIES)
V8 : 구글에의해 오픈소스화된 높은 퍼포먼스를 자랑하는 JavaScript 엔진이며 C++로 구현되어있다. 크롬에서 사용하는 엔진과 동일한 엔진이다. V8은 당신이 JavaScript로 작성한 코드를 받아서, 기계코드로 컴파일한 뒤(이렇기에 빠르다), 실행시킨다. 어떻게 V8이 그냥 빠를 수 있을까? StackOverflow의 답변을 확인해보아라.

libuv : 비동기적 기능을 제공하는 C 라이브러리이다. 이것은 다른 중요한 기능들 사이에서 이벤트 루프, 스레드 풀, 파일 시스템 I/O, DNS 기능, 네트워크 I/O를 관리한다.

다른 C/C++ 요소들/종속물 : c-ares, crypto(OpenSSL), http-parser, zlib. 이 요소들은 서버에서 네트워킹, 압축, 암호화와같은 중요한 기능을 수행하기 위해 low-level로 소통하는 방식을 제공한다. 

앱/모듈 : 이것은 JavaScript 코드가 존재하는 모든 곳이다. 당신의 앱 코드, Node.js의 코어 모듈, npm으로 설치한 모든 모듈, 당신이 작성한 모든 모듈까지 당신은 대부분의 시간을 여기서 보낼 것이다.

바인딩 : 당신은 아마 이 시간을 통해 Node.js가 JavaScript와 C/C++로 작성되었다는 것을 알게 되었을 것이다. 여기엔 수많은 C/C++ 라이브러리가 있는데 그 이유는 간단하다: 빠르기 때문이다. 그러나 어떻게 당신이 JavaScript로 짠 코드가 C/C++로 짠 코드와 자연스럽게 소통할까? 그것은 세개의 서로 다른 언어이지 않는가? 그렇다. 다른 언어로 짜여진 코드는 보통 서로 서통할 수 없다. 바인딩 없이는 안된다. 이름에서 이야기하듯 바인딩은 한 언어를 다른 언어에 '묶어(bind)' 코드를 접합시킨다. 그러면 서로 의사소통을 할 수 있게 된다. 이 경우(Node.js) 바인딩은 단순히 C/C++로 작성된(c-ares, zlib, OpenSSL, http-parser 등) Node.js 내부 코어 라이브러리들을 JavaScript에 연결한다. 바인딩을 작성하는데 첫번째 이유는 재사용이다: 만약 필요한 기능들이 이미 구현되있다면, 그것을 가져다 사용하면 된다. 단지 언어가 다르다는 이유로 그것을 다시 짤 필요는 없다. 왜 그냥 연결시켜 사용하지 않겠는가? 두번째 이유는 퍼포먼스이다: C/C++ 같은 시스템 프로그래밍 언어들은 일반적으로 고수준언어(Python, JavaScript, Ruby등) 보다 빠르다. 그러므로 CPU-집약 오퍼레이션 코드는 C/C++로 작성하는 것이 현명할 것이다.

C/C++ Addon : 바인딩은 단지 zlib, OpenSSL, c-ares, http-parser와 같은 Node.js 내부 라이브러리를 접합하는 기능만 한다. 만일 당신이 써드파티의 C/C++ 라이브러리(외부 C/C++ 라이브러리)를 넣고 싶으면, 스스로 라이브러리를 접합시켜야한다. 당신의 코드를 접합해주는 코드가 addon이라 불리는 것이다. 바인딩과 addon은 당신의 JavaScript 코드와 Node.js의 C/C++ 코드를 연결하는 다리라고 생각하면 편할 것이다.

용어들(TERMINOLOGIES)
I/O : Input/Output의 약자이다. 이것은 기본적으로 시스템 I/O의 하위 시스템에의해 주로 다뤄지는 모든 컴퓨터 오퍼레이션을 나타낸다. I/O 바운드( https://en.wikipedia.org/wiki/I/O_bound)오퍼레이션은 디스크/드라이브와의 상호작용을 포함한다. 예를들어 데이터베이스 접근과 파일 시스템 오퍼레이션을 포함한다. 연관된 다른 개념에는 CPU 바운드, 메모리 바운드 등이 있다. 오퍼레이션이 I/O 바운드에 속하는지 CPU 바운드에 속하는지 아니면 다른데 속하는지 구별하는 좋은 방법은, 특정 오퍼레이션이 더 많은 퍼포먼스를 내고 리소스를 많이 잡아먹는지 체크하면 된다. 예를들어 한 오퍼레이션이 눈에 띄게 빨라지고 CPU 파워가 증가했다면 그것은 CPU 바운드 이다. 

Non-blocking/비동기적 : 보통 리퀘스트가 들어오면 앱은 그 리퀘스트를 다루고, 그 리퀘스트의 작업이 끝날때까지 다른 모든 오퍼레이션을 멈춘다. 다음은 실제 일어날 수 있는 문제이다: 만일 한꺼번에 수많은 리퀘스트가 들어오면, 각 리퀘스트는 이전 리퀘스트의 작업이 끝날때까지 기다려야한다. 다른 말로는, 이전 오퍼레이션이 다음 것을 블럭(block)시킬 것이다. 최악의 상황은,  이전 리퀘스트가 긴 응답시간(eg. 먼저 1000개의 계산 후, DB로부터 3GB 데이터를 읽는 경우) 이라면, 다른 리퀘스트들은 긴 시간동안 멈춤/블럭이 되 있을 것이다. 이 문제를 해결하기 위한 방법에는 각 리퀘스트에 대해 멀티 프로세싱 혹은/그리고 멀티 스레딩이 있을 수 있다. Node.js는 이것을 좀 다르게 다룬다. 모든 새 요청에 대해 새 스레드를 만드는 것 대신, 요청들은 한 메인스레드에서 다뤄지며, 그것이하는 일의 대부분이다.

리퀘스트들을 다루기 : 리퀘스트에 포함된 모든 오퍼레이션(eg. 파일 시스템 접근, 데이터베이스 읽기/쓰기)은 백그라운드에서 (위에서 언급한)libuv에의해 관리되는 일꾼 스레드로 보내진다. 즉, 리퀘스트 안에 있는 I/O 오퍼레이션들은 메이스레드가 아닌 곳에서 비동기적으로 다뤄진다. 이 방법으로 무거운 것을 다른곳으로 보냄으로써, 메인스레드가 블럭되지 않게 해준다. 당신의 앱 코드는 한 순간에 한가지 일만 하고 있고, 그것은 메인스레드 안에 있을 것이다.  libuv의 스레드풀에 있는 모든 일꾼 스레드는 당신이 접근할 수 없게 만들어 놓았다. 여러분은 그들에게 직접 접근할 수 없으므로 그것에대해 걱정할 필요도 없다. Node.js가 당신을 위해 관리해주기 때문이다. 이러한 아키텍처는 I/O오퍼레이션을 특히 효율적이게 만들어준다. 그러나 이것에 단점이 없는 것은 아니다. 오퍼레이션에는 I/O 바운드 하나만 있는게 아니라 CPU바운드, 메모리 바운드 등이 더 있다. 조금만 더 이야기해보자면 Node.js는 I/O 작업을 위한 비동기 기능만을 제공한다. CPU 집약 오퍼레이션을 수행하는 방법이 있지만, 이 글의 논지가 아니니 넘어가도록 하자.

이벤트기반(Event-Driven) : 일반적으로 현대의 대부분 시스템은 메인 앱이 꺼져야 들어오는 리퀘스트에의해 프로세스가 초기화된다. 그러나 그들이 다르게 흘러간다면? 전형적인 구현은 리퀘스트를 다음 순서처럼 다룬다: 리퀘스트를 위한 스레드를 하나 만들고, 다른 오퍼레이션이 끝난뒤 그 오퍼레이션을 수행하며, 오퍼레이션이 느려지면 그 스레드에 있는 모든 오퍼레이션이 멈추거나 느려진다. 모든 오퍼레이션이 완료되면 리스폰을 돌려준다. 그러나 Node.js에서는 메인 앱에의한 것이든 리퀘스트에의한 것이든 모든 오퍼레이션이 트리거(trigger)를 기다리는 Node.js 이벤트로 등록된다.

런타임(시스템) : Node.js 런타임은 고수준, 저수준 둘 다 Node.js 앱 실행을 같이 도와주는 전체 코드베이스(위에서 언급한 요소들)이다.

모든것을 섞어보자. (PUTTING EVERYTHING TOGETHER)
이제 Node.js 요소의 고수준 관점에서 보자. 우리는 그것의 아키텍처를 더 잘 알기위해 그것의 워크플로우(workflow)를 알아보고, 어떻게 다른 요소들이 서로 소통하는지도 알아볼 것이다.

Node.js 앱이 실행되면, V8엔진이 당신의 앱 코드를 실행시킨다. 당신 앱의 객체들은 옵저버(이벤트로 등록할 수 있는 기능)의 목록에 넣어둔다. 이 옵저버는 각 이벤트들이 발생할 때 알림을 받는다.

한 이벤트가 발생되면, 그것의 콜백 함수가 이벤트 큐(event-queue)에 들어간다. 큐에 이벤트가 한개라도 남아있으면 이벤트 루프(event loop)는 큐에서 이벤트를 빼어 콜스택(call stack)에 집어넣는다. 이전 이벤트가 처리가 되면 이벤트 루프는 다음 이벤트를 콜스택에 넣는다는 점만 유의하면 된다.

콜스텍에서 우연히 I/O 오퍼레이션을 만나면, 처리하기위해 libuv에게 넘어간다. 기본적으로, libuv는 4개의 일꾼 스레드를 유지하고 있다. 물론 더 많은 스레드를 만들 수도 있다. 만약 요청이 파일 시스템 I/O나 DNS 관련이면, 처리를 위해 스레드 풀에 할당될 것이다; 다른경우, 네트워킹과 같은 다른 리퀘스트들에서는 특정 플랫폼의 매커니즘이 그 리퀘스트들을 알아서 배치할 것이다.

스레드 풀을 사용하게 만드는 I/O 오퍼레이션들(파일 I/O, DNS 등)은 데이터베이스 트렌젝션이나 파일 시스템 접근과 같은 오퍼레이션을 수행하기 위해 Node.js의 저수준 라이브러리로 일꾼 스레드가 작용할 것이다. 처리가 끝나고 libuv는 이벤트가 다시 메인스레드에서 처리될 수 있게 이벤트 큐에 다시 집어넣는다. 이 동안에는 libuv가 비동기의 I/O 오퍼레이션을 다루고 있고, 메인스레드는 마냥 앉아서 처리 결과를 기다리는 것이 아니라 자신의 할 일을 하고 있는다. libuv에의해 돌려받는 이벤트는 이벤트 루프에의해 콜스택으로 돌아왔을때, 다시 메인스레드에서 처리될 수 있다. 이것이 Node.js에서 이벤트 생명주기이다.

mbg는 기가막히게 Node.js와 레스토랑을 비유해 놓았다. 나는 Node.js 사이클을 더 쉽게 설명하기 위해 그의 이야기를 빌려 설명하겠다.

Node.js 앱을 스타벅스 카페라 생각해보자. 높은 효율과 숙련이 잘 된 웨이터(한 명이고 메인스레드라 할 수 있다)가 주문을 받는다. 만약 같은 시간대에 많은 고객이 들이닥치면 웨이터의 서비스를 받기위해 줄을 서게 된다(이벤트 큐에 넣는다). 한 고객이 웨이터로부터 서비스를 받으면 웨이터는 메니저(libuv)에게 주문을 넘겨준다. 각 주문을 어떤 바리스타(일꾼 스레드 혹은 플랫폼-특정 매커니즘)에게 주문을 할당할지 정한다. 바리스타는 다른 다른 재료들과 기계를 써서(저수준 C/C++ 요소들) 고객이 요청한 것들의 음료를 만든다. 보통 4명의 바리스타가 특정 라떼(파일 I/O, DNS 등)를 돌아가면서 만든다. 그러나 주문이 피크치에 달할 때는 더 많은 바리스타를 일시적으로 고용할 수 있다(개업한 날의 이야기이지, 점심시간대의 이야기는 아니다). 한 번 웨이터가 메니저에게 주문을 넘기면, 웨이터는 다른 고객을 서브하기 위해 커피 만드는 것을 기다리지 않는다. 대신에 다음 고객을 부른다(이벤트 루프에의해 다음 이벤트를 큐에서 꺼네어 콜스택에 넣는다). 이벤트가 콜스택 안에 있는 것은, 고객이 카운터에서 서브를 받으며 있는 것과 비슷하다. 커피가 다 만들어지면, 커피는 고객 줄의 끝으로 보내진다. 커피가 카운터로 나오면 웨이터는 고객의 이름을 부를 것이고, 고객은 주문한 커피를 받게된다. (비유가 다소 현실세계에서는 이상할 수 있지만, 프로그램 관점에서 프로세스를 고려할 때는 딱 들어 맞을 것이다)


고수준 관점의 전반적인 Node.js 내부 코드 베이스와 그것의 일반적인 이벤트 생명주기에대해 설명이 끝났다. 그러나 이 총람은 굉장히 일반적인 상황의 이야기이고 많은 이슈와 세부적인 요소에는 들어맞지 않을 수 있다는 점을 유의하길 바란다. 예를들어 CPU 바운드 핸들링이나 Node.js 디자인 패턴 등등이 있다. 더 많은 다른 글들의 토픽들이 그 부분을 채워줄 것이다. 



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

우리는 건축물을 만들고, 그 후에 건축물이 우리를 만드는 것을 아키텍처의 범주로 잘 알려져있다. 결국 모든 프로그래머가 배움으로서 이것은 그냥 소프트웨어 구축을 보다 더 좋게 하는데 적용된다.

우리가 짠 코드들이 간결한 아이덴티티가 부여되고 명확한 목적을 가지며, 각 코드가 논리적인 측면에서 서로 맞아 떨어지게 설계하는 것이 중요하다. 이것이 바로 소프트웨어 아키텍처가 의미하는 바이다. 좋은 아키텍처는 제품의 성공이 아니라 제품의 유지보수 용이성과 사람들이 유지보수를 할 때 제정신을 차리도록 멘탈을 보호해주는 것이다!

이 글에선 VIPER라 불리는 아키텍처를 iOS에 적용시켜 소개해보려고 한다. VIPER는 많은 큰 프로젝트에 사용되어 왔으나, 이 글의 목적상 간단한 to-do 리스트 앱을 통하여 VIPER를 보여줄 예정이다. 여러분은 여기 Github에 예제 프로젝트와 함께 따라오길 바란다.


VIPER란?
iOS 앱을 만들면서 테스트는 주요 작업이 아닐 때가 많다. 우리는 Mutual Mobile에 있을때 테스팅 절차를 들여라는 요사항이 들어왔는데, iOS 앱을 위해 테스트를 준비하는게 쉽지 않다는 것을 깨달았다. 우리는 소프트웨어를 테스트 할 수 있는 방법을 개선하기로 결정하였고, 앱의 아키텍처를 더 좋은 방법으로 만들 필요가 있었다. 그렇게해서 나온 해답이 바로 VIPER라 불리는 것이다.

VIPER는 iOS 앱들에게 클린 아키텍처의 어플리케이션이다. VIPER라는 단어는 VIew, Interactor, Presenter, Entity, Routing의 약자로 구성된다. 클린 아키텍처는 논리적인 구조를 앱의 책임별 각 층으로 나눠준다. 이것은 의존성을 고립시키고 각 층들 사이 경계에서의 상호작용을 테스트하기 쉽게 해준다.


대부분의 iOS 앱은 MVC(Model-View-Controller) 아키텍처를 사용한다. 앱에서 MVC 아키텍처를 사용하는 것은 당신으로 하여금 모든 클래스들이 model이자 view이자 controller라 생각들게 만들것이다. 그러므로 대부분의 앱 로직은 model이나 view에 들어가있지 않고 controller에 몰려있다. view controller가 비대해지면서 결국 MassiveViewControlle 문제가 되버리고 만다. 이 비대한 view controller의 중량을 줄여 코드의 질을 높히는 과제는 iOS 개발자만이 직면한 문제가 아니지만, 이러한 시도는 좋은 시작 시점에 와있다.

VIPER의 각 층은 명확한 위치에 앱 로직이 들어가고, navigation 관련 코드가 되도록 함으로서 이러한 과제를 해결하는데 도움을 준다. VIPER를 적용함으로서, 우리 to-do 리스트 예제에서의 view controller가 머신을 조종하는 view에 의존한다는 것을 당신은 인지하게 될 것이다. 또한 view controller에 있는 코드와 모든 다른 클래스들이 이해하기 쉽고, 테스트하기 쉬우며, 그리하여 유지보수하기까지 쉽다는 것을 깨달을 것이다.

유스케이스에 기반한 앱 설계
앱은 종종 유스케이스의 한 집합으로 구현된다. 유스케이스는 기준이나 동작을 수용하고 앱이 어떤 일을 의도하는지 설명하는 것으로 알려져 있다. 리스트는 날짜, 타입, 이름으로 정렬이 가능해야 한다고 정의하는 것이 유스케이스이다. 한 유스케이스는 앱에서 비즈니스 로직을 위한 기능인 한 층(layer)이다. 유스케이스들은 그들의 유저 인터페이스 구현으로부터 독립적이여야한다. 또한 그것들은 작아야하고, 잘 정의되있어야한다. 복잡한 앱을 더 작은 유스케이스로 어떻게 쪼갤지 고민하는 것이 과제이고, 이 부분은 숙달이 필요하다. 그러나 당신이 해결할 각 문제나, 당싱이 작성한 각 클래스들의 범위를 제한하는 방법을 사용하면 여러므로 도움이 될 것이다.

VIPER로 앱을 만드는 것은 각 유스케이스를 수행하는 요소의 집합을 구현하는 것을 포함한다. 앱 로직은 유스케이스를 구현하는데 있어서 중요한 부분이지만, 앱 로직만 구현해야하는 것은 아니다. 또한 유스케이스는 유저 인터페이스의 영향을 받을 수 있다. 추가적으로 네트워크나 데이터 퍼시스트와 같은 다른 중요한 요소들과 유스케이스가 어떻게 연결될지 고려하는 것도 중요하다. 요소들은 유스케이스에 플러그인처럼 동작하고, VIPER는 각 요소들의 역할이 무엇인지 그들이 어떻게 서로 상호소통할 수 있는지 표현하는 방법이다.

우리 to-do 앱의 유스케이스나 요구사항 중 하나는 유저 선택을 기반한 여러 방법으로 그룹을 짓는 것이다. 유스케이스에 데이터를 조직화하여, 로직을 쪼갬으로서, 우리는 유저 인터페이스 코드가 깔끔하게 유지되고, 우리가 예상한대로 동작하는지 확인하기 위한 테스트에서 쉽게 유스케이스를 적용할 수 있다.

VIPER의 주요 부분

아래는 VIPER의 주요 부분이다.
  • View : Presenter에 의해 요청 받은 것을 화면에 표시하고, 유조의 입력을 Presenter에게 넘겨준다.
  • Interactor : 유스케이스에 의해 부여된 비지니스 로직을 담고있다.
  • Presenter : (Interactor로부터 받은)content를 화면에 보여주기 위해 준비하거나, (Interactor 로부터 요청한 데이터인) 유저 입력에 반응하여 처리하는 View 로직을 담고 있다.
  • Entity : Interactor에 의해 사용되는 기본 모델의 객체를 담는다.
  • Routing : 명령에의해 어떤 화면으로 갈지 알고있는 navigation 로직을 담고 있다.

이러한 분리는 단일책임원칙(Single Responsibility Principle)에 들어맞게 된다. Interactor는 비지니스 해석의 임무를 가지고 Presenter는 인터렉션 디자이너를 나타낸다. View는 시각적 디자이너 임무를 맡는다. 

아래 다이어그램은 서로 다른 컴포넌트들이 어떤 식으로 연결되는지 보여준다.

VIPER 컴포넌트들은 어떤 순서로도 구현될 수 있지만, 우리는 추천하는 구현 순서대로 소개하고자 한다. 이 순서는 전반적으로 앱을 만드는 과정과 일치한다. 제품에서 이것을 하기 위해 무엇이 필요한지 토론하는 것부터 시작하여, 유저가 어떻게 상호 소통하는지 알려줄 것이다.

Interactor
Interactor는 앱에서 하나의 유스케이스를 의미한다. 이 컴포넌트는 특정 작업을 수행하기 위해 모델 오브젝트(Entity)를 다루는 비즈니스 로직을 가진다.  Interactor에서 완료된 일은 어떤 UI에도 독립적이여야한다. 한 Interactor로 iOS앱이나 OSX앱에서 동작될 수 있어야한다.

왜냐하면 Interactor는 로직을 1순위로 담은 PONSO(Plain Old NSObject)이기 때문에, TDD를 이용하여 개발하기 수훨해진다.

샘플 앱에서 첫째 유스케이스는 다가오는 to-do 아이템들을 보여주는 것이다.(예를들어 다음주까지 끝내야하는 어떤것..) 이 유스케이스의 비즈니스 로직은 오늘부터 다음 주말까지 사이에 있는 to-do 아이템을 찾고, 그 기간을 배정하는 것이다.(오늘, 내일, 이번주내, 다음주..)

아래 VTDListInteractor에 해당하는 메소드이다.


Entity
Entity들은 Interactor에 의해 사용되는 모델 오브젝트들이다. Entity들은 오직 Interactor에 의해서만 관리된다. Interactor는 절때로 Presentation 층에 Entity를 넘겨주지 않는다. Entity들 또한 PONSO이다. 만약 CoreData를 사용하면 managed object를 데이터 층 뒤에 남겨두고 싶어 할 것이다. Interactor는 NSManagedObjects와 함께 작업하지 않아야 한다.

여기 이것은 우리의 to-do 아이템을 위한 Entity이다.

Entity가 단순한 데이터 구조체처럼 생겼다고 너무 놀라지 마라. 모든 앱 의존 로직은 Interactor 안에 구현되 있을 것이다.

Presenter
Presenter도 PONSO인데, 이것은 UI에 넘겨주는 로직으로 구성되있다. Presenter는 유저 인터페이스를 언제 주는지 알고 있다. 이것은 유저 인터렉션으로부터 입력을 받고, UI를 갱신할 수 있으며, Interactor에게 요청을 보낼 수도 있다.

만약 유저가 새 to-do 아이템을 추가하기 위해 다른 UI를 present할지 wireframe에게 물어본다.


Presenter는 또한 Interactor에게 결과물을 받고, View에 표시되기 적합한 결과물로 한번 더 변환한다.

아래는 Interactor로부터 최근 아이템을 받은 메소드이다. 이것은 데이터를 처리하고 유저에게 어떻게 보여줄지 결정한다.


Entity는 절때 Interactor에서 Presenter로 넘어가지 않는다. 대신 간단한 데이터 구조체는 Interactor에서 Presenter로 넘어간다. 이러한 것은 Presenter에서 '실제 작업'을 행하는 일을 방지한다. 따라서 Presenter는 View에 띄우기 위한 데이터를 준비하는 것만이 가능하다.

View
View는 수동적인 녀석이다. 이것은 화면에 뿌릴 컨텐츠를 Presenter로부터 받을때까지 기다린다. 절때 Presenter에게 데이터를 달라고 직접 말하지 않는다. (로그인 화면의 LoginView와 같은) View에 정의된 메소드들은 높은 수준의 추상화를 통해 Presenter와 소통할 수 있게 해주며, 어떻게 그 컨텐츠가 화면에 표시되는지에대한 것은 아니다. Presenter는 UILabel, UIButton등등 이런 것들이 존재하는지 조차 모른다. Presenter는 오직 컨텐츠를 들고 있다는 것과, 언제 화면에 표시하는지만 안다. View는 화면에 어떻게 표시할지만 결정한다.

View는 Objective-C 프로토콜(Protocol)로 정의된 추상 클래스이다. UIViewController나 그것의 자식 클래스가 View프로토콜을 구현할 것이다. 우리 예제의 'add' 화면은 아래 인터페이스를 가진다.


View와 View Controller들은 유저 인터렉션과 유저 입력 또한 다룬다. 이것이 왜 View Controller가 커져버리는지의 이유이고, 그들에겐 입력의 몇 동작을 다루는데 가장 쉬운 위치이다. 유저가 어떤 동작을 취할때, View Controller 의존을 유지하기 위해 그것에게 필요한 부분의 정보를 주도록 해야한다. View Controller는 이 동작에 의해 어떠한 결정도 내려선 안되며, 할 수 있는 어떤것 사이에서 이 이벤트를 넘겨줘야 한다.

우리의 예제에서 AddViewController는 아래 interface를 따르는 이벤트 핸들러 요소를 가진다.


유저가 취소버튼을 누르면 View Controller는 유저가 add 동작을 취소했다고 이벤트 핸들러에게 말한다. 여기서는 이벤트 핸들러가 AddViewController를 dismissing 시키고 리스트 뷰를 갱신해라고 알린다.

ReactiveCocoa는 View와 Presenter를 연결해주는 역할을 한다. 이 예제에서 ViewController는 버튼 액션을 나타내는 신호를 반환하기 위해 메소드를 제공할 수도 있다. 이것은 역할을 나눌 필요 없이 Presenter가 쉽게 이 시그널을 응답할 수 있게 해준다.

Routing
한 화면에서 어디로 갈지의 기능은 인터렉션 설계자에의해 Wireframe에 정의되어있다. VIPER에서 Routing의 기능은 Presenter와 Wireframe 이 두 객체가 공유되게 하는 것이다. Wireframe 객체는 UIWindow, UINavigationController, UIViewController 등이 될 수 있다. 이것의 기능은 View나 ViewController를 생성하고 화면에 설치하는 일이다.

그러므로 Presenter는 유저 입력에 반응하는 로직을 가지고, Presenter는 언제 다른 화면으로 어디로 가는지 알고 있어야 한다. 한편 Wireframe은 어떻게 화면간 이동을 하는지 알고 있어야한다. Presenter는 Wireframe을 사용하여 화면 이동을 실행한다. 둘은 어느 화면에서 다른 화면으로 가게 해준다.

Wireframe은 또한 navigation transition animation을 다루는 명확한 위치에 있다. add wireframe인 이 예제를 보자.


Add View Controller를 띄우기 위해서 커스텀 View Controller Transition을 사용한다. 그러므로 Wireframe은 Transition 실행의 역할을 담당한다. 이것은 Add View Controller를 위한 Transition 델리게이트가 되고 이것은 적절한 Transition 애니메이션을 리턴할 수 있다.

VIPER에 맞춰진 앱 컴포넌트
iOS  앱 아키텍처는 앱을 만드는 주 도구가 UIKit과 CocoaTouch라는 사실을 고려할 필요가 있다. 이 아키텍처는 앱의 모든 요소들이 평화롭게 공존해야한다. 또한  프레임워크의 어떤 부분이 사용되는지, 그들이 어디에 있어야하는지 가이드라인을 제공해야 한다.

iOS 앱의 작업장은 UIViewController이다. 이것은 쉽게말해, MVC를 대체하는 그 대상은 View Controller가 무거워지는 것을 최대한 피해야한다. 그러나 View Controller는 플랫폼의 중심에 있다: 이들은 화면회전을 다루고,  유저 입력에 반응하며, navigation controller와 같은 시스템 요소까지 담고 있으며 iOS7부터는 화면 사이에 커스텀 Transition 까지 가능한데, 이 모든것이 View Controller에 들어가게되면 View Controller가 무거워 질 수 밖에 없다.

VIPER에서는 View Controller는 오직 View를 컨트롤하는 역할만을 한다. 우리 to-do 리스트 앱은 2개의 View Controller를 가진다. 하나는 리스트 화면이고 나머지 하나는 추가(add)화면이다. Add View Controller는 극단적으로 기본적인 것만 구현되있다. 왜냐하면 오직 View를 컨트롤 하는게 전부이기 때문이다.


앱들은 보통 네트워크를 탈 때 다소 부자연스러운 면이 있다. 그러나 어디서 네트어크를 타야하고, 네트워크를 타기위해 누가 초기화를 해줘야할까? 이 일은 보통 Interactor에서 일어나되, Interactor가 직접 네트워크를 타는 코드를 가지고 있지는 않는다. 이것은 Network Manager나 API Client같은 의존(dependency)에 물어볼 것이다. Interactor는 유스케이스 실행에 필요한 다양한 정보를 다양한 소스로부터 제공받아 모으는 일을 할 것이다. 그러면 Interactor로부터 Presenter에게 데이터를 넘겨주는데, 화면에 표시하기 적당한 형식에 맞춘다.

Data Store는 Interactor에게 Entity들을 주는 역할을 한다. Interactor는 그것의 비즈니스 로직을 감당하고 있으므로 Data Store로부터 Entity를 검색하고, Entity를 다루고, 그리고 Data Store에 Entity들을 집어넣어 저장하는 기능들이 필요할 것이다. Data Store는 Entity들의 퍼시스트를 관리한다. Entity들은 Data Store에 대해 전혀 모르므로 Entity들은 어떻게 그들 스스로 퍼시스트 되는지도 모른다.

Interactor도 마찬가지로 어떻게 Entity들을 퍼시스트 하는지 몰라야한다. 때때로 Interactor는 Data Store와 함께 그것의 인터렉션을 위해 Data Manager에서 불려진 오브젝트 타입을 사용하고 싶어한다. Data Manager는 패치(fetch) 요청 생성, 쿼리 만들기 등과 같은 Store에 특화된 오퍼레이션 타입을 다룬다. 이러한 것은 Interactor가 앱로직에 더 집중할 수 있게 해주며, 어떻게 Entity들을 모으고 퍼시스트 하는지 몰라도 된다. Data Manager를 사용하는 시점은 CoreData를 사용하는 시점과 같다. 아래 그 설명이 있다.

이것은 앱 Data Manager의 인터페이스 부분이다:


Interactor를 개발하기 위해 TDD를 사용할때, Production Data Store를 테스트 double/mock(옮긴이: 테스트를 위해 만든 가상의 더미코드, 껍데기코드)으로 전환할 수 있다. 원격의 서버를 사용하지 않고, 디스크 접근을 하지 않으면서 테스트 하는 것은 더 빠르고 더 많은 반복 테스트를 해볼 수 있다.

명확한 경계로 별개 층을 만들어 Data Store를 사용하는 이유는 당신으로 하여금 특정 퍼시스트 기술을 고르는데 지연할 수 있게 해준다(옮긴이: ?). 만약 당신의 Data Store가 하나의 클래스로 이루어져 있다면, 기본 퍼시스트 전략부터 앱을 만들어야하고, 그 다음 SQLite나 CoreData를 업그레이드하여 감각대로 만들면 당신의 앱 코드베이스에서 모든것이 바뀌어야 하게 될지도 모른다.

iOS 프로젝트에서 CoreData를 사용하는 것은 종종 아키텍처를 짜는 것 보다 시간이 더 걸릴 수도 있다. 그러나 VIPER와 함께 CoreData를 사용하면 여태껏 겪어보지 못한 최고의 CoreData를 경험 해볼 수 있을 것이다. CoreData는 유지보수의 빠른 접근과 낮은 메모리 접유율로 데이터를 퍼시스트 하는 좋은 툴이다. 그러나 이것은 NSManagedObjectContext가 앱 구현파일의 전체에 걸쳐 휘감아버리는 습관이 있다. VIPER는 Data Store 층에서 이것을 해결해준다.

여기 to-do 예제에서는 CoreData가 사용된다는 것을 아는 부분은 딱 두 부분이다. Core Data 스택을 설정하는 Data Store 자체와 Data Manager 이 둘이다. Data Manager는 패치 요청을 실행하는데, 표준 PONSO 모델 오브젝트로 Data Store에의해 반환된 NSManagedObject들을 변환하고, 이것을 비즈니스 로직 층에 넘겨준다. 이런 방법은 앱의 코어가 Core Data에 절때 의존하지 않게 된다. 추가적으로 불완전한 스레드의 NSManagedObject 동작을 걱정할 필요가  없다. 이것은 CoreData Store 요청을 만들어 낼 때의 Data Manager 내부 모습이다.


대부분의 CoreData 작업은 UI 스토리보드에서 일어난다. 스토리보드는 수많은 유용한 특징들이 있고, 전적으로 실수를 줄여주는 장점이 있다. 그러나 스토리보드의 기능만을 사용하여 작업하기엔 VIPER의 모든 목적을 달성하기 어렵다.

우리가 만들고자 하는 절충안은 segue( https://developer.apple.com/library/ios/recipes/xcode_help-IB_storyboard/Chapters/StoryboardSegue.html )(옮긴이 : 스토리보드에서 드레그 드롭으로 화면간의 이동을 연결해주는 방식)들을 사용하지 않는 방법이다. 우리는 종종 segue들을 사용하여 화면을 만드는 경우가 있을 수 있지만, segue를 사용하면 화면들 사이에–UI와 앱 로직 사이도 마찬가지로–분리를 유지하기가 쉽지 않을 것이다. prepareForSegue 메소드를 필연적으로 구현해야 할 때 최고의 방법은 segue를 쓰지 않으려 노력해야한다.

다른 경우에 스토리보드들은 유저 인터페이스를 위한 레이아웃 구현이 잘 되있다(특히 Auto Layout 같은). 우리는 스토리보드를 이용한 to-do 리스트 화면 구현과 우리 고유 navigation 동작과 같은 것을 코드를 이용한 구현으로 둘 다 채택하였다.


모듈을 만들기 위해 VIPER를 사용하기
VIPER를 사용하는 종종, 한 화면이나 화면들의 집합이 하나의 모듈로 나뉘는 경향을 보게 될 것이다. 한 모듈은 여러 다른 방법으로 구현될 수 있으나, 일반적으로 이러한 방법(화면 단위로 나누는)이 적당할 때가 많다. 팟케스팅 앱에서, 한 모듈은 오디오 플레이어 혹은 구독 브라우저가 될 수 있다. 우리 to-do 리스트 앱에서, list와 add 화면은 각 다른 모듈로 만들어 질 수 있는 것이다.

모듈들을 만듦으로써 앱 설계를 하는것은 몇가지 이점이 있다. 그 중 한가지는 모듈들이 굉장히 깔끔하고 잘 정의된 인터페이스를 가질 수 있으며, 게다가 다른 모듈에게 독립적이다. 이러한 이유로 기능을 넣고 빼기가 쉬우며 여러분의 인터페이스를 유저에게 보여주는 방법도 쉽게 바꿀 수 있다.

우리는 to-do 리스트 예제에서 모듈 사이에 분리를 굉장히 명황하게 하고자 원했고, 우리는 add 모듈을 위해 두개의 protocol을 정의했다. 첫번째는 모듈 인터페이스로서, 모듈이 무엇을 할 수 있는지 정의했다. 두번째는 모듈 델리게이트로서, 모듈이 무엇을 했는지 보여준다. 아래는 예제이다:


그러므로 한 모듈은 유저에게 어떤 의미를 지니도록 보여질(present) 수 있고, 모듈의 Presenter는 보통 모듈 인터페이스를 구현한다. 다른 모듈에서 이것을 보여주고 싶을 때, 그것의 Presenter는 모듈 델리게이트 프로토콜을 구현해야하고, 모듈이 보여질 때 모듈이 무엇을 했는지 알고 있을 것이다.

한 모듈은 여러 화면에서 사용될 수 있는 Entity, Interactor, Managerdls 인 일반적인 앱 로직 층을 가지고 있어야 한다. 물론 이것은 화면들 사이에 인터렉션에 의존하며 비슷하게 생겼다. 쉽게 to-do 리스트를 예로들어 한 모듈은 오직 한 화면으로 표시될 수 있다. 이 경우에는 앱 로직 층이 그것의 각 모듈의 동작에 굉장히 특화될 수 있다.

모듈들은 코드를 조직화하기에 꽤 좋은 방법이다. 만약 당신이 뭔가 바꾸고 싶을때 XCode에서 모든 코드를 그것의 그룹이나 폴더에 집어 넣어 둔다면 다시 찾기 쉬워질 것이다. 그리고 클래스가 어디 있는지 당신이 예상하는 곳에 있게될 것이다.

VIPER에서 모듈화를 시킬때 또 한가지 장점은 여러 형태로 쉽게 확장할 수 있다는 것이다. Interactor층에 고립된 모든 유스케이스의 앱 로직을 가지는 것은 앱 층을 재사용함으로서 테블릿, 폰, 맥의 새 유저 인터페이스를 만드는데 집중할 수 있게 해준다.

한걸음 더 나아가서 아이패드를 위한 유저 인터페이스는 아이폰의 View, View Controller, Presenter를 재사용 할 수 있을 것이다. 이 경우 아이패드 화면은 아이폰에서 쓰인 Presenter와 Wireframe을 'super' 함으로써 표현될 수 있다. 다중 플랫폼을 지원하면서 개발하고 유지보수하는 것은 꽤 도전일 수 있으나, 모델과 앱 층을 재사용하는 좋은 아키텍처라면 이것을 쉽게 도와줄 것이다.

VIPER에서 테스트
이 VIPER는 일을 쪼개도록 도와주는데, 이것이 TDD를 쉽게 적용시키게 만들어준다. Interactor는 어떤 UI에도 독립적인 순수히 로직만을 담고 있는데, 이것은 테스트하기 쉽게 해주는 역할을 한다. Presenter는 화면에 보여주기 위한 데이터를 준비하는 로직을 가지며, 이것은 어떤 UI 위젯에도 독립적이다. 이 로직을 개발하는 것 또한 테스트하는데 쉽게 도와준다.

우리의 필요한 메소드는 Interactor에서  시작될 것이다. UI에서 모든 것들은 유스케이스의 필요한 것을 그들에게 나를 것이다. Interactor API를 테스트하기 위해 TDD를 사용함으로서, 당신은 UI와 유스케이스간의 관계를 더 잘 이해할 수 있게 될 것이다.

이 예제에서, 다가오는 to-do 아이템 리스트를 위한 Interactor의 역할에 대해 살펴볼 것이다. 다가오는 아이템을 찾는 방법은, 우선 다음 주말까지 모든 아이템을 검색하고, 오늘, 내일, 이후 이번주, 다음주 별로 시간에 따라 묶는 것이다. 

우리가 작성한 첫번째 테스트는 Interactor가 다음 주말까지의 모든 to-do 아이템을 찾아내는지 확인하는 것이다.


Interactor가 적절한 to-do 아이템을 위해 요청한다는것을 알고, 그것이 정확한 날짜와 연관하여 올바르게 to-do 아이템들을 배치하는지 확인하기 위한 여러 테스트를 만들면 된다.


이제 Interactor의 API가 어떻게 생겼는지 알고 있으므로 Presenter를 만들어 볼 수 있다. Presenter가 Interactor로부터 다가오는 to-do 아이템들을 받게되면 이것이 적절한 양식의 데이터인지 UI에 띄울 수 있는지 테스트하고 싶을 것이다.


또한 유저가 새 to-do 아이템을 추가하기 원할때 앱이 적절한 동작을 취하는지도 테스트하고 싶을 것이다.


이제 View를 개발할 수 있다. 만약 다가오는 to-do 아이템이 하나도 없다면, 특정 메시지를 띄울 필요가 있을 것이다.


화면에 띄울 다가오는 to-do 아이템이 있다면, 테이블이 보여지길 원할 것이다.


먼저 Interactor를 만들면 TDD에 맞추기 편해진다. Presenter에 따라 Interactor부터 먼저 개발하면 이 층 주변에 테스트들을 만들어 낼 수 있고, 이 유스케이스를 구현하기 위한 기반을 설치할 수 있다. 그것들을 테스트하기 위해 UI와 연결될 필요가 없으므로 이 클래스들을 빠르게 재사용할 수 있을 것이다. 그러고 View를 개발할 때 테스트된 로직과 디스플레이될 층을 서로 연결만 시켜주면 되겠다. 모든 테스트가 잘 동작했다면, View 개발을 다 끝냈을 땐 한번에 앱의 모든 기능이 잘 동작함을 확인할 수 있을 것이다.

결론
VIPER 소개에 즐거웠기를 바란다. 많은 사람들이 이제 다음으로 무얼 해야할지 고민하고 있을 것이다. 만약 당신의 다음 앱을 VIPER를 써서 만들고 싶다면, 어디서부터 시작할 수 있을까?

이 글과 VIPER를 사용한 우리 예제는 우리가 만들 수 있는한 최대한으로 명확하고 잘 정의되게 했다. 우리의 to-do 리스트 앱은 꽤 같단하지만서도 어떻게 VIPER를 이용하여 앱을 만드는지 정확하게 설명하고 있다. 실제 프로젝트에서 당신의 과제와 제약사항들이 이 예제와 얼마나 비슷할지는 모르겠다. 우리의 경험에서는 각 프로젝트에서 VIPER를 조금씩 사용하도록 조금씩 바꾸었고, 바꾼 프로젝트 모두 그러한 접근법을 가이드하기위해 그것을 사용하는 것으로부터 좋은 이점을 얻었다.

다양한 이유로 VIPER에 의해 놓인 길로부터 탈선하고 싶은 생각이 들 수 있다. 아마 '토끼(bunny)' 객체를 양토장에 넣거나, 당신의 앱이 스토리보드에서 segue를 사용하고 싶을 수도 있을 것이다. 괜찮다. 이런 경우에는 당신이 어떤 결정을 내리든 VIPER가 어떤 표현을 할지 그 정신만을 기억하면 된다. 그것의 핵심은 VIPER가 단일 책임 원칙(Single Responsibility Principle)을 기반한 아키텍처라는 것이다. 당신에게 뭔가 문제가 생기면 어떤것을 먼저 옮길지 결정할 때 이 원칙을 생각해보아라.

그리고 또한 당신은 이미 존재하는 앱에 어떻게 VIPER를 적용시킬지 고민할 수도 있다. 이 경우, 새 기능이 추가될때 VIPER를 고려하여 추가해보아라. 우리는 이미 존재하는 프로젝트들도 이러한 루틴을 타고 있다. 이렇게 하면 VIPER를 이용한 모듈을 만들 수 있고, 또한 단일 책임 원칙을 기반으로 한 아키텍처는 현재 적용시키기 어려운 이슈들을 해결하는데 도움을 줄 것이다.

소프트웨어를 개발하는데 있어 재미있는점 중 하나는 모든 앱이 각기 다르다는 것이다. 따라서 각기 다른 앱마다 서로 다른 아키텍처를 구현할 수 있다. 우리는 모든 앱마다 새로운 배움의 기회를 얻을 수 있고 새로운 것을 시도한다. 만일 여러분이 VIPER를 사용하기로 결정하면, 몇가지 새로운 어떤 것을 배우게 될 것이라 생각된다. 읽어주어서 감사하다.

Swift 부록
저번주에 애플은 WWDC에서 Cocoa, Cocoa Touch 개발로서 Swift 프로그래밍 언어를 소개했었다. Swift 언어에 대해 복합적인 견해를 만들기에 좀 이르지만, 언어는 소프트웨어를 어떻게 설계하고 만드는지에 가장 큰 영향을 줄 것이라 생각된다. 우리는 VIPER가 무엇인지 배우는데에 돕기위해 Swift를 이용한 VIPER to-do 앱을 새로 만들었다. 우리는 Swift의 몇 특징을 찾았는데, VIPER를 이용해 앱을 만들기에 더 향상된 경험을 할 수 있었다.

Structs
VIPER에서 우리는 각 층 (Presenter에서 View로) 사이에 데이터를 주고 받기 위해 가벼운 모델 클래스를 사용한다. 이 PONSO들은 정말 간단하게 작은 양의 데이터를 다루는 경향이 있고 자식 객체가 되지 않으려는 경향이 있다. Swift 구조체들은 이 상황과 아주 맞아 떨어진다. 여기 VIPER Swift 예제에서 사용된 구조체의 예시가 있다. 이 구조체는 동일한지 체크하는 것이 필요했고, 이 타입의 두 객체를 비교하기 위해 == 연산자를 오버로드 하였다.


Type Safety
Objective-C와 Swift의 가장 큰 차이는 어쩌면 타입을 어떻게 다루는지이다. Objective-C는 다이나믹 타입이고 Swift는 컴파일 시간에 이것이 어떻게 구현되는지 타입체크를 함으로서 굉장히 의도적인(intentionally) 제약을 가진다. VIPER와 같은 아키텍처에는 앱이 여러 층에 걸쳐 구성될 때, type safety가 프로그래머의 효율면이나 아키텍처 구조 측면에서 크게 도움을 줄 수 있다. 컴파일러는 컨테이너를 확신할 수 있게 도와주고 오브젝트는 그들이 각 층 경계를 지나갈 때 옳바른 타입인지 확신할 수 있게 도와준다. 위에서 보여주듯 구조체를 사용하기에 좋은 장소이다. 만약 한 구조체가 두 층 사이에 경계에서 존재해야 한다면, 두 층 사이에서 어딘가로 사라져버리지 않는다는 이점을 얻을 것이다. type safety에 감사하라.


iOS 아키텍처 관련 번역글



번역에 도움을 주신 분 :



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,


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를 시도해보기에 관심이 생겼을 것이라 생각된다.


추가적인 읽을거리




WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

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 아키텍처 관련 번역글



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,