이 포스트는 디스커버리 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

  1. htmlPrefix = “/task/”

    html을 반환해주는 path, 즉 브라우저로 접속하게 되는 path

  2. apiPathPrefix = “/api/v1/task/”

    api로 접근하는 path입니다.

  3. idPattern = “/{id:[0-9a-f]+}”

    URL에 포함된 task의 id의 regex

Handler

  1. htmlHandler

    html URL을 처리하는 Handler

  2. apiGetHandler

    GET api 요청을 처리하는 Handler(id 필요)

  3. apiPutHandler

    PUT api 요청을 처리하는 Handler(id 필요)

  4. apiPostHandler

    POST api 요청을 처리하는 Handler(id 불필요)

  5. 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가지를 먼저 설명하도록 하겠습니다.

  1. Memory DAO

    memory로 접근하는 DAO입니다. 별도의 데이터베이스를 사용하지 않기 때문에 재구동시 데이터는 사라집니다.

     var m = task.NewMemoryDataAccess()
    
  2. id := task.ID(mux.Vars(r)[“id”])

    request에서 id라는 key를 가진 값을 반환받습니다. 예를 들어 /task/123의 123을 반환해줍니다.

  3. 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")

실행하고 확인해보면 데이터가 보존되고 있는 것을 확인 할 수 있습니다.

Referene

책정보, 디스커버리 Go 언어 : 네이버 책