4. RESTful API 서버 만들기 (1)
페이지 정보
작성자 관리자 댓글 1건 조회 1,624회 작성일 21-07-18 09:05본문
4. RESTful API 서버 만들기 (1)
GoLang에서는 net/http 라는 강력한 HTTP 패키지가 내장되어 있습니다.
이 패키지를 사용하면 Request를 보내는 것부터 받는 것까지 모든 HTTP 통신을 처리할 수 있다.
여기에서도 net/http 패키지를 기본으로 하여 RESTful API 서버를 만듭니다.
또한 웹서버의 기본이라고도 할 수 있는 라우팅은 httprouter 를 사용하였습니다.
일반적으로 RESTful API 서버는 웹기반의 URI를 사용해서 만들어진다.
여기에서도 웹기반의 RESTful API 서버를 만듭니다.
때문에 먼저 HTTP 서버를 열고 각 메소드를 받는 것부터 시작합니다.
먼저 웹서버의 라우팅을 처리할 httprouter 패키지를 다운로드 해야 합니다.
$ go get github.com/julienschmidt/httprouter
간단한 api 서버를 만들어보록 하겠습니다.
[jklee@www golang]$ mkdir simple_api
[jklee@www golang]$ cd simple_api/
[jklee@www simple_api]$ vi go_simple_api.go
package main
import (
"fmt"
"github.com/julienschmidt/httprouter"
"net/http"
"log"
)
func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
fmt.Fprint(w, "Welcome!\n")
}
func Hello(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
fmt.Fprintf(w, "hello, %s!\n", ps.ByName("name"))
}
func main() {
router := httprouter.New()
router.GET("/", Index)
router.GET("/hello/:name", Hello)
log.Fatal(http.ListenAndServe(":8080", router))
}
보시면 사용법이 아주 간단합니다.
httprouter.New()로 라우터를 생성하고, GET, POST, PUT, DELETE 등 대문자로 작성된 HTTP 메소드의 이름을 함수로 호출하면 해당 메소드의 라우팅이 등록됩니다.
만약 POST 메소드를 등록하려면 아래와 같이 하면 등록된다.
router.POST("/some/post", handleSomePost)
컴파일하고 실행해보겠습니다.
[jklee@www simple_api]$ go mod init simple_api
go: creating new go.mod: module simple_api
go: to add module requirements and sums:
go mod tidy
[jklee@www simple_api]$ go mod tidy
go: finding module for package github.com/julienschmidt/httprouter
go: downloading github.com/julienschmidt/httprouter v1.3.0
go: found github.com/julienschmidt/httprouter in github.com/julienschmidt/httprouter v1.3.0
[jklee@www simple_api]$ go install
[jklee@www simple_api]$ simple_api
simple_api를 수정해보도록 하겠습니다.
HTTP 기반의 RESTful API 서버에서는 Get, Post, Put, Delete의 네 가지 메소드를 사용합니다.
이를 인터페이스로 만들어 보겠습니다.
type Response struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
}
type Resource interface {
Uri() string
Get(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) Response
Post(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) Response
Put(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) Response
Delete(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) Response
}
이 리소스의 URI를 반환할 Uri() 함수, 각 메소드에 대응되는 핸들러들이 정의되어 있는 인터페이스 입니다.
하지만 리소스가 네 가지 메소드를 모두 지원하지 않을 수도 있죠. 그러니 각 메소드에 맞게 NotSupported를 만들어줍니다.
type (
GetNotSupported struct{}
PostNotSupported struct{}
PutNotSupported struct{}
DeleteNotSupported struct{}
)
func (GetNotSupported) Get(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) Response {
return Response{405, "", nil}
}
func (PostNotSupported) Post(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) Response {
return Response{405, "", nil}
}
func (PutNotSupported) Put(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) Response {
return Response{405, "", nil}
}
func (DeleteNotSupported) Delete(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) Response {
return Response{405, "", nil}
}
만약 "/hello"라는 URI로 Get 메소드만 지원하는 API를 만들고 싶다면 아래와 같이 사용하면 될 것입니다.
type HelloResource struct {
PostNotSupported
PutNotSupported
DeleteNotSupported
}
func (HelloResource) Uri() string {
return "/hello"
}
func (HelloResource) Get(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) Response {
return Response{200, "message", map[string]interface{}{
"key1": "value1",
}}
}
이렇게 만들어진 API는 아래와 같이 등록할 수 있습니다.
router.GET(HelloResource.Uri(), HelloResource.Get)
router.POST(HelloResource.Uri(), HelloResource.Post)
router.PUT(HelloResource.Uri(), HelloResource.Put)
router.DELETE(HelloResource.Uri(), HelloResource.Delete)
그런데 보기엔 좋은 것 같지만 여전히 반복적인 코드가 생겨납니다.
인터페이스도 만들었으니, 함수를 만들어보겠습니다.
func HttpResponse(rw http.ResponseWriter, req *http.Request, res Response) {
content, err := json.Marshal(res)
if err != nil {
abort(rw, 500)
}
rw.WriteHeader(res.Code)
rw.Write(content)
}
func AddResource(router *httprouter.Router, resource Resource) {
fmt.Println("\"" + resource.Uri() + "\" api is registerd")
router.GET(resource.Uri(), func(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) {
res := resource.Get(rw, r, ps)
HttpResponse(rw, r, res)
})
router.POST(resource.Uri(), func(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) {
res := resource.Post(rw, r, ps)
HttpResponse(rw, r, res)
})
router.PUT(resource.Uri(), func(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) {
res := resource.Put(rw, r, ps)
HttpResponse(rw, r, res)
})
router.DELETE(resource.Uri(), func(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) {
res := resource.Delete(rw, r, ps)
HttpResponse(rw, r, res)
})
}
이제 HelloResource를 등록하려면 아래와 같이 단 한 줄만 넣으면 됩니다.
AddResource(router, new(HelloResource))
전체 소스는 다음과 같습니다.
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"github.com/julienschmidt/httprouter"
)
type Response struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
}
type Resource interface {
Uri() string
Get(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) Response
Post(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) Response
Put(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) Response
Delete(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) Response
}
type (
GetNotSupported struct{}
PostNotSupported struct{}
PutNotSupported struct{}
DeleteNotSupported struct{}
)
func (GetNotSupported) Get(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) Response {
return Response{405, "", nil}
}
func (PostNotSupported) Post(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) Response {
return Response{405, "", nil}
}
func (PutNotSupported) Put(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) Response {
return Response{405, "", nil}
}
func (DeleteNotSupported) Delete(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) Response {
return Response{405, "", nil}
}
func abort(rw http.ResponseWriter, statusCode int) {
rw.WriteHeader(statusCode)
}
func HttpResponse(rw http.ResponseWriter, req *http.Request, res Response) {
content, err := json.Marshal(res)
if err != nil {
abort(rw, 500)
}
rw.WriteHeader(res.Code)
rw.Write(content)
}
func AddResource(router *httprouter.Router, resource Resource) {
fmt.Println("\"" + resource.Uri() + "\" api is registerd")
router.GET(resource.Uri(), func(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) {
res := resource.Get(rw, r, ps)
HttpResponse(rw, r, res)
})
router.POST(resource.Uri(), func(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) {
res := resource.Post(rw, r, ps)
HttpResponse(rw, r, res)
})
router.PUT(resource.Uri(), func(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) {
res := resource.Put(rw, r, ps)
HttpResponse(rw, r, res)
})
router.DELETE(resource.Uri(), func(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) {
res := resource.Delete(rw, r, ps)
HttpResponse(rw, r, res)
})
}
// user string array
var users = []string{
"user1", "user2", "user3",
}
// /user
type UserResource struct {
PutNotSupported
DeleteNotSupported
}
func (UserResource) Uri() string {
return "/user"
}
func (UserResource) Get(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) Response {
return Response{200, "", users}
}
func (UserResource) Post(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) Response {
user := r.FormValue("user")
if len(user) > 0 {
users = append(users, r.FormValue("user"))
return Response{200, "", nil}
} else {
return Response{400, "", nil}
}
}
// /user/:index
type UserEachResource struct {
PostNotSupported
}
func (UserEachResource) Uri() string {
return "/user/:index"
}
func (UserEachResource) Get(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) Response {
if index, err := strconv.Atoi(ps.ByName("index")); err == nil {
if len(users) > index && index > 0 {
return Response{200, "", users[index]}
}
}
return Response{400, "", nil}
}
func (UserEachResource) Put(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) Response {
user := r.FormValue("user")
if len(user) > 0 {
if index, err := strconv.Atoi(ps.ByName("index")); err == nil {
if len(users) > index && index > 0 {
users[index] = user
return Response{200, "", nil}
}
}
return Response{400, "", nil}
} else {
return Response{400, "", nil}
}
}
func (UserEachResource) Delete(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) Response {
if index, err := strconv.Atoi(ps.ByName("index")); err == nil {
if len(users) > index && index > 0 {
users = append(users[:index], users[index+1:]...)
return Response{200, "", nil}
}
}
return Response{400, "", nil}
}
func main() {
router := httprouter.New()
AddResource(router, new(UserResource))
AddResource(router, new(UserEachResource))
log.Fatal(http.ListenAndServe(":8080", router))
}
실행 해보록 하겠습니다.
소스를 수정하고 컴파일 후, 다시 서버를 구동해 줍니다.
[jklee@www simple_api]$ go install
[jklee@www simple_api]$ simple_api
"/user" api is registerd
"/user/:index" api is registerd
웹브라우저에서 확인해봅니다.
댓글목록
관리자님의 댓글
관리자 작성일
[jklee@www ~]$ curl -X GET --header 'Accept: application/json' 'http://localhost:5001/hello/Jinkwan'
hello, Jinkwan!
[jklee@www ~]$