디스커버리 Go 6장 - 웹 애플리케이션 작성하기
이 포스트는 디스커버리 GO 언어
6장을 읽고 정리한 것이며 작업 환경은 Ubuntu 16.04
입니다. 소스를 기반으로 설명하는 형태로 진행합니다.
중간에 필요한 경우 첨언합니다.
6.1 Hello, 세계!
브라우저로 http://localhost:8080으로 접속하면 “Hello, 세계!”를 보여주는 예제입니다.
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, 세계!")
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
curl로도 확인할 수 있습니다.
$ curl http://localhost:8080
Hello, 세계!
6.2 할 일 목록 관리 웹 앱 만들기
할 일 목록을 관리하는 웹 앱을 만들어보겠습니다. 이름은 TaskMan 입니다.
- 끝까지 구현하지는 않습니다.
6.2.1 RESTful API
- GET, PUT, POST, DELETE 메소드를 이용하여 어떤 동작을 취할 지 지정하는 API(자세한 설명은 생략한다)
- http://example.com/abc/def?q=ghi 에서 /abc/def 는 URL 경로, q=ghi 는 쿼리가 됩니다.
- 각각의 resource에는 ID가 부여됩니다(/task/123)
6.2.2 Data Access Object
DAO^Data ^Access ^Object 란 데이터베이스에 필요한 연산들을 처리하기 위해서 만드는 추상 인터페이스입니다.
- 사용하는 데이터베이스와 비지니스 로직을 분리할 수 있는 방법
package task
// ID is a data type to identify the task.
type ID string
// Accessor is an interface to access the tasks.
type Accessor interface {
Get(id ID) (Task, error)
Put(id ID, t Task) error
Post(t Task) (ID, error)
Delete(id ID) error
}
Accessor interface 를 상속받는 MemoryDataAccess 를 구현합니다.
package task
import (
"errors"
"fmt"
)
type MemoryDataAccess struct {
tasks map[ID]Task
nextID int64
}
func NewMemoryDataAccess() Accessor {
return &MemoryDataAccess{
tasks:map[ID]Task{},
nextID: int64(1),
}
}
var ErrTaskNotExist = errors.New("task does not exist")
func (m *MemoryDataAccess) Get(id ID) (Task, error) {
t, exists := m.tasks[id]
if !exists {
return Task{}, ErrTaskNotExist
}
return t, nil
}
func (m *MemoryDataAccess) Put(id ID, t Task) error {
if _, exists := m.tasks[id]; !exists {
return ErrTaskNotExist
}
m.tasks[id] = t
return nil
}
func (m *MemoryDataAccess) Post(t Task) (ID, error) {
id := ID(fmt.Sprint(m.nextID))
m.nextID++
m.tasks[id] = t
return id, nil
}
func (m *MemoryDataAccess) Delete(id ID) error {
if _, exists := m.tasks[id]; !exists {
return ErrTaskNotExist
}
delete(m.tasks, id)
return nil
}
6.2.3 RESTful API 핸들러 구현
GET /api/v1/task/{id}
PUT /api/v1/task/{id}
task: {task}
POST /api/v1/task/
task: {task}
DELETE /api/v1/task/{id}
response.go
Client의 request에 돌려주는 Response 객체를 정의합니다.
이 Response 객체를 json으로 변환하기 위한 것들을 정의해줍니다.
- JSON tag를 Response 객체에 정의 `json:”task”`
- ResponseError 객체에 MarshalJSON(), UnmarshalJSON() 를 정의
package main
import (
"fmt"
"encoding/json"
"errors"
"github.com/amazingguni/gogo/task"
)
type ResponseError struct {
Err error
}
func (err ResponseError) MarshalJSON() ([]byte, error) {
if err.Err == nil {
return []byte("null"), nil
}
return []byte(fmt.Sprintf("\"%v\"", err.Err)), nil
}
func (err *ResponseError) UnmarshalJSON(b []byte) error {
var v interface{}
if err := json.Unmarshal(b, v); err != nil {
return err
}
if v == nil {
err.Err = nil
return nil
}
switch tv := v.(type){
case string:
if tv == task.ErrTaskNotExist.Error() {
err.Err = task.ErrTaskNotExist
return nil
}
err.Err = errors.New(tv)
return nil
default:
return errors.New("ResponseError unmarshal failed")
}
}
type Response struct {
ID task.ID `json:"id,omitempty"`
Task task.Task `json:"task"`
Error ResponseError `json:"error"`
}
taskman.go
이 웹 어플리케이션의 main() 함수를 포함하고 있는 파일입니다.
Path prefix
-
htmlPrefix = “/task/”
html을 반환해주는 path, 즉 브라우저로 접속하게 되는 path
-
apiPathPrefix = “/api/v1/task/”
api로 접근하는 path입니다.
-
idPattern = “/{id:[0-9a-f]+}”
URL에 포함된 task의 id의 regex
Handler
-
htmlHandler
html URL을 처리하는 Handler
-
apiGetHandler
GET api 요청을 처리하는 Handler(id 필요)
-
apiPutHandler
PUT api 요청을 처리하는 Handler(id 필요)
-
apiPostHandler
POST api 요청을 처리하는 Handler(id 불필요)
-
apiDeleteHandler
DELETE api 요청을 처리하는 Handler(id 필요)
package main
import (
"log"
"net/http"
"github.com/gorilla/mux"
)
const (
htmlPrefix = "/task/"
apiPathPrefix = "/api/v1/task/"
idPattern = "/{id:[0-9a-f]+}"
)
func main() {
r:= mux.NewRouter()
r.PathPrefix(htmlPrefix).Path(idPattern).Methods("GET").HandlerFunc(htmlHandler)
s:=r.PathPrefix(apiPathPrefix).Subrouter()
s.HandleFunc(idPattern, apiGetHandler).Methods("GET")
s.HandleFunc(idPattern, apiPutHandler).Methods("PUT")
s.HandleFunc("/", apiPostHandler).Methods("POST")
s.HandleFunc(idPattern, apiDeleteHandler).Methods("DELETE")
http.Handle("/", r)
http.Handle(
"/css/",
http.StripPrefix(
"/css/",
http.FileServer(http.Dir("cssfiles")),
),
)
log.Fatal(http.ListenAndServe(":8884", nil))
}
handler.go
request를 처리해주는 handler들을 포함하고 있는 파일입니다.
공통적으로 쓰이는 2가지를 먼저 설명하도록 하겠습니다.
-
Memory DAO
memory로 접근하는 DAO입니다. 별도의 데이터베이스를 사용하지 않기 때문에 재구동시 데이터는 사라집니다.
var m = task.NewMemoryDataAccess()
-
id := task.ID(mux.Vars(r)[“id”])
request에서 id라는 key를 가진 값을 반환받습니다. 예를 들어 /task/123의 123을 반환해줍니다.
-
getTask() method(PUT, POST 명령에서 사용)
request에 포함되어 있는 task 객체를 추출하여 return 합니다.
func getTasks(r *http.Request) ([]task.Task, error) { var result []task.Task if err := r.ParseForm(); err != nil { return nil, err } encodedTasks, ok := r.PostForm["task"] if !ok { return nil, errors.New("task parameter expected") } for _, encodedTask := range encodedTasks { var t task.Task if err := json.Unmarshal([]byte(encodedTask), &t); err != nil { return nil, err } result = append(result, t) } return result, nil }
apiPostHandler
POST request를 받아 task를 추가하고, 추가한 task 정보를 response로 돌려주는 handler입니다.
func apiPostHandler(w http.ResponseWriter, r *http.Request) {
tasks, err := getTasks(r)
if err != nil {
log.Println(err)
return
}
for _, t := range tasks {
id, err := m.Post(t)
err = json.NewEncoder(w).Encode(Response{
ID: id,
Task: t,
Error: ResponseError{err},
})
if err != nil {
log.Println(err)
return
}
}
}
apiGetHandler
GET request를 받아 해당하는 id의 task 정보를 response로 돌려주는 handler입니다.
func apiGetHandler(w http.ResponseWriter, r *http.Request) {
id := task.ID(mux.Vars(r)["id"])
t, err := m.Get(id)
err = json.NewEncoder(w).Encode(Response{
ID: id,
Task:t,
Error:ResponseError{err},
})
if err != nil {
log.Println(err)
}
}
apiPutHandler
PUT request를 받아 해당하는 id의 task 정보를 업데이트하고, 수정된 task 정보를 response에 담아 돌려주는 handler입니다.
func apiPutHandler(w http.ResponseWriter, r *http.Request) {
id := task.ID(mux.Vars(r)["id"])
tasks, err := getTasks(r)
if err != nil {
log.Println(err)
return
}
for _, t := range tasks {
err = m.Put(id, t)
err = json.NewEncoder(w).Encode(Response{
ID: id,
Task: t,
Error: ResponseError{err},
})
if err != nil {
log.Println(err)
return
}
}
}
apiDeleteHandler
DELETE request를 받아 해당하는 id의 task를 삭제하는 handler입니다.
func apiDeleteHandler(w http.ResponseWriter, r *http.Request) {
id := task.ID(mux.Vars(r)["id"])
err := m.Delete(id)
err = json.NewEncoder(w).Encode(Response{
ID: id,
Error:ResponseError{err},
})
if err != nil {
log.Println(err)
return
}
}
htmlHandler
주어진 id의 task 정보를 포함한 html을 response로 돌려주는 handler입니다.
var tmpl = template.Must(template.ParseGlob("html/*.html"))
func htmlHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
log.Println(r.Method, "method is not supported")
return
}
getID := func() (task.ID, error) {
id := task.ID(r.URL.Path[len(htmlPrefix):])
if id == "" {
return id, errors.New("htmlHandler: ID is empty")
}
return id, nil
}
id, err := getID()
if err != nil {
log.Println(err)
return
}
t, err := m.Get(id)
err = tmpl.ExecuteTemplate(w, "task.html", &Response{
ID: id,
Task: t,
Error:ResponseError{err},
})
if err != nil {
log.Println(err)
return
}
}
6.4.2 몽고디비와 연동하기
MemoryDataAccess는 데이터가 재구동시에 사라진다는 문제가 있습니다. 그래서 데이터베이스를 사용하는 DAO를 정의할 필요가 있습니다.
여기서는 몽고디비를 연동하기 위해 mgo라는 써드파티 라이브러리를 사용하도록 하겠습니다.
우선 아래 명령어로 mgo를 설치합니다.
$ go get gopkg.in/mgo.v2
아래와 같이 Accessor interface를 정의한 MongoAccessor를 구현합니다.
package task
import (
"errors"
"fmt"
)
type MemoryDataAccess struct {
tasks map[ID]Task
nextID int64
}
func NewMemoryDataAccess() Accessor {
return &MemoryDataAccess{
tasks:map[ID]Task{},
nextID: int64(1),
}
}
var ErrTaskNotExist = errors.New("task does not exist")
func (m *MemoryDataAccess) Get(id ID) (Task, error) {
t, exists := m.tasks[id]
if !exists {
return Task{}, ErrTaskNotExist
}
return t, nil
}
func (m *MemoryDataAccess) Put(id ID, t Task) error {
if _, exists := m.tasks[id]; !exists {
return ErrTaskNotExist
}
m.tasks[id] = t
return nil
}
func (m *MemoryDataAccess) Post(t Task) (ID, error) {
id := ID(fmt.Sprint(m.nextID))
m.nextID++
m.tasks[id] = t
return id, nil
}
func (m *MemoryDataAccess) Delete(id ID) error {
if _, exists := m.tasks[id]; !exists {
return ErrTaskNotExist
}
delete(m.tasks, id)
return nil
}
handler.go 에서 Accessor 초기화 부분을 수정합니다.
// var m = task.NewMemoryDataAccess()
var m = mongodao.New("", "taskman", "tasks")
실행하고 확인해보면 데이터가 보존되고 있는 것을 확인 할 수 있습니다.