let's go를 배워보자 7일차 - RESTful Routing

0

lets-go

목록 보기
7/15

RESTful Routing

우리의 app server에 대한 요청을 유연하게 처리할 수 있도로 바꾸자.

  1. GET /snippet/create 요청이 들어오면 새로운 snippet을 추가하기 위한 html을 user에게 보여주도록 한다.
  2. POST /snippet/create 요청이 들어오면 form data를 처리하고 새로운 snippet record를 database안에 저장하도록 한다.

이 과정에서 다른 routes들도 GET 또는 HEAD만 요청이 가능하도록 제한하는 것이 좋다.

MethodPatternHandlerAction
GET/homeDisplay the home page
GET/snippet?id=1showSnippetDisplay a specific snippets
GET/snippet/createcreateSnippetFormDisplay the new snippet form
POST/snippet/createcreateSnippetCreate a new snippet
GET/static/http.FileServerServe a specific static file

또 다른 routing 관련 개선안은 semantic URL(https://en.wikipedia.org/wiki/Clean_URL)을 사용하여 URL path에 어떠한 변수든 포함되도록 하고, query string으로 추가되지 않도록 한다. 다음과 같이 말이다.

MethodPatternHandlerAction
GET/homeDisplay the home page
GET/snippet/:idshowSnippetDisplay a specific snippets
GET/snippet/createcreateSnippetFormDisplay the new snippet form
POST/snippet/createcreateSnippetCreate a new snippet
GET/static/http.FileServerServe a specific static file

이러한 변화는 application routing 구조가 기본적인 REST의 원칙을 따르도록 만들어주며 modern web application에서 일하는 누군가에게 매우 친숙하고 logical하게 다가올 것이다.

1. Installing a Router

그러나, 앞서 말했듯이 go의 servemux는 method based routing 또는 semantic URLs를 제공하지 않는다. 이 때문에 대부분 third-party routing package를 사용하여 도움을 받는다.

이 중에서 유명한 것을 추려보면 다음과 같다.

  • bmizerany/pat: method-base routing을 제공하고, semantic URLs를 위한 지원을 제공한다. 그러나 현재 지원이 종료되었다는 단점이 있다.
  • gorilla/mux: 더욱 더 full-featured된 라이브러리이다. method-base routing 뿐만 아니라, semantic URL도 지원한다. 또한, scheme, host 그리고 header를 기반으로 route를 제공한다. URL에 정규표현식까지도 제공하는데, 해당 패키지의 단점은 상대적으로 느리고 메모리를 많이 잡아먹는다는 것이다. 그러나, database-driven web application의 경우(우리)는 상대적으로 매우 작은 영향이다.

우리의 경우는 굉장히 단순한 웹사이트를 개발하는 것이므로,굳이 gorilla mux를 사용하여 더 좋은 기능들을 사용할 필요가 없다. 그래서 우리는 Pat을 사용하기로 하였다.

go get github.com/bmizerany/pat

두 가지 route package를 추천하였지만 여기에는 더 많은 route package들이 존재한다.

  1. go-zoo/bone: Pat과 비슷한 기능들을 제공하지만 추가적인 편리한 기능들을 제공한다. 그러나 test coverage가 안습이라고 한다.(2019 기준)
  2. julienschmidt/httprouter: radix-tree 기반의 router로 굉장히 유명하다. method-base routing과 semantic URLs를 제공한다. 그러나 wildcard parameter들로 인한 conflicting patterns를 처리하지 못한다. 이는 RESTful 라우팅 스키마를 사용하는 우리의 application에서 치명적인데, /snippet/create/snippet/:id가 충돌할 위험이 크다. 그럼에도 사람들이 선호하는 큰 이유는 기본적으로 제공되는 net/http 패키지와 호환이 잘되기 때문이다.
  3. dimfeld/httptreemux: radix-tree 기반의 router이지만 conflicting patterns로 부터 벗어나기 위해 고안되었다. 단점은 기본 패키지인 net/http와 호환이 잘안된다고 한다.
  4. go-chi/chi: 또 다른 radix-tree 기반의 router로 매우 빠르고, gorilla mux에 견줄만할 정도로 많이 사용되고 있다.

2. Implementing RESTful Routes

bmizerany/pat 패키지에서 route를 등록하고 router를 만드는 문법은 다음과 같다.

mux := pat.New()
mux.Get("/snippet/:id", http.HnadlerFunc(app.showSnippet))

해당 코드에서

  • /snippet/:id 패턴은 named capture:id를 가진다. 나머지 패턴들이 문자 그대로 매치되는 반면에, 이 named capture은 마치 wildcard처럼 동작한다. Pat은 런타임에 동작 중 named capture의 내용(content)을 URL query string에 넣어준다.

  • mux.Get() 메서드는 URL pattern을 등록하고 오직 GET HTTP method를 가지는 request에 대해서만 호출되도록 하는 핸들러를 가진다. Post(), Put(), Delete() 그리고 다른 메서들도 또한 이에 상응하게 제공된다.

  • Pat은 handler function을 직접 등록하도록 하지 않도록 한다. 그래서 http.HandlerFunc() 어댑터를 사용하여 변경해주도록 해야한다.

이제 우리의 routes.go 파일을 Pat을 사용하여 변경해보도록 하자.

  • cmd/web/routes.go
package main

import (
	"net/http"

	"github.com/bmizerany/pat"
	"github.com/justinas/alice"
)

func (app *application) routes() http.Handler {
	standardMiddleware := alice.New(app.recoverPanic, app.logRequest, secureHeaders)

	mux := pat.New()
	mux.Get("/", http.HandlerFunc(app.home))
	mux.Get("/snippet/create", http.HandlerFunc(app.createSnippetForm))
	mux.Post("/snippet/create", http.HandlerFunc(app.createSnippet))
	mux.Get("/snippet/:id", http.HandlerFunc(app.showSnippet))

	fileServer := http.FileServer(http.Dir("./ui/static/"))
	mux.Get("/static/", http.StripPrefix("/static", fileServer))
	
	return standardMiddleware.Then(mux)
}

몇 가지 중요한 점이 적용된 것이 있다.

  • Pat은 pattern들이 등록된 순서대로 매칭된다. 우리의 application에서 GET "/snippet/create" HTTP request은 사실 두 개의 routes로 매칭된다. /snippet/create에도 매칭되지만 wildcard match로 /snippet/:id로도 매칭된다. 왜냐하면 "create"":id"로도 매칭될 수 있기 때문이다. 그래서 정확히 매칭되기 위해서 우리는 wildcard route이전에 우리의 정확한 route를 등록하도록 하여 매칭되도록 하는 것이다.

  • tailing slash로 끝나는 URL 패턴("/static/")들은 go의 build-in servemux와 동일하게 동작한다. 패턴의 start에 매칭되는 어떠한 요청이든 이에 상응하는 핸들러로 dispatch된다.

  • /패턴은 special case로 오직 정확히 /로 요청이 오는 경우만 처리한다.

이를 명심해두고 handlers.go 파일를 수정하도록 하자.

  • cmd/web/handlers.go
func (app *application) home(w http.ResponseWriter, r *http.Request) {
	// Because Pat matches the "/" path exactly, we can now remove the manual check
	// of r.URL.Path != "/" from this handler.

	s, err := app.snippets.Latest()
	if err != nil {
		app.serverError(w, err)
		return
	}

	app.render(w, r, "home.page.tmpl", &templateData{
		Snippets: s,
	})
}

func (app *application) showSnippet(w http.ResponseWriter, r *http.Request) {
	// Pat doesn't strip the colon from the named capture key, so we need to
	// get the value of ":id" from the query string instead of "id".
	id, err := strconv.Atoi(r.URL.Query().Get(":id"))
	if err != nil || id < 1 {
		app.notFound(w)
		return
	}

	s, err := app.snippets.Get(id)
	if err != nil {
		if errors.Is(err, models.ErrNoRecord) {
			app.notFound(w)
		} else {
			app.serverError(w, err)
		}
		return
	}
	app.render(w, r, "show.page.tmpl", &templateData{
		Snippet: s,
	})
}
// Add a new createSnippetForm handler, which for now returns a placeholder response.
func (app *application) createSnippetForm(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("create a new snippet"))
}

func (app *application) createSnippet(w http.ResponseWriter, r *http.Request) {
	// Checking if the request method is a POST is now superfluous and can be
	// removed.
	title := "gyu"
	content := "O snail\nClimb Mount Fuji,\nBut slowly, slowly!\n\n– Kobayashi Issa"
	expires := "7"
	id, err := app.snippets.Insert(title, content, expires)
	if err != nil {
		app.serverError(w, err)
		return
	}
	// Change the redirect to use the new semantic URL style of /snippet/:id
	http.Redirect(w, r, fmt.Sprintf("/snippet/%d", id), http.StatusSeeOther)
}

handlers.go에서는 대부분 이전에 가내 수공업으로 처리했던 부분들을 삭제했다. home에서 url path를 확인했던 부분과, createSnippet에서 method를 확인했던 부분들이 대표적이다. 또한 이제 semantic url를 사용하므로, 이를 적용해야한다.

showSnippet에서 query string을 가져오는 부분은 id가 아니라 :id를 통해서 가져올 수 있는데, Patcolon을 따로 strip해주지 않기 때문이다. 또한, createSnippet에서 redirect을 하기위해서 semantic url로 redirect 처리한 것을 확인할 수 있다.

이제 home.page.tmpl 페이지에서 <td><a href='/snippet?id={{.ID}}'>{{.Title}}</a></td>으로 url 처리했던 부분을 semantic url로 변경해주도록 하자.

  • home.page.tmpl
{{template "base" .}}

{{define "title"}}Home{{end}}

{{define "main"}}
    <h2>Latest Snippets</h2>
    {{if .Snippets}}
    <table>
        <tr>
            <th>Title</th>
            <th>Created</th>
            <th>ID</th>
        </tr>
        {{range .Snippets}}
        <tr>
            <td><a href='/snippet/{{.ID}}'>{{.Title}}</a></td>
            <td>{{humanDate .Created}}</td>
            <td>#{{.ID}}</td>
        </tr>
        {{end}}
    </table>
    {{else}}
        <p>There's nothing to see here... yet!</p>
    {{end}}
{{end}}

완료 후 다시 application을 재시작 한다음 semantic URL이 잘 동작하는 지 확인하도록 하자.

http://10.251.72.203:4000/snippet/1

해당 페이지에 접속하였을 대 원하는 snippet이 나왔다면 대성공이다.

그럼 이제 지원하지 않는 method로 요청을 보내보도록 하자.

curl -I -X POST http://localhost:4000/snippet/1

HTTP/1.1 405 Method Not Allowed
Allow: HEAD, GET
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
X-Frame-Options: deny
X-Xss-Protection: 1; mode=block
Date: Tue, 03 Jan 2023 08:52:38 GMT
Content-Length: 19

showSnippet은 GET만을 지원하지 POST를 지원하지 않는다. 에러가 발생한 것은 성공한 것이다.

0개의 댓글