제목: Vapor and its callback and throwing stacks

이 글은 스위프트로 쓰여진 서버 프레임워크, Vapor에 관한 이야기이다. 나는 iOS 개발자이며, 스위프트를 사랑하기때문에 내 개인 블로그를 이것으로 바꾸려고 하였다. 그렇게 하면서 내가 발견한 몇몇가지 특징들(미들웨어, 콜백체인, 에러핸들링)을 소개해주고 싶었다. 우리는 계층을 만들고 기능을 확장하고 프로젝트를 더 구조화하기위해 프로토콜과 익스텐션 사용을 할 것이다.

다소 간단한 서버로 시작해보자.
let drop = Droplet()  // 1



try? addProvider(VaporPostgreSQL.Provider.self) // 2

drop.preparations += Model.self // 3


drop.middleware.insert(RedirectMiddleWare(), at: 0) // 4

drop.middleware.append(GzipMiddleWare()) // 5

drop.middleware.append(HeadersMiddleware())

drop.get("/test", handler: {request in

    print("4")
    return try JSON(node: "This is a test page.")
})
Droplet 설정

droplet을 만듦으로서 시작하고(1), 데이터베이스 프로바이더를 추가하며(2) 함께 사용될 Model 엔티티를 추가한다. 그런다음 우리의 미들웨어를 추가한다. 미들웨어의 순서는 보통 상관으므로 append하면 된다. 그러나 가끔 구체적인 요구나 미들웨어중 하나를 가장 먼저 실행해야하는 경우가 있을 수 있다. 이 경우는 insert하여 스택의 제일 첫번째에 넣을 수 있다(4). 그리하여 내부에 다른 것들 이전에 첫번째것이 호출된다.

이제 미들웨어로가서, 이것이 어떻게 동작하는지 그리고 왜 이렇게 추가했는지 보자. Vapor는 Middleware라는 프로토콜을 제공하는데, 이것은 그냥 하나의 메소드를 필요로 한다.
func respond(to request: Request, chainingTo next: Responder) throws -> Response
미들웨어의 유일한 메소드


이 메소드를 통해 미들웨어는 요청을 받고, 적절하게 수정하여(혹은 그대로두어), 그것을 다음 응답자(next responder)에 보낸다. 이 응답자는 또다른 미들웨어일 수도 있고 엔드포인트의 핸들러일 수도 있다. 이 응답자는 응답을 반환하고, 적절하게 수정하여(혹은 그대로두어), 직접 응답을 생성하고 반환한다. 아래 예제에서 그것에대한 각각의 상황을 확인할 수 있다.

먼저 스택에서 RedirectMiddleware이다.
struct RedirectMiddleware: Middleware {
    func respond(to request: Request, chainingTo next: Responder) throws -> Response {
        print("1")
        guard request.uri.scheme != "https" else { // 1

            let response = try next.respond(to:request) // 2

            print("7")
            return response // 3

        }

        let uri = uriWithSecureScheme

        print("alternate")
        return Response(redirect:uri.description) // 4

    }
}
RedirectMiddleware

이 미들웨어의 유일한 목표는 현재 요청이 안전한지 확인하는 것이다(1). 안전하지 않으면 요청 URIhttps 스킴을 넣어 새로운 응답을 만들어 반환한다. 만약 안전하면 다음 응답자에게 보내고(2), 끝난다. 우리에게 반환된것이 어떤것이든 반환한다(3). 모든 요청이 안전하고싶기 때문에 모든 다른 미들웨어를 통해 요청이 통과될 필요가 없다. 그러므로 0번째 인덱스에 위치시킨다.

다음 미들웨어는 HeaderMiddleware이다.
struct HeadersMiddleware: Middleware {

    funcrespond(torequest:Request,chainingTonext:Responder)throws->Response{
        print("2")
        let response=try next.respond(to:request) // 1

        print("6")

        response.headers["Cache-Control"] = "public, max-age=86400" // 2


        // Disable the embedding of the site in an external one.

        response.headers["Content-Security-Policy"] = "frame-ancestors 'none'"
        response.headers["X-Frame-Options"]  ="DENY"

        return response// 3

    }

}
HeadersMiddleware

이것은 요청을 바로 다음 응답자에게 보내고(1), 응답이 리턴된 후에 여기에 몇 헤더를 설정하고(2), 반환한다(3).

이 스택의 다음으로 다음 응답자인 GzipMiddleware이다.
struct GzipMiddleware: Middleware {
    func respond(torequest: Request, chainingTo next: Responder) throws->Response {
        print("3")
        let response = try next.respond(to: request) // 1

        print("5")

        response.body = gzippedBody // 2

        response.headers["Content-Encoding"] = "gzip"// 3

        response.headers["Content-Length"] = gzippedBody.length

        returnresponse // 4

    }

}
GZipMiddleware

여기서도 바로 다음 리스폰더에게 응답을 보내고(1), 몇몇 헤더를 추가한다(3). 그러나 다른점은 이것을 리턴하기 전에(4) 바디를 바꾼다.

내부적으로 미들웨어는 어떻게 동작할까? 기본적으로 우리가 이것을 사용한 것처럼 된다. DropletResoonder 프로토콜을 따르는데, 이것은 그냥 한 메소드를 가지고 있다.
func respond(to request: Request) throws -> Response
Droplet의 응답 메소드

Middleware 프로토콜과 비교해보면, 이것은 아무거나 연결(chain)하여 호출될 수 없고 그 구현에서 모든 미들웨어를 연결한다.
extension Droplet: Responder {

    public func respond(to request: Request) throws -> Response {

        [...]

        print("0")
        let mainResponder = middleware.chain(to: routerResponder) // 1

        var response: Response


        do {
            response = try mainResponder.respond(to: request) // 2

        }
        catch {
            return Response(status: .internalServerError, headers: [:], body: "Error message".bytes)// 3

        }

        print("10")
        returnresponse// 4

    }

}
Droplet의 respond 메소드 구현

모든 미들웨어를 연결하여 만들어진 응답자(1)를 볼 수 있다(아직 호출되진 않았다). 그리고 요청의 바깥에 응답을 만드는데 사용되며(2), 모든게 괜찮으면 리턴될 것이다(4). 만약 실패하면 새로운 응답이 만들어지고 반환된다(3). 여기에는 에러메시지와 상태코드가 담겨있다. 미들웨어는 어떻게 연결되고(1) 호출될까? Collection을 익스텐션하고 Responder를 상속하여 이런 한 목표는 클로저를 잡아두고 호출하는 것이다.
extension Request {
    public struct Handler: Responder {
        public typealias Closure = (Request) throws -> Response


        private let closure: Closure


        public init(_c losure: @escaping Closure) {
            self.closure = closure // 1

        }

        public func respond(to request: Request) throws -> Response {
            return try closure(request) // 2

        }
    }
}


extension Collection where Iterator.Element == Middleware {

    func chain(to responder: Responder) -> Responder {
        return reversed().reduce(responder) { nextResponder, nextMiddleware in // 3

            return Request.Handler { request in

                return try nextMiddleware.respond(to: request, chainingTo: nextResponder) // 4

            }
        }
    }

}
Request와 Collection을 익스텐션하기

chain 메소드는 모든 미들웨어를 뒤집고(3) 각 단계에 새로운 Handler를 생성하고 반환한다. 이것은 요청과 현재 응답을 보냄으로서 앞에서 말한 Middleware 프로토콜의 메소드를 호출한다. 이렇게하여 우리의 미들웨어 스택(쉽게 설명하기위해 줄인말)을 기억한다면 [Redirect, Headers, Gzip]는 아래처럼 chain메소드를 통해 갈것이다.
reverse ->
[Gzip, Headers, Redirect] ->
create and return a Handler, that in its closure calls Gzip's respond(to: request, chainingTo: mainResponder) ->
create and return a Handler, that in its closure calls Headers' respond(to: request, chainingTo: gzipResponder) ->
create and return a Handler, that in its closure calls Redirect's respond(to: request, chainingTo: headersResponder) ->
return the Redirect Handler
미들웨어의 클로저를 연결하기

Dropletrespond 메소드는 반환된 Handlerclosure가 호출된 곳에 위치한다(2). 이것은 다음 그리고 다음을 호출할 것이다. 마지막으로 호출될 것은 get 메소드에서 전달된 Handler 클로저 이고, 이것은 응답/에러를 다시 연결한 처음의 것이다.
drop.get("/test", handler: { request in

    print("4")
    return try JSON(node: "This is a test page.")
})
get의 핸들러

마지막으로 모두 합치고 그 chain을 따라가보자. 안전한 요청은 이런식으로 보내진다.
create the main responder ("0") ->
RequestMiddleware ("1") ->
Other, internal middleware ->
HeadersMiddleware ("2") ->
GZipMiddleware ("3") ->
MiscMiddleware1 ->
MiscMiddleware2
안전한 요청 chain

그리고 응답은 이런식으로 보내진다.
get's handler ("4") ->
GZipMiddleware ("5") ->
HeadersMiddleware ("6") ->
Other, internal middleware ->
RequestMiddleware ("7") ->
response is sent to client ("10")
응답 chain

안전하지않은 요청은 아래처럼 약간 긴 경로를 따라간다.
get("/test") ->
RequestMiddleware ("1") ->
RequestMiddleware ("alternate") ->
start over ->
RequestMiddleware ("1") ->
[continue with the rest of the secure path]
안전하지 않은 요청 chain

여러분도 보았듯, 미들웨어의 주된 장점은 특화된 클래스로 더 작게 쪼개어 코드를 모듈화 시킬 수 있다는 점이다. 각각이 보통 하나의 작은 목적을 제공하는 덕분에, 더 쉬워지고 더 표현력있고 테스트/변경/제고하기 더 쉬워질 수 있다. 이것은 서로 전혀 모르며 며,이것들은 모두 체인의 위 아래에서 무슨일이 일어나는지 모른체 독립적으로 요청/응답이 동작한다.

체인을 넘기는 것은 정확히 같은 경로를 따르며, 굉장히 직관적이다. 만약 무엇이든 에러를 던졌는데 처리하지 않았다면 헨들러가 발견될때까지 요청/응답 체인에 돌려 보내거나 제네릭 에러 메시지와 함께 만들어진 Response에 내부 캐치(catch) 블럭(extension Droplet: Responder 예제코드에 있는 3)에 도달하게 될 것이다.

어떻게 에럴를 처리할 수 있는지 한번 보자. 먼저, 가장 직관적인 그 자리에서 처리하는 방법이다.
extension String: Error { } // 1


drop.get("/handle", handler: { request in

    do {
        let answer = try findAnswer(in: request)
        returnJSON(answer) // 2

    }
    catch {
       return JSON("Answer couldn't be computed.") // 3

    }
})

drop.get("/pass-along", handler: { request in

   let answer = try findAnswer(in: request) // 4

   return JSON(answer) // 5

})


func findAnswer(for request: Request) throws->String {
   guard let answer = request.extract("answer") as? String else {
       throw "Answer parameter is not present." // 6

   }

  guard computingFinishedFast else {
       throw "Computing took too long to finish."
   }

   return answer
}
문자열 던지기

StringError를 따르게 하여(1) , 연관 값으로 만든 열거형을 만들어서 Error를 따르게 할 수 있으면 문자열을 던질 수 있다.

handle 엔드포인트 경우에, findAnswer에서 잘 처리했다면 answer로 만들어낸 JSON을 반환한다(2). 뭔가 문제가 있다면 에러를 캐치하고 에러로 만들어낸 JSON을 반환한다.

pass-along 엔드포인트의 경우,  모든것이 잘 처리되었다면 같은 시나리오대로 (5)를 적용하고, 에러가 던져졌다면 여기서 처리하지 않으므로 앞에서 언급한대로 콜백체인으로 보내질 것이다.

앞에서 본 것 처럼, findAnswer 메소드의 사용할 다른 엔드포인트를 추가하면 계속계속 에러를 다뤄야 할 것이다.

Vapor의 문서에서 추천해주는 다른 방법으로는, 모든 에러를 처리해주는 에러 처리 미들웨어를 만드는 것이다. AppError 엔티티를 만들고 findAnswer 메소드를 바꿈으로서 시작해보자.
enum AppError: Error {
    case argumentNotPresent(name: String)
    case computingTimedOut
}

drop.get("/handle", handler: { request in

    do {
        let answer = try findAnswer(in: request)
        return JSON(answer) // 1

    }
    catch {
        return JSON("Answer couldn't be computed.") // 2

    }
})

drop.get("/pass-along", handler: { request in

    let answer = try findAnswer(in: request) // 3

    return JSON(answer) // 4

})


func findAnswer(for request: Request) throws -> String {
    guard let answer = request.extract("answer") as? String else {
        throw AppError.argumentNotPresent(name:"answer") // 5

    }

    guard computingFinishedFast else {
        throw AppError.computingTimedOut // 6

    }

    return answer
}
커스텀 에러 던지기

여전히 그 자리에서 에러를 다룰 수 있지만(1)(2), 에러를 다루기위한 미들웨어를 추가할 것이기 때문에 이것들을 함께 던져(3)(4) 어디로 갈지 정할 수 있다. 이제 findAnswer메소드는 기대하는 파라미터를 위해 연관 값과함께 AppError를 던지며(5), 타임아웃을 위한 에러도 던진다(6).

이미 알고 있듯, 한 메소드만 구현하면 되는데, 아래에서 어떻게 하는지 확인해보자.
struct ErrorHandlingMiddleware: Middleware {

    func respond(to request: Request, chainingTo next: Responder) throws -> Response {
        do {
            return try next.respond(to: request) // 1

        }
        catch AppError.argumentNotPresent(let name) { // 2

            throw Abort.custom( // 3

                status: .badRequest,
                message: "Argument \(name) was not found." // 4

            )
        }
        catch AppError.computingTimedOut { // 5

              throw Abort.custom(
                status: .requestTimeout, // 6

                message: "Computing an answer has timed out."
            )
         }
         catch { // 7

                return Response( // 8

                    status: .serverError,
                    message: "Something unexpected happened."
                )
          }
    }
}
ErrorHandlingMiddleware

우리가 해야할 일은 do-catch 블럭 안에서 다음 응답자에 요청을 보내주는 일만 하면 된다. 만약 모든 처리가 잘 되었다면(1), 요청은 get 헨들러에 도달할 것이고 응답은 우리에게 돌아온다(1). 그리고 체인을 따라 간 것을 반환할 수 있다(1).

findAnswer 메소드에서 뭔가 잘못되었다면 우리 스스로 캐치할 수 있게 만들수 있고 특정의 Abort 에러들을 만들어서 던질 수 있다(3). Abort는 Vapor에서 제공하는 열거형일 수도 있고 Response를 생성해서 반환할 수도 있다(8).

연관 값과함께 에러를 캐치하는 것은 우리에게 어떤 유연함을 제공해 주는데, 보이지 않는 파라미터를 추출해내거나(4) 다른 파라미터에 같은 에러를 던질 수 있는 가능성을 열어준다.
func findMeaningOfLife(for request: Request) throws -> Int {
    guard let answer = request.extract("meaningOfLife") as? Int else {
        throw AppError.argumentNotPresent(name: "meaningOfLife")
    }

    return 42
}
연관 값과 함께 에러 던지기

이제 적절한 상태와 함께(6) 개별적인 타임아웃 에러를 다룰 수 있게 되었다(5). 그리고 serverError를 반환하는 곳에 나머지 모든 것을 위한 catch 콜백도 가질 수 있다. 이 serverError는 제네릭 메시지이다.

마지막으로 droplet에  우리의 새 미들웨어를 추가한다.
// [...]
drop.middleware.append(ErrorHandlingMiddleware())
// [...]
에러 처리 미들웨어 추가하기

한가지만 더, 미들웨어의 괜찮은 기능을 소개하고 싶다. 서버당 설정(per-server configuration)이다. ErrorHandlingMiddleware를 제품판(production)에서만 동작하게 하고 싶다고 가정하자. 어떤 이유로 우리앱에서 부분적으로 크래쉬를 내고 싶다고 하자. 우리가 지금까지 봐온 방식대로 미들웨어를 붙이는 것 대신에, droplets은 설정파일을 제공하며, 아래와같이 사용할 수 있다.

먼저, 미들웨어(middleware)로 붙이는 것 대신 컨피겨레이블(configurable)로 미들에웨를 추가하자.
// [...]
// drop.middleware.append(ErrorHandlingMiddleware()) -> replaced with:
drop.addConfigurable(middleware: ErrorHandlingMiddleware(), name: "error-handling")
// [...]
컨피겨레이블 미들웨어 추가하기

Config/production/droplet.jsonConfig/staging/droplet.json 파일서 적절한 키를 추가하자.
// production/droplet.json
{
   ...
    "middleware": {
        "server": [
            ...
            "error-handling", // 1
            ...
        ],
        "client": [
            ...
        ]
    },
    ...
}

// staging/droplet.json
{
    ...
    "middleware": {
        "server": [
            ... // 2
        ],
        "client": [
            ...
        ]
    },
    ...
}
droplet.json

이제 앱 실행때 ErrorHandlingMiddleware는 제품판 서버의 미들웨어에 추가될 것이지만(1), (2) 단계에서는 아니다. 서버와 클라이언트 두가지 다에 미들웨어를 추가할 수 있고, Config/server-type/droplet.json에도 추가할 수 있다. 또한 그 배열의 순서에 따라 된다.
이 글의 끝에 도달하고 있는 것처럼 스위프트는 서버 세팅을 위한 실용적인 솔루션임을 볼 수 있다. 우리의 요청/응답을 수정하기 쉽게 하기 위해 프로토콜은 Middleware를 정의하게 해주고, 체인된 스택에 핸들러를 추가하는 다양한 방법을 제공한다. 또한 기본 에러 처리를 위해 문자열을 던지는 용도로 익스텐션 하는 것 뿐만 아니라, 프로젝트의 더 나은 구조를 만들기 위해 메인 스트럭쳐에서 Request.Handler를 분리해내는 것도 하였다.

서버와 클라이언트에서 둘 다 스위프트를 사용하면, 중복을 피하면서 모델, 기능, 핼퍼/유틸리티를 공유할 수 있고,  서버와 클라이언트를 전체적인 하나로 봄으로서 모든것을 더 쉽게 만들 수 있다.(역자: 이 문장은 조금 더 의논해볼 필요가 있는 것 같습니다.)

Vapor 그 자체로서, 프레임워크로서 선택인가?(As for Vapor itself, as the framework of choice?) 이 에 따르면 최고도 아니고 최악도 아니다. Vapor는 Express.js와 Sinatra와 비슷한 성격을 제공하며(이것들은 이전에 내 블로그에 쓰인 두 방법이었다.), 내 요구에 잘 들어 맞았다(블로그는 너무 많은 요청이 없으며, API를 필요로 하지 않는다).


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

으로 보내주시면 됩니다.



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

,