라우팅(Routing)#
라우팅은 유입되는 요청(Incomming Request)에 적합한 요청 핸들러(Request Handler)를 찾는 과정입니다. Vapor 라우팅의 핵심에는 RoutingKit의 고성능, 트라이 노드(trie-node) 라우터가 있습니다.
개요#
Vapor에서 라우팅이 어떻게 동작하는지 이해하기 위해서는 먼저 HTTP 요청에 대한 몇 가지 기본 사항을 이해해야 합니다. 다음의 요청 예시를 참고해 주세요.
GET /hello/vapor HTTP/1.1
host: vapor.codes
content-length: 0
이것은 /hello/vapor URL로 보내는 간단한 GET 형태의 HTTP 요청입니다. 브라우저 주소창에서 다음의 URL을 입력했을 때, 브라우저가 보내는 HTTP 요청과 같은 종류입니다.
http://vapor.codes/hello/vapor
HTTP 메서드#
요청의 첫 번째 파트는 HTTP 메서드입니다. 가장 보편적인 HTTP 메서드는 GET입니다. 그러나 여러분이 자주 사용하는 몇 개의 메서드가 더 있습니다. 이 HTTP 메서드들은 CRUD 개념과 관계가 있습니다.
| Method | CRUD |
|---|---|
GET |
읽기 |
POST |
생성 |
PUT |
교체(덮어쓰기) |
PATCH |
부분 수정 |
DELETE |
삭제 |
요청 경로(Request Path)#
HTTP 메서드 바로 뒤에는 요청의 URI가 있습니다. URI는 /로 시작하는 경로와 ? 뒤에 따라오는 선택적인(Optional) 쿼리 스트링으로 구성됩니다. Vapor는 요청을 이 HTTP 메서드와 Path를 사용해서 라우팅합니다.
URI 다음에는 HTTP 버전이 표시됩니다. 그 뒤에는 헤더와 본문(Body)이 올 수 있습니다. GET 요청에는 본문(Body)이 없습니다.
라우터 메서드(Router Methods)#
다음 요청을 Vapor가 어떻게 처리하는지 살펴보겠습니다.
app.get("hello", "vapor") { req in
return "Hello, vapor!"
}
일반적인 모든 HTTP 메서드는 Application의 메서드로 사용 가능합니다. 이 메서드들은 /로 구분되는 하나 또는 그 이상의 문자열 인자의 경로를 받습니다.
참고로 on 뒤에 메서드를 명시하는 방식으로 사용할 수도 있습니다.
app.on(.GET, "hello", "vapor") { ... }
경로가 등록되면, 예시의 HTTP 요청은 다음 HTTP 응답(response)을 반환할 것입니다.
HTTP/1.1 200 OK
content-length: 13
content-type: text/plain; charset=utf-8
Hello, vapor!
경로 파라미터(Route Parameters)#
HTTP 메서드와 경로를 기반으로 요청을 성공적으로 라우팅했습니다. 이제는 동적 경로를 만들어보겠습니다. 이전에는 “vapor”라는 이름이 경로와 응답 모두에 고정되어 있었습니다. 이것을 동적으로 바꿀 수 있습니다. /hello/<any name>으로 변경해서, 입력되는 name에 따라 응답을 받을 수 있습니다.
app.get("hello", ":name") { req -> String in
let name = req.parameters.get("name")!
return "Hello, \(name)!"
}
경로 컴포넌트 앞에 :을 붙이면, 라우터에게 해당 컴포넌트가 동적 컴포넌트 경로임을 나타냅니다. 이 자리에 오는 어떤 문자열이든 이 라우트와 매칭됩니다. req.parameters를 사용해서 해당 문자열의 값에 접근할 수 있습니다.
만약 예시의 요청을 다시 실행한다면 여전히 “Hello, vapor!”라는 요청을 받을 것입니다. 그러나 이제는 /hello/뒤에 어떤 이름을 추가할 수 있고, 그 이름이 포함된 응답을 볼 수 있을 것입니다. /hello/swift로 요청을 보내보세요.
GET /hello/swift HTTP/1.1
content-length: 0
HTTP/1.1 200 OK
content-length: 13
content-type: text/plain; charset=utf-8
Hello, swift!
이제 기본적인 것들을 살펴보았습니다. 각 섹션을 통해서 파라미터, 그룹 등 더 많은 것을 알아보세요.
라우트(Routes)#
라우트는 주어진 HTTP 메서드와 URI 경로에 대한 요청 핸들러를 지정합니다. 또한, 추가적인 메타데이터를 저장할 수 있습니다.
메서드(Methods)#
다양한 HTTP 메서드 헬퍼를 사용해서 Application에 라우트를 직접 등록할 수 있습니다.
// responds to GET /foo/bar/baz
app.get("foo", "bar", "baz") { req in
...
}
라우트 핸들러는 ResponseEncodable를 준수하는 모든 타입을 반환할 수 있습니다. 여기에는 Content, async 클로저, 그리고 미래의 결괏값이 ResponseEncodable을 준수하는 EventLoopFuture가 포함됩니다.
in 앞에 -> T를 사용해서 라우트의 반환 타입을 지정할 수 있습니다. 반환 타입을 컴파일러가 결정할 수 없는 상황에서 유용하게 사용할 수 있습니다.
app.get("foo") { req -> String in
return "bar"
}
지원하는 라우트 헬퍼 메서드는 다음과 같습니다.
getpostpatchputdelete
HTTP 메서드 헬퍼 이외에 on 함수도 있습니다. on 함수는 HTTP 메서드를 인자로 전달할 수 있습니다.
// responds to OPTIONS /foo/bar/baz
app.on(.OPTIONS, "foo", "bar", "baz") { req in
...
}
경로(Path Component)#
각 라우트 등록 메서드는 PathComponent 리스트를 가변 인자 형태로 받습니다. 이 타입은 문자열 리터럴로 표현 가능하고, 네 가지 케이스가 있습니다.
Constant (
foo)Parameter (
:foo)Anything (
*)Catchall (
**)
상수(Constant)#
상수는 정적 경로 컴포넌트입니다. 해당 위치에 문자열이 정확히 일치하는 요청만 허용됩니다.
// responds to GET /foo/bar/baz
app.get("foo", "bar", "baz") { req in
...
}
파라미터(Parameter)#
동적 경로 컴포넌트입니다. 해당 자리의 어떤 문자열이든 허용합니다. :를 접두사를 사용하여 파라미터 경로 컴포넌트를 명시합니다. :뒤의 문자열은 파라미터의 이름으로 사용됩니다. 해당 이름을 사용해서 요청의 파라미터 값을 가져올 수 있습니다.
// responds to GET /foo/bar/baz
// responds to GET /foo/qux/baz
// ...
app.get("foo", ":bar", "baz") { req in
...
}
Anything#
Anything 컴포넌트는 파라미터 컴포넌트와 비슷합니다. 하지만, 값을 버린다는 점에서 차이가 있습니다. Anything 컴포넌트는 *로 명시할 수 있습니다.
// responds to GET /foo/bar/baz
// responds to GET /foo/qux/baz
// ...
app.get("foo", "*", "baz") { req in
...
}
Catchall#
Catchall은 하나 또는 그 이상의 컴포넌트와 매치되는 동적 경로 컴포넌트입니다. **을 사용해서 명시할 수 있습니다. 해당 위치 또는, 그 이후 위치에 오는 모든 문자열이 이 요청에 매치됩니다.
// responds to GET /foo/bar
// responds to GET /foo/bar/baz
// ...
app.get("foo", "**") { req in
...
}
Parameters#
(접두사 :와 함께) 파라미터 경로 컴포넌트를 사용할 때, 해당 위치의 URI 값이 req.parameters에 저장됩니다. 경로 컴포넌트의 이름을 사용해서 값에 접근할 수 있습니다.
// responds to GET /hello/foo
// responds to GET /hello/bar
// ...
app.get("hello", ":name") { req -> String in
let name = req.parameters.get("name")!
return "Hello, \(name)!"
}
Tip
라우트 경로에 :name이 포함되어 있다면 req.parameters.get에는 nil이 절대로 반환되지 않을 것입니다. 하지만, 만약 미들웨어나 여러 라우트들에서 공통적으로 사용하는 코드가 있다면, 라우트 파라미터에 접근할 때 nil이 반환될 가능성이 있습니다. 이를 고려한 작업이 필요합니다.
Tip
예를 들어 /hello/?name=foo 같은 URL에서 쿼리 파라미터를 가져오려면, Vapor의 Content API를 사용해야 합니다. URL 쿼리 스트링 안에서 URL 인코딩 데이터를 처리할 수 있습니다. 더 자세한 정보를 위해서 Content reference를 살펴보세요.
req.parameters.get은 LosslessStringConvertible 타입을 준수하는 타입으로 자동 캐스팅합니다.
// responds to GET /number/42
// responds to GET /number/1337
// ...
app.get("number", ":x") { req -> String in
guard let int = req.parameters.get("x", as: Int.self) else {
throw Abort(.badRequest)
}
return "\(int) is a great number"
}
Catchall (**)로 매치된 URI 값은 [String]으로 req.parameters에 저장됩니다. req.parameters.getCatchall을 사용해서 이 컴포넌트들에 접근할 수 있습니다.
// responds to GET /hello/foo
// responds to GET /hello/foo/bar
// ...
app.get("hello", "**") { req -> String in
let name = req.parameters.getCatchall().joined(separator: " ")
return "Hello, \(name)!"
}
본문 스트리밍(Body Streaming)#
on 메서드를 사용해서 라우트를 등록할 때, 요청 본문(Body)을 어떻게 처리할지 지정할 수 있습니다. 요청 본문들은 핸들러를 요청하기 전에 기본적으로 메모리에 수집됩니다. 애플리케이션에 들어오는 요청이 비동기적으로 읽히더라도, 동기적으로 요청 콘텐츠 디코딩을 수행할 수 있게 해주는데 유용합니다.
Vapor는 기본적으로 스트리밍 본문을 16KB로 제한합니다. app.routes를 사용해서 설정할 수 있습니다.
// 스트리밍 본문 제한을 500kb로 증가시킵니다.
app.routes.defaultMaxBodySize = "500kb"
만약 수집된 스트리밍 본문이 설정된 제한을 초과하면 413 Payload Too Large 에러가 반환됩니다.
각각의 라우트마다 요청 본문 수집 전략을 설정하려면 body 파라미터를 사용하세요.
// 라우트가 실행되기 전, 스트리밍 본문을 최대 1mb로 수집합니다.
app.on(.POST, "listings", body: .collect(maxSize: "1mb")) { req in
// Handle request.
}
라우트에 collect 메서드로 maxSize를 전달하면 애플리케이션의 기본값보다 우선되어 적용됩니다. application의 기본값을 사용하려면 maxSize 인자를 생략하세요.
파일 업로드 같은 대용량 요청의 경우, 요청 본문을 버퍼에 수집하는 것은 잠재적으로 시스템 메모리에 부담을 줄 수 있습니다. 요청 본문이 수집되는 것을 막기 위해서는 stream 전략을 사용하세요.
// 요청 본문을 버퍼로 수집하지 않습니다.
app.on(.POST, "upload", body: .stream) { req in
...
}
요청 본문이 스트리밍 될 때, req.body.data는 nil입니다. 경로로 보내지는 각 조각 데이터를 처리하기 위해서는 req.body.drain를 사용해야 합니다.
대소문자를 구분하지 않는 라우팅(Case Insensitive Routing)#
대소문자를 구분하고 유지하는 것은 라우팅의 기본 동작입니다. Constant 경로 컴포넌트는 라우팅의 목적에 따라 대소문자를 구분하지 않으면서 원래의 대소문자 형태를 유지하도록 처리할 수 있습니다. 이 동작을 사용하기 위해서는 application의 시작 전에 다음과 같이 설정하세요.
app.routes.caseInsensitive = true
원래의 요청은 변경되지 않습니다. 라우트 핸들러는 수정되지 않은 요청 경로 컴포넌트를 수신할 것입니다.
Viewing Routes#
app.routes를 사용하거나 Routes 서비스를 생성해서 Application의 라우트에 접근할 수 있습니다.
print(app.routes.all) // [Route]
Vapor는 routes라는 명령어를 제공합니다. 이 명령어는 사용 가능한 모든 라우트들을 ASCII 형식의 테이블로 출력합니다.
$ swift run App routes
+--------+----------------+
| GET | / |
+--------+----------------+
| GET | /hello |
+--------+----------------+
| GET | /todos |
+--------+----------------+
| POST | /todos |
+--------+----------------+
| DELETE | /todos/:todoID |
+--------+----------------+
메타데이터(Metadata)#
모든 라우트 등록 메서드는 생성된 Route를 반환합니다. 이를 통해 라우트의 userInfo Dictionary에 메타데이터를 추가할 수 있습니다. 설명을 추가하는 것처럼 기본적으로 사용할 수 있는 몇 가지 메서드들이 있습니다.
app.get("hello", ":name") { req in
...
}.description("says hello")
라우트 그룹(Route Groups)#
라우트를 그룹화해서 경로 접두사나 특정 미들웨어가 있는 라우트 집합을 생성할 수 있습니다. 그룹화는 빌더와 클로저 기반의 문법을 제공합니다.
모든 그룹화 메서드는 RouteBuilder를 반환합니다. RouteBuilder는 다른 라우트 빌딩 메서드와 함께 제한 없이 혼합, 매치, 중첩시킬 수 있습니다.
경로 접두사(Path Prefix)#
라우트 그룹에 경로 접두사를 사용해서 하나 또는 그 이상의 경로 컴포넌트를 라우트의 그룹 앞에 추가할 수 있습니다.
let users = app.grouped("users")
// GET /users
users.get { req in
...
}
// POST /users
users.post { req in
...
}
// GET /users/:id
users.get(":id") { req in
let id = req.parameters.get("id")!
...
}
get 이나 post 같은 메서드에 전달할 수 있는 경로 컴포넌트는 grouped에도 전달할 수 있습니다. 클로저 기반의 문법으로 사용할 수도 있습니다.
app.group("users") { users in
// GET /users
users.get { req in
...
}
// POST /users
users.post { req in
...
}
// GET /users/:id
users.get(":id") { req in
let id = req.parameters.get("id")!
...
}
}
경로 접두사 라우트 그룹을 중첩해서 CRUD API를 간결히 정의할 수 있습니다.
app.group("users") { users in
// GET /users
users.get { ... }
// POST /users
users.post { ... }
users.group(":id") { user in
// GET /users/:id
user.get { ... }
// PATCH /users/:id
user.patch { ... }
// PUT /users/:id
user.put { ... }
}
}
미들웨어(Middleware)#
경로 컴포넌트를 접두사로 붙이는 것 외에도, 미들웨어를 라우트 그룹에 추가할 수도 있습니다.
app.get("fast-thing") { req in
...
}
app.group(RateLimitMiddleware(requestsPerMinute: 5)) { rateLimited in
rateLimited.get("slow-thing") { req in
...
}
}
서로 다른 인증 미들웨어를 사용해서 라우트들의 일부 하위 부분들을 보호할 수 있습니다.
app.post("login") { ... }
let auth = app.grouped(AuthMiddleware())
auth.get("dashboard") { ... }
auth.get("logout") { ... }
리다이렉션(Redirections)#
리다이렉트는 다양한 시나리오에서 유용합니다. SEO를 위해서 옛날 주소를 새로운 주소로 이동시키거나, 인증이 되지 않은 사용자를 로그인 페이지로 이동시키거나, 새로운 API 버전에서 하위 호환성을 지원하기 위해 사용할 수 있습니다.
리다이렉트를 요청하기 위해서는 아래와 같이 사용하세요.
req.redirect(to: "/some/new/path")
리다이렉트의 유형을 지정할 수도 있습니다. (SEO가 적절하게 업데이트 되도록) 페이지를 영구적으로 리다이렉트 할 수 있습니다.
req.redirect(to: "/some/new/path", redirectType: .permanent)
Redirect의 차이는 다음과 같습니다.
.permanent- 301 Permanent 리다이렉트를 반환합니다..normal- 303 see other 리다이렉트를 반환합니다. Vapor의 기본값은 303입니다. 클라이언트에게 GET 요청으로 리다이렉트를 지시합니다..temporary- 307 Temporary 리다이렉트를 반환합니다. 클라이언트에게 원래의 HTTP 요청을 유지하도록 합니다.
적절한 리다이렉션 상태 코드를 선택하기 위해서는 전체 리스트를 참고하세요.