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

,