Result orignted

Result Oriented Development : Result 지향 프로그래밍

함수형 프로그래밍에 관한 Saul Mora의 지난 번 강연을 통해, 최소한의 기능을 수행하는 작은 함수들을 이용해 복잡하고 추적하기 어려운 함수를 파이프라인 형태로 재작성할 수 있다는 사실을 배울 수 있었습니다. 함수들을 연결하는 기능을 충분히 활용하기에는 optional 만으로는 부족합니다. 이번 try! Swift NYC 강연에서는 Result (또는 Either) 라고 불리는 유용한 Monad를 통해 함수형 프로그래밍 능력을 한층 향상시키는 방법을 소개합니다.

번역: 문상준 iOS 개발자. Reactive, Functional programming과 Swift 관련 글쓰기에 관심이 많습니다. 현재는 Vandad Nahavandipoor가 진행하는 crowdsource swift book 프로젝트에서 열심히 활동하고 있습니다. 관심있는 분들의 참여는 언제나 환영입니다.


Swift에서의 Result 지향 프로그래밍(00:00)

이전 강연인 객체 지향 함수형 프로그래밍 에서는 아래와 같이 Objective-C 로 작성한 코드를 원형 그대로 Swift 언어로 작성한 함수를 살펴보았습니다:

func old_and_busted_expired(fileURL: NSURL) -> Bool {
   let fileManager = NSFileManager()
   if let filePath = fileURL.path {
      if fileManager.fileExistsAtPath(filePath) {
         var error: NSError?
         let fileAttributes = fileManager.attributesOfItemAtPath(filePath, error: &error)
         if let fileAttributes = fileAttributes {
           if let creationDate = fileAttributes[NSFileModificationDate] as? NSDate {
               return creationDate.isBefore(NSDate.oneDayAgo())
            }
        }
         else {
             NSLog("No file attributes \(filePath)")
        }
      }
    }
    return true
}

이것은 Swift 방식으로 코드를 작성한 것이 아니라 Objective-C 코드를 그대로 변환한 것에 불과합니다. 때문에 이전 강의에서는 bind 라고 불리는 새로운 함수를 작성한 다음, 아래와 같이 함수형 스타일의 커스텀 연산자로 변환했습니다. 좀 어려워 보이는 코드이긴 하지만, 함수들의 공통된 특징을 추출하여 간결하게 만든 것에 불과합니다.

func bind<A, B>(_ a: A?, f: (A) -> B?) -> B?
{
  if let x = a {
    return f(x)
  } else {
    return .none
  }
}

func >>=<A, B>(a: A?, f: (A) -> B?) -> B?
{
  return bind(a, f: f)
}

우리는 이 아이디어가 함수형 프로그래밍에서 비롯되었다는 사실과 함수들을 서로 연결할 수 있다는 것을 배웠습니다. 함수를 체인의 형태로 서로 결합하는 이 방법은 매우 유용하죠.

func expired(fileURL: NSURL) -> Bool {
   return fileURL.path
   >>- fileExists
   >>- retrieveFileAttributes
   >>- extractCreationDate
   >>- checkExpired
   ?? true
}

여기서 한발 더 나가볼까요? 이미 Result 타입에 대해 알고 계시겠죠. 사실 Result 는 좀 더 강력한 Optional 타입이라 할 수 있습니다. Result 타입은 에러 처리에 특히 유용하며 코드의 가독성을 높여주며, 에러 처리 코드를 마치 에러를 처리하기 위한 코드가 아닌 것처럼 보이게 합니다. Result 타입을 이용하면 그동안 에러를 처리하기 위해 필요했던 수많은 if 문을 더이상 사용하지 않아도 됩니다.

Result (01:16)

어떻게 Result 타입을 작성하는지 살펴봅시다.

enum Optional<Wrapped>
{
  case none
  case some(Wrapped)
}

Optional 에서부터 시작해봅시다. Optional 은 위에서 보는 것처럼 두 가지 case 문을 가지고 있습니다. Swift 에서 Optional 은 단지 열거형으로, value 라는 값을 가진 어떤 것 또는 아무 것도 가지고 있지 않은 것을 표현합니다. 이제 Optional 에서 아무 것도 가지고 있지 않은 경우에 대해 관련 값을 설정하겠습니다.

enum Optional<Wrapped>
{
  case some(Wrapped)
  case none(error)
}

이제 이것은 더이상 none 이 아닙니다. 여기에 에러를 추가했기 때문에 이를 failure 와 같은 이름으로 변경해볼까 합니다. 추가로, some 이라는 이름도 좋지만 failure 와 some 은 그다지 어울리는 이름은 아닌 것 같아서 some 의 이름을 success 로 변경했습니다.

이런 개발 뉴스를 더 만나보세요

enum Result<T>
{
  case success(T)
  case failure(NSError)
}

열거형에는 여전히 두 가지 case 만 존재합니다. value 를 가진 success 와 에러를 갖고 있는 failure 입니다.

enum Result<T, E: Error>
{
  case success(T)
  case failure(E)
}

이것은 더이상 Optional 이 아니라 Result 입니다. Swift 에서 기본적으로 제공하는 타입 중의 하나인 Optional 타입은 제너릭이기 때문에 Objective-C 또는 Swift 에서 어떠한 타입이라도 타입을 둘러싸서(wrap) Optional 로 만들 수 있습니다.

success 와 에러가 포함된 failure 로 Result 를 구성하면 다음과 같습니다.

enum Result<T>
{
  case success(T)
  case failure(NSError)
}

아래처럼 제너릭 에러(여기서는 E)를 인자로 사용해서 Result 를 구현한 많은 라이브러리들을 보았을 것입니다. 제너릭 에러를 인자로 정의하였기 때문에 상황에 따라 특정한 에러를 설정하여 Result 타입에 전달할 수 있습니다.

enum Result<T, E: Error>
{
  case success(T)
  case failure(E)
}

하지만, 지금은 에러를 아래와 같이 NSError 로 가정할 것입니다.

enum Result<T>
{
  case success(T)
  case failure(NSError)
}

주제를 좀 더 명확하게 해주고 에러에 신경쓰는 대신 Result 타입이 무엇인지를 이해하는데 집중할 수 있도록 이런 방식을 사용했습니다. 앞으로 언급될 Result 타입은 위의 형태로 구성된 Result 를 의미합니다.

다시 Optional 로 돌아가 봅시다. Optional string 을 반환하는 아래와 같은 함수가 있습니다.

func doSomethingFun() -> String?
{
  return "跟朋友们一起学习说中文 🇨🇳"
}

이것은 String 에 대한 Optional wrapper 이기 때문에 Optional 형태로 표현하면 다음과 같습니다.

func doSomethingFun() -> Optional<String>
{
  return .some("跟朋友们一起学习说中文 🇨🇳")
}

Optional로 둘러싸여 있는(wrapped) String value 를 얻을 수 있습니다. 이를 사용하기 위해서는 먼저 optional 을 풀어야(unwrap) 해야 합니다. Result 타입에서도 마찬가지입니다. 위의 예제를 Result 타입으로 구성하면 아래와 같습니다.

func doSomethingFun() -> Result<String>
{
  return .success("Ride bike across the Rocky Mountains 🚵")
}

타입 정보가 포함된 Result 를 정의했습니다. success case는 value를 둘러싸고(wrap) 있습니다. 예제에서는 String 타입과 문자열 value가 되겠네요.

만약 Result 타입을 사용한다면, 함수 호출 결과를 switch 문을 이용해 간단하게 처리할 수 있습니다. 상당히 간편한 방식이지요?

let result = doSomethingFun()
switch result {
case .success(let funThing):
  print("Had fun: \(funThing)")
case .error:
  print("Error having fun 😢")
}

Result 타입을 이용해 더 많은 일을 할 수 있습니다. Swfit standard library 에서 Optional 타입을 찾아봅시다. Xcode 에서 Swift library 의 optional 에 관한 헤더 정보는 다음과 같습니다.

/// A type that can represent either a `Wrapped` value or `nil`, the absence
/// of a value.
public enum Optional<Wrapped> : _Reflectable, NilLiteralConvertible {
    case none
    case some(Wrapped)
    /// Construct a `nil` instance.
    public init()
    /// Construct a non-`nil` instance that stores `some`.
    public init(_ some: Wrapped)
    /// If `self == nil`, returns `nil`.  Otherwise, returns `f(self!)`.
    @warn_unused_result
    public func map<U>(@noescape f: (Wrapped) throws -> U) rethrows -> U?
    /// Returns `nil` if `self` is `nil`, `f(self!)` otherwise.
    @warn_unused_result
    public func flatMap<U>(@noescape f: (Wrapped) throws -> U?) rethrows -> U?
    /// Create an instance initialized with `nil`.
    public init(nilLiteral: ())
}

nonesome 이 있다는 것과 제너릭 인자인 Wrapped를 가진다는 사실을 확인할 수 있습니다. 또한 mapflatMap 이라는 두가지 함수가 있다는 것도 확인할 수 있습니다. 정말 유용한 함수들인데요, Swift 에서는 Optional 에서 map 을 쓸 수 있습니다.

Map 과 FlatMap (05:07)

Map 은 쉬운 개념으로, input 타입을 output 타입으로 1:1 매핑하는 것입니다. 예를 들면, 바나나로 이루어진 배열을 map 하고 변환하여 사과들로 이루어진 배열로 만들 수 있습니다. 단순히 1:1 매핑만 하고도 이런 결과를 얻을 수 있다니 정말 간단하죠?

FlatMap 도 유사합니다. 배열에 대해 map 변환을 적용하고 싶은데 중첩된 배열 또는 고차원 배열을 만날 때가 있습니다. FlatMap 은 이런 경우 내부적으로는 map 을 먼저 적용한 다음 flatten 절차를 수행합니다. 또한 부적합한 값들을 제거하거나 배열을 1차원 배열로 만드는데 사용할 수 있습니다. map 과 매우 유사하며, 함수형 프로그래밍 개념에서 매우 유용하게 사용됩니다. 다음으로, functorsmonads 에 대해 알아봅시다.

Functor 와 Monads (06:22)

functor 는 또 다른 타입을 둘러싸고(wrap) 있는 타입이며 map 함수를 가지고 있습니다. 위에서 설명한 Optional 타입, Result 타입에서도 사용했었지요? 순수 함수형 프로그래밍 지지자들은 세 가지 법칙을 수학적으로 만족해야 한다고 설명하겠지만, 프로그래밍에서는 functor 는 map 함수를 가지고 있는 타입이라고 이해하면 됩니다. map 함수의 구조는 다음과 같습니다.

func map<U>(T-> U) -> FunctorOf<U>

Monads 도 매우 유사합니다. 이것은 다른 타입을 둘러싸고(wrap) 있는 타입이며 flatMap 을 갖고 있습니다. 즉 Functorsmap 을 가지고 있고, monadsflatMap 을 가지고 있다고 간단히 이해하시면 됩니다.

func flatMap<U>(T -> MondadOf<U>) -> MonadOf<U>

실제 프로그래밍에서 이들을 어떻게 사용할까요?

아래의 코드에서 우리는 string value 라는 문자열을 값으로 가지고 있는 string 변수를 가지고 있습니다. ? 로 optional 을 표현하는 간편한 문법 대신, Optional 을 이용한 표기법을 사용했습니다. map 을 사용하고 싶다면 아래와 같이 map 함수를 사용할 수 있으며, 이전 강의에서 작성한 binding 연산자(»-)를 사용할 수도 있습니다.

let string: String? = "string value"
let string = Optional<String>("string value")

string.map { print($0) }

string >>- { print($0) }

만약 map 함수를 사용한다면 Optional 내부에 있는 map 함수에서는 다음과 같은 작업이 수행됩니다. 우선, switch 구문을 이용해 Optional 안에 저장된 값에 접근했습니다. 저장된 값(여기서는 x 에 해당함)을 얻으면, 여기에 transform 이란 이름을 가진 함수를 적용합니다. 그리고 이것을 다시 둘러싸서(rewrap) 리턴값으로 돌려줍니다.

func map<U>(_ transform: T -> U) -> U?
{
    switch self {
      case .some(let x):
        return .some(transform(x))
      case .none:
        return .none
      }
}

물론, none 이라면 아무 것도 리턴하지 않습니다. 하지만 여기서는 functor 또는 monads 를 매번 리턴하고자 하며, 위와 유사한 작업을 Result 타입에서 map 함수를 이용해 수행하고자 합니다. switch 구문과 success, failure 를 넣었습니다.

func map<U>(_ transform: (T) -> Result<U>) -> Result<U>
{
  switch self {
    case .success(let value):
    return transform(value)

    case .failure(let error):
    return Result<T>.failure(error)
  }
}

두 개의 case 문이 있습니다. success 에서는 transform 함수를 적용한 결과값을 리턴합니다. transform 인자값으로 전달된 함수의 타입을 살펴보면, T 를 인자로 받아서 Result<U> 를 리턴하고 있습니다. transform 함수를 적용한 결과값을 바로 리턴할 수도 있습니다. transform 함수를 적용한 결과는 map 함수로부터 직접 얻을 수 있습니다.

failure 에서는 에러를 새로운 Result 타입과 다시 매핑하여 리턴하는 작업을 수행하고 있습니다. 실제로 어떤 것을 수행하며 항상 어떤 값을 리턴하므로 어떤 결과값이 항상 보장됩니다. 아마 특정 값이거나 에러일 것입니다.

flatMapmap 과 유사한 형태로 구성할 수 있습니다. map 을 실제로 사용할 때는 아마 아래같이 사용하게 될 겁니다. Optional 을 이용할 때 optional 바인딩을 위해 if-let 구문을 자주 사용하곤 합니다. map 함수 사용의 장점은 값에 접근하는 작업과 switch 구문이 enumeration 과정 안에 캡슐화되기 때문에 코드를 깔끔하게 유지할 수 있다는 점입니다. 이때 enumeration 은 이 map 함수를 기반으로 수행됩니다.

enum Result<T>
{
  case success(T)
  case failure(Error)

  func map<U>(_ transform: (T) -> U) -> U
  {
    switch self {
    case .success(let value):
      return transform(value)
    case .failure(let error):
      return Result<T>.failure(error)
    }
  }
}

let result: Result<String> = doSomethingFun()
result
.map { value in
  print("Had fun: \(value)")
}
.mapError { error -> NSError in
  print("Error having fun: \(error)")
}

이제 Result 를 가지고 value 를 map 하겠습니다. map 함수와 value 를 가지고 있으니 그것을 수행하기만 하면 됩니다. map 함수가 우리를 대신해 value 에 접근할 수 있게 했습니다. 만약 에러가 발생한다면 value 대신에 에러를 갖게 될까요?

Result 타입을 실제 구현할 때 보통 map error 헬퍼 함수를 함께 구현합니다. 이것은 에러가 무엇인지 확인하는 작업을 담당합니다. 대부분의 경우 map 이 발생하면 map error 는 발생하지 않습니다. 그 반대도 마찬가지입니다. 같은 시퀀스 안에서 두 경우가 함께 발생하는 일은 절대 없을 것입이다. 물론, 이것을 처리하는 또다른 방식도 존재합니다. 앞에서 언급한 처리방식이 항상 옳은 것은 아니지만 특정 상황에서는 매우 깔끔하고 훌륭한 방식일 것입니다.

마이크로-네트워킹 프레임워크 (10:30)

마이크로 네트워킹 프레임워크 예제를 통해 실제 업무에서는 어떻게 사용하는지 살펴봅시다:

HTTP 관련 네트워크 프로그램을 작성할 때 위에서 설명했던 개념들을 잘 활용할 수 있을 거라고 생각합니다. 웹을 통해 데이터를 요청했을 때, 저는 제가 요청한 실제 데이터 타입에 집중하고 싶고, 그밖의 다른 것들에 대해서는 신경쓰고 싶지 않습니다. 제가 원하는 API 는 다음과 같습니다. 어느 곳으로부터 비동기적으로 데이터를 요청하면 제가 찾고 있는 실제 데이터 타입을 전달해 주고, 이 연산을 기반으로 다른 작업도 수행할 수 있습니다.

예전에 발표되었던 마이크로 네트워킹 라이브러리와 관련된 Chris Eidhof 의 포스팅을 읽어보신 분이 있을 것으로 생각됩니다. 그는 API 요청과 함께 몇 가지 인자를 전달할 수 있는 작고 멋진 함수를 작성했습니다. 이것이 멋진 이유는 URL 과 위에서 설명한 개념이 사용된 resource, completion 블럭을 가지고 있기 때문입니다. 이 함수에는 비동기적인 데이터 요청을 돕는 함수적인 작업들이 들어있습니다. 함수의 구조는 다음과 같습니다.

func apiRequest<A> (
  modifyRequest: NSMutableURLRequest -> (),
  baseURL: NSURL,
  resource: Resource<A>,
  failure: (Reason, NSData?) -> (),
  completion: A -> ()
)

API 를 직접 만들어 봤는데요. 제 API 에서는 HTTP response 반환값을 처리하기 위해 Result 타입을 사용하였습니다. API 중의 하나인 request 함수는 request, resource 를 가지고 있습니다. resource 는 과연 어떤 모습일까요?

func request<R: HTTPResource> (
 resource: R,
 cachePolicy: URLRequest.CachePolicy,
 requestTimeout: TimeInterval,
 host: HTTPHost?,
 completion: (Result<R.RequestedType>) -> Void
)

resource 프로토콜 안에는 ResultType 이 들어있습니다. 이 resource 는 특별한 associated value 를 가지고 있습니다. 예를 들어 person 객체가 있다고 해봅시다. 이것을 프로토콜과 연관된 제너릭 타입으로 전달하여 사용할 수 있으며 이와 유사한 방법을 사용하여 ErrorType 에도 적용할 수 있습니다.

public protocol HTTPResource
{
  associatedtype ResultType
  associatedtype ErrorType: HTTPResourceError
}

또한, 네트워크 리소스를 요청하기 위해 필요한 것들을 아래와 같이 몇 개 추가하였습니다. 그 중에는 path 도 있고, 쿼리 메소드도 있습니다. 이런 세세한 것들은 앱의 view controller 또는 좀 더 높은 수준에서 비동기적인 요청을 수행할 때 별로 신경쓰고 싶지 않은 것들입니다. 이제 프로토콜에는 path, 메소드, 쿼리 메소드, 쿼리 인자값들, parse 함수가 있습니다. parse 함수는 Result 타입을 가지고 있습니다.

public protocol HTTPResource
{
  associatedtype ResultType
  associatedtype ErrorType: HTTPResourceError
  var path: String { get }
  var method: HTTPMethod { get }
  var queryParameters: [String: String] { get }
  var parse: Result<ResourceDataType, HTTPResponseError> { get }
}

사실, 위에서 작성한 parse 함수는 잘못된 것입니다. 아래의 코드가 올바른 코드입니다. 다른 함수에 대한 별명(alias) 도 정의했으며 Swift 3 문법을 이용해 제너릭 typealias 를 표현했습니다. 제너릭 인자를 다음 번 타입에 전달할 수 있습니다. 멋진 기능이죠? HTTP 에서 파싱과 관련된 모든 것들을 제너릭 방식을 이용해 처리하였습니다.

public typealias ResourceParseFunction<ResourceDataType> =
(Data) -> Result<ResourceDataType, HTTPResponseError>
public protocol HTTPResource
{
  associatedtype ResultType
  associatedtype ErrorType: HTTPResourceError
  var path: String { get }
  var method: HTTPMethod { get }
  var queryParameters: [String: String] { get }
  var parse: ResourceParseFunction<ResultType> { get }
}

HTTP response 를 받아서 어떻게 처리할 것인지에 대해 좀 더 살펴봅시다:

  • validate response code
  • validate/preprocess data
  • deserialize JSON from data
  • decode JSON objects
  • call completion handler

우선, response 코드가 적절한지 확인할 것입니다. 401, 500 에러, 201 등의 response code 를 받았을 때 적절성 검사를 수행한 다음 빠르게 값을 리턴합니다. 만일 이것을 통과했다면, 재확인 작업을 하거나 데이터에 대한 사전처리 작업을 진행할 수 있을 것입니다.

그러고 나면 올바른 데이터가 수신된 것으로 보고, JSON deserialization 과 JSON 파서를 이용한 decode 작업을 수행합니다. 마지막에는 completion 핸들러를 호출할 것입니다.

API 에는 completion 블럭이 있다는 사실을 기억하실 겁니다. completion 핸들러는 요즘에 많이 사용하는 구조이며, 여기서는 completion 블럭에 result 를 전달하는 형태로 사용하고 있습니다. 위의 처리 절차들을 모두 포함한 구현 코드를 작성할텐데, 그 구조는 다음과 같습니다.

func completionHandlerForRequest<R: HTTPResource>(
 resource: R,
 validate: ResponseValidationFunction,
 completion: @escaping (Result<R.RequestedType>) -> Void
)
 -> (Data?, URLResponse?, Error?) -> Void
{
return { (data, response, error) in
  _ = Result(response as? HTTPURLResponse, failWith: .InvalidResponseType)

    >>- validateResponse(error)
    >>- validate(data)
    >>- resource.parse
    >>- completion
  }
}

HTTP 결과를 처리하는 함수를 반환하는 함수를 작성했는데 return 구문 뒤에 { 로 시작하는 또다른 함수가 있는 구조가 조금 이상하게 보일지도 모르겠습니다. 이 코드의 중간부분에서 일어나는 작업에 초점을 맞춰 살펴보도록 하겠습니다. 이것은 NSURLSession task 메소드의 completion 핸들러와 유사합니다. 한 번 살펴볼까요?

  return { (data, response, error) in
  _ = Result(response as? HTTPURLResponse,
                failWith: .InvalidResponseType)
    >>- validateResponse(error)
    >>- validate(data)
    >>- resource.parse
    >>- completion
}

우선, 파이프라인 구조를 만들고자 합니다. 이를 위해 >>- 의 형태를 가진 bind 연산자를 사용했습니다. 파이프라인을 시작하기 위해 우선 Result 를 구성했습니다. 여기서는 Result 구현을 위해 response 를 HTTPURLResponse 로 옵셔날 캐스트(as?) 처리했습니다. 만약 실패한다면, 이미 정의해 놓은 failure error type 으로 지정됩니다. 실패시 HTTPURLResponse 가 아닌 어떤 것이 반환되며, 나머지 파이프라인은 맨 처음 발생한 에러 값을 가진 채로 생략될 것입니다.

그런 다음, error 를 가지고 response 적절성을 확인하는 작업을 수행합니다. 함수를 반환하는 함수를 작성했는데 여기서는 특정 시점에 발생할 수 있는 에러와 함께 적절성 확인 작업을 처리할 것입니다. 이 파이프라인에서는 HTTPURLResponse 를 사용할 것이며, HTTPURLResponse 가 포함된 Result 타입이 반환됩니다. 파이프라인에서 다음 단계로 넘어가면, 함수를 리턴하는 또 다른 함수를 만나게 되고, 이 과정이 계속 반복됩니다.

func validateResponse(_ error: Error?) ->
(HTTPURLResponse?) -> Result<HTTPURLResponse,
HTTPResponseError>

func validate(Data?) -> (HTTPURLResponse) ->
Result<Data>

resource.parse: (Data) -> Result<ResourceType>

completion :
(Result<ResourceType>) -> Void

resource 에는 parse 함수가 있습니다. parse 함수는 data 를 인자로 받고, HTTP resource 에서 실제 찾고자 하는 타입으로 정의한 타입을 리턴합니다. 하지만 처음에는 기본적으로 NSData 또는 Data(swift 3 버전의 경우) 와 같은 바이너리 데이터를 얻게 될 것입니다.

validateResponse:
(HTTPURLResponse?) -> Result<HTTPURLResponse>

customValidate:
(HTTPURLResponse) -> Result<Data>

resource.parse: 
(Data) -> Result<ResourceType>

completion :
(Result<ResourceType>) -> Void

여기서 파이프라인이 등장합니다. validate, custom validate, parse, completion 을 인자로 사용합니다. URL response 로부터 URL response 로 전달되며 그 결과는 풀어진 상태로(unwrap) 다음 함수에 전달됩니다. 이 함수는 URL response 를 인자로 사용하고 Result 를 반환합니다. 그 데이터는 다음 함수에 전달되며 parse 함수는 ResourceType 이 포함된 Result 를 반환합니다. 결과물은 completion 함수의 인자로 전달됩니다.

파이프라인이 하는 역할은 이게 전부이며, 현재 잘 동작합니다. 왜냐하면 이전 함수의 모든 결과값은 다음 함수의 입력값으로 사용되기 때문입니다. 바로 이 것이 함수를 체인 형태로 구성하는 방법입니다. 단, 결과값은 입력값과 항상 일치해야 합니다. 예제에서 보았듯이 Result 를 인자로 받아서 함수형 파이프라인을 이용해 data, response 등으로 연결했습니다.

Result 를 사용했을 때의 장점은 이 모든 과정을 수행하면서 문제가 발생했을 때, completion 블럭까지 Result, 에러가 전달된다는 점입니다. fail 처리를 위한 if 문을 작성하지 않아도 됩니다. Result 를 사용하지 않는다면 실패한 경우에 대해 if 문에서 NSError 를 생성하고 failure type 을 가진 completion 블럭을 호출하거나, failure completion 블럭을 호출할 것입니다. 성공한 경우에도 success type 과 함께 호출하는 코드를 작성해야 합니다. 성공과 실패는 똑같아 보입니다. 성공하는 경우를 처리하는 코드나 실패한 경우를 처리하는 코드는 동일한 코드입니다. 즉 하나의 코드만으로도 처리할 수 있다는 이야기입니다. 이것이 Result 를 이용한 방식의 장점입니다.

제가 구현한 API 를 사용하는 예제를 보여드리겠습니다. Gaugaes 라고 불리는 서비스 사이트로 연결하는 API 클라이언트를 작성했습니다. 이것은 HTTP 분석 사이트 입니다. 이 사이트 대신에 여러분의 웹사이트를 사용할 수도 있습니다. site ID 를 인자로 갖는 siteGaguge 함수가 있습니다.

func siteGauge(siteID: SiteID) -> HTTPResource<Gauge>
{
  let path = "gauges/\(siteID)"
  return HTTPResource(path: path, parse: parse(rootKey: "gauge"))
}

request(resource: siteGauge(siteID: "my_site_id")) {
 $0.map { site in
   print("Found \(site)")
 }
}

HTTPResource 안의 파라미터 타입으로 정의된 Gauge 는 제가 찾고자 하는 데이터의 실제 객체입니다. 특정한 site ID 에 대한 모든 정보를 요청하고, path 는 이 guage 의 site ID 로 설정했습니다. 그런 다음 siteGauge 함수를 request 함수에 전달하였습니다. request 함수는 특정한 호스트에 사용하기 위한 설정을 마친 상태이며 호스트 기반 URL과 site gauge URL 을 인자값과 함께 연결해줄 것입니다. 이는 또한 이전 함수로부터 받은 모든 정보들을 기반으로 필요한 모든 요청을 구성해줄 것입니다. request 함수에서 siteGauge 함수를 호출하게 되면, 비동기적인 데이터 요청으로부터 Result 를 받아 처리할 수 있으며, 해당 Result 는 map 함수를 이용해 풀어서(unwrap) 사용할 수 있습니다.

에러도 이런 방식으로 처리할 수 있습니다. map 을 활용하는 대신 switch 구문을 사용하여 보다 명확하게 처리할 수도 있습니다.

request(resource: siteGauge(siteID: "my_site_id")) {
 result in

 result.map { site in
   print("Found \(site)")
 }
 .mapError { error -> HTTPResourceError in
    print("Error requesting resource: \(error)")
   return error
 }
}

request(resource: siteGauge(siteID: "my_site_id")) {
 result in

 switch result {

  case .success(let site):
    print("Found \(site)”)

  case .failure(let error):
    print("Error getting site: \(error)")
  }
}

개인적으로 map 을 이용한 처리방법을 선호합니다. 왜냐하면 때로는 에러를 처리하고 싶지 않기 때문이죠. 가끔은 에러를 처리하지 않아도 괜찮을 때도 있습니다. 단, 항상 그런 것은 아니기 때문에 꼭 에러 처리를 하시길 바랍니다.

Libraries (20:02)

이런 멋진 기능들을 구현하기 위해 Result 를 다시 작성해야 할까요? 아니요, 그럴 필요는 없습니다. Result 를 활용하고 싶다면, GitHub 를 참고하시기 바랍니다. 만약 제가 작성한 CoreHTTP 라이브러리에 대해 궁금하다면 GitHub 저장소를 방문해 주세요.

다음: Realm Obj-C와 Realm Swift의 새로운 기능을 소개합니다.

General link arrow white

컨텐츠에 대하여

2016년 11월에 진행한 try! Swift NYC 행사의 강연입니다. 영상 녹화와 제작, 정리 글은 Realm에서 제공하며, 주최 측의 허가 하에 이곳에서 공유합니다.

Saul Mora

Trained in the mystical and ancient arts of manual memory management, compiler macros and separate header files. Saul Mora is a developer who honors his programming ancestors by using Optional variables in swift on all UIs created from Nib files. Despite being an Objective C neckbeard, Saul has embraced the Swift programming language. Currently, Saul resides in Shanghai China working at 流利说 (Liulishuo) helping Chinese learn English while he is learning 普通话 (mandarin).

4 design patterns for a RESTless mobile integration »

close