※ 디스커버리 Go 언어 라는 책 내용을 전반적으로 다루지만 책 리뷰보다는 좀 더 간단하게 정리해보겠습니다.
6. 웹 어플리케이션 작성
이번 장에서는 간단한 웹 앱을 만들어보겠습니다.
6.1 Hello, World!
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, World!")
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
웹 앱에서는 위처럼 아주 간단한 코드로 Hello, Wrold 를 찍을 수 있습니다. 브라우저에서 http://localhost:8080 주소를 열어 확인 가능합니다.
6.2 할 일 목록 관리 웹 앱 만들기
위의 간단한 예제를 벗어나 할 일 목록을 관리하는 웹 앱을 만들어 보겠습니다. 우선은 하나의 파일에 모든 코드를 다 때려 넣은 뒤 리팩토링도 해 볼 것입니다.
6.2.1 RESTful API
다들 아시겠지만 대략적으로 설명하자면 URI 로 어떤 자원을 접근할지 지정하고 GET, PUT, POST, DELETE 메서드를 통해 어떤 동작을 취할지 지정하는 것을 RESTful API 라 합니다.
6.2.2 Data Access Object
줄여서 DAO 라고도 하는 이 데이터베이스 접근 객체는 데이터베이스에 필요한 연산들을 추상 인터페이스로 만들어서 사용하는 것입니다. 단점도 꽤 있는 패턴이지만, 이번 장에선 이 패턴을 이용해보도록 하겠습니다. 우선 실제로 데이터베이스를 이용하진 않고 메모리 내에서 돌아가게끔 할 것입니다.
type ID string // to identify a task
type DataAccess interface {
Get(id ID) (task.Task, error)
Put(id ID, t task.Task) error
Post(t task.Task) (ID, error)
Delete(id ID) error
}
RESTful API 를 따라서 위와 같은 인터페이스를 만들었습니다. 이제 메모리에 접근해 위 인터페이스의 기능을 하는 구현체를 만들어보겠습니다.
type MemoryDataAccess struct {
tasks map[ID]task.Task
nextID int64
}
// MemoryDataAccess 를 반환, 이렇게 인터페이스를 반환하게끔 하면 받는쪽에서 좀 더 유연한 처리 가능
func NewMemoryDataAccess() DataAccess {
return &MemoryDataAccess {
tasks: map[ID]task.Task{},
nextID: int64(1),
}
}
// id 에 해당하는 task 가 없을 경우의 에러 정의
var ErrTaskNotExit = errors.New("task does not exit")
// DataAccess 인터페이스의 GET, PUT, POST, DELETE 구현
func (m *MemoryDataAccess) Get(id ID) (task.Task, error) {
t, exists := m.tasks[id]
if !exists {
return task.Task{}, ErrTaskNotExit
}
return t, nil
}
func (m *MemoryDataAccess) Put(id ID, t task.Task) error {
if _, exists := m.tasks[id]; !exists {
return ErrTaskNotExist
}
m.tasks[id] = t
return nil
}
func (m *MemoryDataAccess) Post(t task.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 핸들러 구현
이제 API 핸들러를 구현해보겠습니다. 각 요청에 대한 응답은 아이디, 작업, 에러가 들어가야 하므로 이 셋을 묶는 응답 구조체를 하나 만들겠습니다. 에러를 구조체에 넣어야 하니 에러가 JSON 으로 변환되어야 하기 때문에, 먼저 MarshalJSON 과 UnmarshalJSON 또한 구현해보도록 하겠습니다.
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 == ErrTaskNotExist.Error() {
err.Err = ErrTaskNotExit
return nil
}
err.Err = errors.New(tv)
return nil
default:
return errors.New("ResponseError unmarshal failed")
}
}
이어서 응답 구조체를 정의하니다.
type Response struct {
ID ID `json:"id,omitempty"`
Task task.Task `json:"task"`
Error ResponseError `json:"error"`
}
/* 세가지 필드를 두고, 필드 이름을 대문자에서 소문자로 바꾸기 위한 태그 추가 */
이제 메인함수를 추가하겠습니다.
var m = NewMemoryDataAccess()
const pathPrefix = "/api/v1/task/" // RESTful API 에 넘어가는 URL 경로
func apiHandler(w http.ResponseWriter, r *http.Request) {
... // 추가로 구현해야 할 부분
}
func main() {
http.HandleFunc(pathPrefix, apiHandler)
log.Fatal(http.ListenAndServe(":8887", nil)) // 임의로 8887 포트 사용
}
위 코드에선 전역변수 m 을 선언했는데, 좋은 방법은 아니며 후에 리팩토링하도록 하겠습니다. 아래부터는 apiHandler 안에 들어갈 코드부분입니다.
// URL 경로에서 ID 값을 가져오는 함수
getID := func() (ID, error) {
id := task.ID(r.URL.Path[len(pathPrefix):])
if id == "" {
return id, errors.New("apiHandler: ID is empty")
}
return id, nil
}
// POST 로 넘어온 task 를 받는 함수
getTasks := func ([]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
}
switch r.Method {
case "GET":
id, err := getID()
if err != nil {
logPrintln(err)
return
}
t, err := m.Get(id)
err = json.NewEncoder(w).Encode(Response{
ID: id,
Task: t,
Error: ResponseError(err),
})
if err != nil {
log.Println(err)
}
case "PUT":
id, err := getID()
if err != nil {
log.Println(err)
return
}
tasks, err := getTasks()
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
}
}
case "POST":
tasks, err := getTasks()
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
}
}
case "DELETE":
id, err := getID()
if err != nil {
log.Println(err)
return
}
err = m.Delete(id)
err = json.NewEncoder(w).Encode(Response{
ID: id,
Error: ResponseError(err),
})
if err != nil {
log.Println(err)
return
}
}
중복 코드가 많아서 코드가 좀 길어졌습니다. 이 부분 또한 후에 리팩토링 하도록 하겠습니다. 파일 하나가 상당히 길어졌는데, 우선 아쉬운대로 완성이며 실행하면 API 가 제대로 동작하는지 테스트 해 볼 수 있습니다.
6.2.4 HTML 템플릿 작성하기
기본으로 제공되는 HTML 템플릿 엔진 링크만 달아두며 이부분은 크게 필요하다고 생각하지않아 넘어가도록 하겠습니다.
6.3 코드 리팩토링
6.3.1 통일성 있게 파일 나누기
- 데이터 액세스 인터페이스 : accessor.go (type DataAccess 부터 func Delete 까지)
- 응답 자료형 및 구현 : response.go (type ResponseError 부터 type Response 까지)
- 핸들러 : handlers.go (var m 부터 apiHandler 까지)
이렇게 우선 파일을 나눠보도록 하겠습니다. 지금까지의 과정을 순서대로 코딩했다면 괄호의 영역대로 나눠질 것입니다. goimport 설정이 제대로 되어있다면 나눠진 파일 안에 import 는 자동으로 붙습니다.
6.3.2 라우터 사용하기
라우터를 이용하면 핸들러 코드를 더 깔끔하게 작성할 수 있습니다. 여러 라이브러리가 있지만 Gorilla Web Toolkit 의 mux 라는 써드파티 라이브러리를 사용해보도록 하겠습니다.
> go get github.com/gorilla/mux
이제 메인 함수를 변경합니다.
const (
apiPathPrefix = "/api/v1/task/"
htmlPathPrefix = "/task/"
idPattern = "/{id:[0-9]+}"
)
func main() {
r := mux.NewRouter()
r.PathPrefix(htmlPathPrefix).
Path(idPattern).
Methods("GET").
HandlerFunc(htmlHandler)
s := r.PathPrefix(apiPathPrefix).Subrouter()
s.HandleFunc(idPattern, apiGetHandler).Methods("GET")
s.HandleFunc(idPatttern, apiPutHandler).Methods("PUT")
s.HandleFunc("/", apiPostHandler).Methods("POST")
s.HandleFunc(idPattern, apiDeleteHandler).Methods("DELETE")
http.Handle("/", r)
log.Fatal(http.ListenAndServe(":8884", nil))
}
URL 구조에서 ID 의 패턴을 정해주고, 메서드도 강제해서 보낼 수 있게 되었습니다. 이로 인해 getID() 가 필요없어졌고 메서드별로 case 를 나눌 필요가 없어졌습니다.
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)
}
}
apiGetHandler 만 구현해봤습니다. ID 의 패턴을 미리 검사하기 때문에 ID 를 얻고 에러처리하는 부분까지 id := ... 2번 라인 하나로 대체 가능합니다. apiPutHandler, apiPostHandler, apiDeleteHandler 는 각자 구현해보면 되겠습니다.
6.4 추가 주제
6.4.1 HTTP 파일 서버
자바스크립트, CSS, 이미지 등을 다른 파일에 분리하여 두고 싶을 땐, http.FileServer 함수를 참조하면 됩니다.
package main
import (
"flag"
"log"
"net/http"
)
var (
addr = flag.String(
"addr",
":8080",
"address of the webserver",
)
root = flag.String(
"root",
"/var/www",
"root directory"
)
)
func main() {
flag.Parse()
log.Fatal(http.ListenAndServe(
*addr,
http.FileServer(http.Dir(*root)),
))
}
addr 과 root 플래그를 넘겨줘서 원하는 주소로 원하는 디렉토리를 웹에서 접근할 수 있습니다. 아래와 같은 명령으로 /home/me/www/css/style.css 파일은 http://localhost:8000/css/style.css 주소로 접근할 수 있습니다.
> go run fileserver.go --addr=localhost:8000 --root=/home/me/www
위의 예제에서 나아가 작성중인 웹 어플리케이션에 핸들러를 추가하는 법은 아래와 같습니다.
// /css/ 로 시작하는 URL 경로에서는 로컬 디렉토리의 path/to/css 경로에 있는 파일 접근 가능
http.Handle("/css/", http.FileServer(http.Dir("path/to")))
// /css/ URL 경로를 받지만 로컬에선 path/to/cssfiles 로 되어있는 경우
http.Handle(
"/css/",
http.StripPrefix(
"/css/",
http.FileServer(http.Dir("path/to/cssfiles")),
),
)
6.4.2 몽고디비와 연동하기
mgo 라는 써드파티 라이브러리가 유명합니다. 아래와 같이 설치합니다.
> go get gopkg.in/mgo.v2
코드의 구현은 각자 해보도록 넘어가겠습니다.
6.4.3 에러 처리
Go 에서의 에러처리는 중요하면서 귀찮기도 합니다. Exception 이 쓰이는 언어들에 비해 에러를 반환하고 이것을 직접 검사하는 방법은 꽤 고전적으로 보이기도 합니다. 에러 처리에 대한 것들을 추가적으로 보도록 하겠습니다.
- 에러이 추가 정보 실어서 보내기
type ErrNegativeID ID
func (e ErrNegativeID) Error() string {
return fmt.Sprintf("ID %d is negative", e)
}
var err error = ErrNegativID(-100)
fmt.Println(err) // -> ID -100 is negative
위는 예시 코드인데, 이렇게 해줌으로써 ErrNegativeID 는 ID 와 표현이 같은 자료형이 되었고 Error 메서드를 구현하기 떄문에 error 가 쓰인 모든 곳에 쓸 수 있습니다. 위처럼 에러 인터페이스 구현을 통한 유연한 처리 덕에 더 많은 에러 정보를 주고 받을 수 있습니다.
- 반복된 에러 처리 피하기
에러가 발생했을 때 프로그램을 끝내고 싶은 경우를 보도록 하겠습니다.
func Must(err error) {
if err != nil {
panic(err)
}
}
Must(f())
위처럼 Must 와 같은 이름의 함수를 만들어 err 이 nil 값이 아닐 때 패닉을 발생시키면, if err != nil 과 같은 구문을 여러번 반복할 필요 없이 Must(f()) 호출 한번으로 해결 할 수 있습니다. 에러가 나면 프로그램이 종료되어야 하거나, 반드시 에러가 발생하지 않는 경우에 사용하면 편리할 것입니다.
- panic 과 recover
패닉이 발생하면 호출 스택을 타고 역순으로 올라가서 프로그램이 종료됩니다. 이때 호출 스택에 있는 함수 내부에 defer 함수가 같이 등록되었다면 defer 함수가 호출되고, 이런 형태로 defer 를 계속 처리하면서 패닉을 상위 호출자로 전파합니다.
타고 올라가는 과정에서 패닉이 전파되지 않도록 하는 방법이 recover 이며 이는 defer 안에서만 유효합니다.
func f() int {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered in f", r)
}
}()
g() // this function panics
return 100
}
func g() {
panic("panic!")
}
fmt.Println("f() = ", f())
// Output:
// Recovered in f
// 0
defer 는 패닉이 발생했을 때, 발생하지 않았을 때 모두 실행됩니다. defer 내에 있는 recover 를 호출햇을 떄 nil 값이 반환된다면 패닉이 발생하지 않은 경우입니다. 위처럼 f() 를 호출하면 100 이 반환되는 부분까지 가지 못 했기 때문에 int 기본형인 0 이 반환되었습니다.
6.5 Summary
- 기본으로 제공되는 http 패키지를 이용해 간단한 웹 서버 만들기 가능
- 이를 응용해 어렵지 않게 RESTful API 만들기 가능
- 라우터와 같은 써드타피 라이브러리를 사용해 간결한 코드 만들기
- 에러 처리에 대한 몇몇 방법 확인
- panic, recover 의 활용
- 출처 : https://book.naver.com/bookdb/book_detail.nhn?bid=10337667
'Backend > Golang' 카테고리의 다른 글
Go 언어 - [7장 - (2)] (동시성) (0) | 2020.09.12 |
---|---|
Go 언어 - [7장 - (1)] (동시성) (0) | 2020.09.05 |
Go 언어 - [5장] (구조체) (0) | 2020.08.22 |
Go 언어 - [4장] (함수) (0) | 2020.08.16 |
Go 언어 - [3장] (문자열 및 자료구조) (0) | 2020.08.04 |