본문 바로가기

Backend/Golang

Go 언어 - [6장] (웹 어플리케이션 작성)

※ 디스커버리 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