제목: Isolating tasks in Swift, or how to create a testable networking layer.

최근 몇년동안 더욱더 멋져지고있는 iOS 아키텍처가 많다. 모두 유효하고 부분적으로 장단점을 가진다. 이들은 모두 같은 것을 다룬다. 바로 프레젠테이션으로부터 비지니스 로직을 분리하는 것이다. 오늘 나는 여러분이 다음 프로젝트의 아키텍처에 적용시킬 수 있지만 어떤 아키텍처를 사용하더라해도 쓸 수 있는, 간단한 개념을 적어보려 한다.

꽤 일반적인 네트워크 레이어
내 관점을 설명하기 위해, 나는 먼저 보통 네트워크 레이어를 어떻게 구현하는지에대해 이야기할 것이다.

나는 각기 다른 여러 네트워크 레이어를 보아왔다. 대부분 NetworkManager, ConnectionManager 이런 모습이다. 앱에서 한 클래스안에 있는 모든 API 호출을 담고있다. 이게 유효하고 동작할지라도, 소프트웨어 설계에서 핵심 개념인 단일책임(Single Resoponsibility)은 실패한 것이다.

ConnectionManager에는 좋다고 생각되는 책임들을 너무 많이 담고있다. 게다가 종종 싱글톤으로 구현된다. 그리고 나는 싱글톤이 필연적으로 나쁘다고 말하진 않겠지만, 싱글톤은 의존성으로서 주입될 수 없고 테스팅에 쉽게 목(mock)이 될 수도 없다.

네트워크 레이어는 일반적으로 싱글톤으로 구현된다네트워크 레이어는 일반적으로 싱글톤으로 구현된다


이것은 매우 자주있는 방법이다. 나는 이것을 MVVM나 MVP 아키텍처에서도 보았다.

다른 방법
데이터 접근 레이어는 다른 방법으로 구현될 수 있다. 네트워크 패칭에서 그 프로세스를 표현해보자.

네트워크 호출에 포함된 단계네트워크 호출에 포함된 단계


이 방법을 넣어 세단계로 네트워크 호출을 함축한다.
  1. 요청 만들기: 요청 만들기에서는 URL, method, 파라미터(URL 경로든 http 바디에든), HTTP 헤더를 설정한다.
  2. 요청 디스패치하기: 이것은 매우매우 중요한 단계이다. 이전단계에서 만들어지고 설정된 이 요청은 반드시 URLSession이나 이걸 덮는 레이어(예를들면 Alamofire)를 사용하여 디스패치 되야한다.
  3. 응답을 받고 파싱하기: 이 부분은 이전 두 단계와 분리되어 구현되야할 중요한 단계이다. 이것은 JSON이나 XML 응답을 검증하고 유효한 Entity(혹은 Model)에 파싱하는 곳이다.

여러분의 아키텍처가 깔끔해지고 테스트하기 좋아지길 원한다면, 이 세단계는 다른 오브젝트에서 구현되어야한다.

네트워크 레이어는 적어도 세 컴포넌트를 사용하여 구현되어야함네트워크 레이어는 적어도 세 컴포넌트를 사용하여 구현되어야함


  1. Request: Request 오브젝트는 네트워크 요청을 구성하는데 필요한 모든 것들을 가진다. 이 구조체(혹은 클래스)는 하나의 네트워크 요청을 구성하고있는 책임을 갖는다. 그리고 한 네트워크 요청에 한 Request 오브젝트굉장히 중요하다.
  2. NetworkDispatcher: NetworkDispatcherRequest를 받아 Response를 반환하는 역할의 오브젝트이다. 이것은 프로토콜로 구현될 수 있다. 구체적인 클래스(혹은 구조체)가 아닌 프로토콜로 코드를 짤 수 있지만, 절때 싱글톤으로 구현하지 말아야한다. 그렇게 한다면, 이 NetworkDispatcherMockNetworkDispatcher과 대체될 수 잇고, 이 목은 실제 네트워크 요청을 날리지 않는 대신에 JSON 파일로부터 응답을 받아준다. 이것이 자연스럽게 테스트하기 좋은 아키텍처를 만든다.
  3. NetworkTask: NetworkTaskTask라는 제네릭 클래스의 자식클래스이다. 이 Task(좀 있다가 더 설명할것이지만)는 비동기적으로든 동기적으로든 Input 타입을 받아서 Output 타입을 반환하는 책임을 가지는 제네릭 클래스이다. TaskRxSwiftReactiveCocoaHydraMicrofuturesFOTask나 아니면 간단하게 클로저를 이용해서 구현할 수 있다. 여러분에 달려있다. 여기서 중요한 부분은 세부적인 구현이 아니라 개념이다.

요청 만들기
RequestURLRequest를 만드는데 필요한 모든 구성에대한 책임을 가진 오브젝트이다.

네트워크 요청에대한 예시는 다음과같이 생겼을 것이다.
//
//  Request.swift
//
//  Created by Fernando Ortiz on 2/12/17.
//

import Foundation

enum HTTPMethod: String {
    case get, post, put, patch, delete
}

protocol Request {
    var path        : String            { get }
    var method      : HTTPMethod        { get }
    var bodyParams  : [String: Any]?    { get }
    var headers    : [String: String]? { get }
}

extension Request {
    var method      : HTTPMethod        { return .get }
    var bodyParams  : [String: Any]?    { return nil }
    var headers    : [String: String]? { return nil }
}
여기서 중요한 부분은 Request가 분리된 오브젝트로 구현되었다는 점이다. 물론 Moya promotes처럼 열거형으로 구현될 수도 있는데, 여러분이 원하는 것에 달렸다. 나는 개인적으로 객체지향 스타일을 선호하고, BaseRequest 클래스와 AuthenticatedRequest같은 자식클래스나 GetAllUsersRequest, LoginRequest같은 세부적인 요청을 구현하길 선호한다.



NetworkDispatcher 구현하기
NetworkDispatcher는 네트워크 요청을 디스패치하는 책임을 가지는 컴포넌트이다.

주의: 여기서부터 내 예제에는 RxSwift를 사용할 것이지만, 여러분은 여러분이 좋아하는 방법으로 구현하면 된다.
//
//  NetworkDispatcher.swift
//
//  Created by Fernando Ortiz on 2/11/17.
//  Copyright © 2017 Fernando Martín Ortiz. All rights reserved.
//

import Foundation
import RxSwift

protocol NetworkDispatcher {
    func execute(request: Request) -> Observable<Any>
}
NetworkDispatcherRequest 오브젝트를 디스패치하고 응답을 반환하는 단일 책임을 가진다.
"
구체적인 구현대신 이 프로토콜을 사용하는것에서 멋진 점은 프로토콜 기반 구현은 쉽게 교체가능하게 만들어준다. MockNeoworkDispatcher은 실제 "네트워크" 작업을 실항하지는 한고 대신 JSON 파일에서 응답을 반환해주도록 하여 더욱 테스트하기 쉽게 만들어준다.

Task 고립시키기
Task는 하나의 로직 오퍼레이션을 실행시키는 책임의 간단한 오브젝트이다. 뒷단에서 사용자를 받아오거나, 로그인하기, 사용자 등록하기등이 있을 것이다. Task는 동기적으로나 비동기적으로 일어날 수 있지만, 클라이언트단에서 투명해야한다. 나는 편리한 추상화인 RxSwift의 Observable을 사용하길 좋아하는데, Promise, Signal, Future, 혹은 간단한 콜백이 충분할 수 있다.

Task의 간단한 구현은 아래처럼 될 수 있다.
//
//  Task.swift
//
//  Created by Fernando Ortiz on 2/11/17.
//

import Foundation
import RxSwift

class Task<Input, Output> {
    func perform(_ element: Input) -> Observable<Output> {
        fatalError("This must be implemented in subclasses")
    }
}
나는 객체지향 스타일을 사용했지만, 연관 타입이나 타입 erasure같은 것도 좋은 방법으로 사용할 수 있다. 이것도 잘 동작할 것이다. 내가 이런 객체지향 스타일을 좋아하는 이유는 덜 조잡하고 구현하기 단순해 보이기 때문이다.

모든 TaskInput 타입과 Output 타입이라는 두 제네릭 파라미터를 필요로한다. TaskInput 오브젝트를 받아 Output을 반환하는 일을 포함한 작업을 수행하는데, Observable처럼 이것을 추상화하여 사용할 것이다.

Task를 네트워크 작업을 실행시키기위해 특별하게 만들어줘야한다.
//
//  NetworkTask.swift
//
//  Created by Fernando Ortiz on 2/11/17.
//

import Foundation
import RxSwift

class NetworkTask<Input: Request, Output>: Task<Input, Output> {
    let dispatcher: NetworkDispatcher

    init(dispatcher: NetworkDispatcher) {
        self.dispatcher = dispatcher
    }

    override func perform(_ element: Input) -> Observable<Output> {
        fatalError("This must be implemented in subclasses")
    }
}
위에서 볼 수 있듯 ,NetworkTask는 두가지 제네릭 타입이 필요한데, InputOutput이다. Input이 반드시 Request 오브젝트이여야한다는 것은 당연한 일이다. NetworkTaskNetworkDispatcher로만 인스턴트화되어야 하므로 테스트하고 싶을때 MockNetworkDispatcher를 쉽게 보낼 수 있다.

아키텍처 검토하기
이 방법으로 비지니스 로직을 구현하면 복잡성을 설명하거나 테스트 용이함을 증가시키지 않고 결합력을 줄이는데 도움이 된다.

이 방법은 아래처럼 다이어그램으로 표현될 수 있다.

Task 기반 네트워크 레이어Task 기반 네트워크 레이어


결론
분리된 오브젝트에서 비지니스 로직 오퍼레이션을 고립시키는 것은 더욱 테스트하기 좋은 아키텍처로 만들기 때문에 좋은 방법이다. 복잡성을 줄여주고, 여러분이 사용하는 아키텍처를 전반적으로 독립시킨다. 이것은 ViewModel, Presenter, Interactor, Store 혹은 프레젠테이션 로직에서 비지니스 로직을 분리하는데 사용하기위한 어떤것 뒤에서든 사용될 수 있다.

이것이 나만큼 도움이 되었길 바란다. 뭔가 의심스러운 점이 있거나, 더 좋은 방법을 안다면 커멘트를 남겨달라.



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

으로 보내주시면 됩니다.



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

받은 트랙백이 없고 , 댓글이 없습니다.
secret
제목: 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

받은 트랙백이 없고 , 댓글이 없습니다.
secret
제목: 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

받은 트랙백이 없고 , 댓글  3개가 달렸습니다.
  1. 비밀댓글입니다
secret
제목 : 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

받은 트랙백이 없고 , 댓글 하나 달렸습니다.
  1. 비밀댓글입니다
secret
제목: 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

받은 트랙백이 없고 , 댓글이 없습니다.
secret

두 프로젝트를 동시에 진행하면서 앱 아키텍처에 관한 중요한 경험을 얻을 수 있었다. 내가 공부했던 것이나 생각했던 것에서 적용해보고 싶은 개념을 두 프로젝트에 적용시켜 진행했다. 그중 하나는 최근에 내가 공부하기도 했고 가장 의미있다고 생각되는, 네트워크 레이어를 어떻게 만드는지에 관한 이야기이다.

요즘 모바일 앱은 클라이언트-서버 지향이고, 앱이 크든 작든 앱 어디에나 네트워크 레이어를 사용한다. 나는 수 많은 네트워크 레이어 구현을 보았지만, 다들 뭔가 단점들이 있었다. 내가 제일 마지막으로 만든것에서 단점이 아예 없을거라 생각하진 않지만 두 프로젝트에서 굉장히 잘 동작하는듯 보였고, 현재까지도 작업을 하고 있다. 또한 테스트 커버리지가 거의 100%에 이른다.

이 글에서는 그렇게 복잡한것 까지는 다루지 않을 것인데, 인코딩된 요청을 JSON으로 보내는 한 백엔드의 네트워크 레이어만 다룰 것이다. 이 레이어는 나중에 AWS와 함께 이야기해보고 거기에 몇몇 파일도 보내볼 것이지만 기능적으로 확장하기 쉬울 것임을 확신한다.

프로세스를 생각해보자
이 레이어를 만들기 전에 던졌던 질문들이다.
  • 백엔드 url에관한 정보는 코드 어디에 놔둘까?
  • url 끝부분은 어느 코드에서 알아야할까?
  • 요청을 어떻게 만드는지는 코드 어디에서 알아야 할까?
  • 요청을 보내기 위해 준비해야하는 파라미터는 코드 어디에 둘까?
  • 인증(authentication) 토큰은 어디에 저장해 둘까?
  • 어떻게 요청(request)을 실행할까?
  • 언제 그리고 어디서 요청을 실행할까?
  • 요청 취소에 대해서도 고려해야할까?
  • 잘못된 백엔드 응답이나 백엔드 버그도 고려해야할까?
  • 써드파티 라이브러리를 사용해야하나? 어떤것을 사용할까?
  • 코어데이터도 처리해야할 것이 있나?
  • 이 솔루션을 어떻게 테스트해볼까?

백엔드 url을 저장하기
먼저, 어디에 백엔드 url을 저장해 두어야할까? 어떻게 우리 시스템에서 어떤 다른 시스템에 요청을 날릴지 알까? 나는 BackendConfiguration 클래스를 만들어 그 정보(url)를 담아둔다.

테스트하기도 쉽고, 구성하기도 쉽다. shared라는 스태틱 변수에 할당해놓고 파라미터로 전달할 필요 없이 네트워크 레이어 어디에서나 접근할 수 있다.

끝부분(Endpoints)
이 토픽은 내가 준비-시작(ready-to-go) 솔루션을 찾기 전에 경험했던 것이다. NSURLSession을 구성하는 동안 끝부분을 하드코딩하려 했고, url의 끝부분을 알고 쉽게 객체로 만들 수 있거나 주입될 수 있는 더미의 Resource스러운 오브젝트를 시도했다. 그러나 내가 찾던 해답은 아니었다. 

결국 *Request라는 객체를 만들기로 했는데, 이 객체는 다음과 같은 것들을 가진다. 어떤 메소드를 사용하는지, GET, POST, PUT 혹은 다른 어떤것인지, 요청의 바디(body)가 어떻게 구성되는지, 헤더에는 무엇을 담아 보내는지 이 객체에 담겨있다.

프로토콜로 구현된 이 클래스는 요청을 만들때 필요한 기본 정보를 제공한다. NetworkService.Method는 GET, POST, PUT, DELETE를 표현하는 열거형이다.

url 뒷부분이 저장된 예제 요청은 아래와같이 생겼다.

헤더 어디에도 딕셔너리를 생성하지 않으려고 BackendAPIRequest를 위한 extension을 정의할 수 있다.

*Request 클래스는 요청에 성공하기위해 필요한 모든 파라미터를 받는다. 적어도 당신이 필요로한 모든 파라미터는 보내야하고, 그렇지 않으면 요청 객체는 생성할 수 없다.

url 끝부분을 선언하는 것은 간단하다. 만약 끝부분에 id를 포함해햐한다면 아주 쉽게 추가할 수 있다. 왜냐하면 프로퍼티로 저장해둔 id를 가지고 있기 때문이다.

요청의 메소드는 절때 바뀌지 않으며 파라미터 바디나 헤더가 쉽게 구성되고 쉽게 수장할 수 있다. 테스트하기에 모든것이 쉬워진다.

요청을 실행하기

백엔드와 소통하려면 써드파티 프레임워크가 필요한가?

사람들이 AFNetworking(Objective-C)나 Alamofire(Swift)를 사용하는 것을 보았다. 나도 이것을 오랫동안 써왔으나 어떨때는 사용하지 않았다. NSURLSession이라는 것이 있는데, 이것이 굉장히 잘 되있기 때문에 나는 여러분이 굳이 써드파티 프레임워크를 사용하지 않아도 된다고 생각한다. 내 생각에는 이런 의존성이 여러분의 앱 구조를 더 복잡하게 만든다.

현재 솔루션은 NetworkService와 BackendService의 두 클래스로 구성된다.
  • NetworkService — 내부적으로 NSURLSession과 구성되어 HTTP 요청을 실행할 수 있게 한다. 모든 네트워크 서비스는 한번에 하나만 요청할 수 있고, 요청을 취소할 수도 있으며(큰 장점이다), 성공하거나 실패한 응답을 위한 콜백을 가지고 있는다.
  • BackendService — (좋은 이름은 아니지만 꽤 잘 들어 맞는다)백엔드와 관련있는 요청(위에서 설명한 *Request 객체)을 받는 클래스이다. NetworkService를 사용한다. 내가 현재 사용하는 버전에서는 응답 데이터를 NSJSONSerializer를 이용하여 json으로 만든다.

위에서 볼 수 있듯 BackendService는 인증 토큰을 헤더에 세팅할 수 있다. BackendAuth 객체는 NSUserDefault에 토큰을 저장하는 간단한 저장소이다. 필요에따라 토큰을 키체인에 저장할 수 있다.

BackendService는 request(_:success:failure:) 메소드의 파라미터로 BackendAPIRequest를 받고 request 객체로부터 필요한 정보를 얻어낸다. 이렇게 하면 캡슐화면에서도 좋고 백엔드 서비스도 꺼네온것만 사용한다.

NetworkService, BackendService, BackendAuth 모두 테스트하거나 유지보수하기 쉽다.

요청을 큐하기
어떤 방식으로 네트워크 요청을 날려야할까? 한번에 많은 네트워크 요청을 날려야 한다면 어떻게 할까? 일반적으로 요청의 성공과 실패를 처리하려면 어떻게 할까? 이 파트에서 위 질문들을 해결할 수 있을 것이다.

네트워크 요청을 실행하는 NSOperation과 NSOperationQueue를 사용하자.

나는 NSOperation으로 서브클래스를 만들고 asynchronous 프로퍼티를 오버라이드하여 true를 반환하게 한다.

다음으로, 네트워크 콜을 실행시키기위해 BackendService를 사용하고 싶으므로 NetworkOperation을 상속받은 ServiceOperation을 하나 만든다.

이 클래스 내부에는 BackendService를 생성하므로 이제 모든 서브클래스마다 이것을 생성할 필요가 없다.

이제 로그인 동작이 어떻게 구현되는지 보자.

서비스는 이 오퍼레이션의 초기화때 만들어놓은 요청을 start 메소드에서 실행시킨다. handleSuccess와 handleFailure 메소드는 서비스의 request(_:success:failure:) 메소드에 콜백으로 전달된다. 내 생각엔 이렇게하면 코드가 더 깔끔해지고 가독성도 좋아진다.

오퍼레이션들은 싱글톤 오브젝트인 NetworkQueue로 전달되며 모든 오퍼레이션이 이 큐에 들어갈 수 있다. 이제 나는 가능한 간단하게 유지한다.

한 곳에 오퍼링션 실행을 모아두면 어떤 이점이 있을까?
  • 모든 네트워크 오퍼레이션을 간편하게 취소할 수 있다.
  • 이미지를 다운받거나 열악한 네트워크 환경에서 많은 데이터를 소모해야하지만 앱 동작에는 크게 상관없는 다른 오퍼레이션들을 취소할 수 있다.
  • 큐를 필요한 순서대로 실행할 수 있고 빨리 답변 올 수 있는 것부터 실행할 수 있다.

코어데이터와 함께 작업하기
사실 이것 때문에 이 글의 발행이 늦어졌다. 이전 버전에서는 네트워크 레이어 오퍼레이션이 코어데이터 객체를 반환했었다. 응답을 받고 파싱한 뒤, 또다시 코어데이터 객체로 변환시켰다. 이 방법은 그다지 이상적이지 않았다.
  • 오퍼레이션에서 어떤 코어데이터인지 알고 잇어야했다. 왜냐하면 프레임워크를 분리하기 위해 떼어놓은 모델을 가지고 있고 네티워크 레이어 역시 프레임워크로부터 분리되 있었다. 네트워크 프레임워크는 모델 프레임워크에대해 알고 있어야 했다.
  • 각 오퍼레이션은 어떤 컨텍스트에서 동작하는지 알아야 했으므로 NSManagedObjectContext 파라미터를 받아야 했다.
  • 응답을 받고 성공(success) 블럭을 호출할 때마다 먼저 컨텍스트에 객체를 찾으려 하거나 디스크에 객체를 패치하기위해 디스크 검색을 해야만 했다. 내 생각엔 이 점이 매우 큰 단점이었고, 아마 당신도 항상 코어데이터 객체를 생성하고 싶지는 않았을 것이다.

그래서 나는 네트워크 레이어를 코어데이터로부터 완전히 떼어냈고, 응답을 파싱하여 오브젝트를 만드는 중간 레이어를 만들었다.
  • 이렇게 파싱하고 오브젝트를 생성하면 디스크에 접근하지 않으므로 빠르다.
  • 오퍼레이션에 NSManagedObjectContext를 전달할 필요가 없다.
  • 성공 블럭에서 파싱된 아이템으로 코어데이터 객체를 갱신할 수도 있고, 오퍼레이션을 생성하는 곳 어디에든 있을 수 있는 코어데이터 객체를 참조할 수 있다. — 오퍼레이션이 큐에 추가될 때의 내 경우이다.

응답을 맵핑하기
유용한 아이템을 위해 JSON을 파싱하는 로직과 매핑하는 로직을 분리해주는 응답 맵퍼(response mapper)가 있다.

우리는 두가지 타입으로 파서를 구별할 수 있다. 첫번재는 특정 타입의 한 객체를 반환한다. 두번째는 어떤 항목의 배열을 파싱하는 파서이다.

먼저 모든 항복에 해당하는 일반적인 프로토콜을 선언하자.

이제 모델과 맵핑하는 몇 객체이다.

그리고 파싱하다가 에러가 발생시 던지는(throw) 에러타입을 선언하자.
  • Invalid — 파싱된 json이 nil이고, nil을 반환하면 안될때나, json이 하나의 객체가 아니라 객체의 배열일때 던지는 에러타입이다.
  • MissingAttribute — 이름이 의미하는 바와 같다. json에서 키를 잃어버리거나 파싱 후에 값이 nil이면 안되는데 nil일때 던지는 에러타입이다.

ResponseMapper는 이렇게 생겼을 것이다.

ResponseMapper는 백엔드로부터 받은 obj(우리의 경우 JSON이다)와 parse라는 메소드를 받는데, parse는 obj을 이용해 ParsedItem을 따르는 A 객체를 반환한다.

이제 우리가 세부적으로 구현한 제네릭 매퍼를 만든다. 아래 매퍼는 로그인 오퍼레이션에대한 응답을 파싱하는데 사용되는 매퍼이다.

ResponseMapperProtocol은 세부적인 매퍼에의해 구현된 프로토콜로서 메소드를 공유하여 응답을 파싱한다.

그러면 이 매퍼를 오퍼레이션의 성공 블락에서 사용하고, 이것을 딕셔너리 대신에 특정 타입의 객체로 사용할 수 있다. 이전보다 훨씬 사용하기 쉽고 테스트하기도 쉽다.

마지막으로 배열을 파싱하기위한 응답 매퍼이다.

모든것이 정상적으로 파싱되면 매핑할 함수를 받아 항목의 배열을 반환한다. 매퍼의 결과가 당신이 예상한 것에 따라 한 아이템이 파싱되지 않을 경우 에러를 던질 수도 있고, 문제가 있을 때는 빈 배열을 반환할 수도 있다. 매퍼는 백엔드로부터 받은 응답인 obj가 JSON 요소의 배열이라 예상한다.

이 도표는 네트워크 레이어의 구조를 보여준다.




예제 프로젝트
여러분은 내 깃헙에서 예제 프로젝트를 확인해 볼 수 있다. 프로젝트에서 사용된 백엔드 url은 모두 가짜이며 모든 요청은 실패할 것이다. 나는 여러분에게 네트워크 레이어의 파운데이션이 어떻게 생겼는지 보여주기만을 위해 이것을 만들었다.

요약
나는 이러한 네트워크 레이어 방식이 굉장히 유용하고 간편하며 작업에 편리하다는 것을 깨닭았다.

  • 가장 큰 이점은 비슷한 설계의 다른 새 오퍼레이션을 쉽게 추가할 수 있고, 이 레이어가 코어데이터에대해 전혀 모른다는 점이다.
  • 큰 노력없이 코드 커버리지를 거의 100%에 근접하도록 만들 수 있다. 완전 어려운 케이스를 어떻게 커버할지 생각할 필요도 없다. 어디에도 케이스가 없기 때문이다(원문: because there is no such cases at all).
  • 이 네트워크 레이어의 핵심은 비슷한 복잡성을 가진 다른 앱에서도 다시 사용할 수 있다는 점이다.



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

받은 트랙백이 없고 , 댓글이 없습니다.
secret

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

받은 트랙백이 없고 , 댓글이 없습니다.
secret
원문 :  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

받은 트랙백이 없고 , 댓글이 없습니다.
secret

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

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

이 글에선 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

받은 트랙백이 없고 , 댓글이 없습니다.
secret


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

받은 트랙백이 없고 , 댓글이 없습니다.
secret

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

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

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

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

VIPER CarVIPER Car


왜 우리는 VIPER를 골랐을까?


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

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


MassiveMassive

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

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

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


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

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

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

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

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

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

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

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

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

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

Passing DataPassing Data


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

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


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

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

CoreData SchemaCoreData Schema

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


그러나 Entity는 어떨까?

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

CoreData Convert To EntityCoreData Convert To Entity

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


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


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


의존성 주입(Dependency Injection)

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



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

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

이게 다다.

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

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

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

Unit Test MockUnit Test Mock

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



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

Service 동작



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

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


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

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


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


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

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

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


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

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

ViperOrNotViperOrNot


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

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

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

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

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

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

흥미로운 자료들

iOS 아키텍처 관련 번역글
 



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

받은 트랙백이 없고 , 댓글이 없습니다.
secret

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

받은 트랙백이 없고 , 댓글  8개가 달렸습니다.
  1. 비밀댓글입니다
    • 네 직역하면 댓글 말씀처럼 됩니다만.. 저도 조사해보니 원문처럼 해석되기도 하는것 같았습니다. 비교해보고 좀 더 나은 해석방식을 골라서 포스팅에 적용했습니다 ^^
  2. 좋은 글 잘 봤습니다. 아직 iOS쪽 패턴에 대해서는 잘 모르고 있었는데
    이 글을 바탕으로 개념을 잡을 수 있겠네요 ^^
  3. 좋은글 번역 감사합니다! 앞으로도 종종 부탁드려요 ^^~
  4. 이 소스를 참고해서 mvp를 구현 해보려고 하는데 mvp 어셈블링은 어느 파일에서 또는 어떻게 하는게 좋을까요???!
    • http://stackoverflow.com/questions/2056/what-are-mvp-and-mvc-and-what-is-the-difference

      위 링크에서, 다양한 관점에서 mvp, mvc 비교를 잘 놓았어요~ 여러가지 방법이 있겠지만, 위 링크 설명을 토대로 말씀드릴게요.
      다 그런건 아니지만 mvp는 일반적으로 한 프레젠터당 하나의 뷰를 가집니다. (뷰와 프레젠터는 인터페이스로서 소통하구요)

      뷰-프레젠터: 뷰와 모델이 서로를 참조하면 됩니다. 뷰의 액션을 프레젠터에게 보내기위해 뷰는 프레젠터를 가지고 있어야하고, 프레젠터의 갱신을 뷰에 반영하기위해 프레젠터도 뷰를 가지고 있어야합니다.

      프레젠터-프레젠터: 뷰의 어떤 액션을 실제로 처리하는 녀석은 프레젠터입니다. 한스택에서 다른 스택으로 이동할시에는 프레젠터에서 해주면 됩니다.
secret