목차
GoLanb로 wiki 만들기의 후속 문서다. 지난 번에 한 일은 아래와 같다.
- 애플리케이션 목표 설정
- 애플리케이션 구조 만들기
이번에 할 일은 아래와 같다.
- 데이터베이스 연동 : Mysql 데이터베이스를 연동한다. ORM 패키지인 gorm을 사용 할 것이다.
- Wiki 문서를 생성하는 API를 만들어서 테스트한다.
- 미들웨어를 작성한다.
원본은
jwiki2에서 확인 할 수 있다.
Docker로 실행했다.
# docker run --name wiki -e MYSQL_ROOT_PASSWORD=1234 -d mysql
wiki 데이터베이스를 만들었다.
# mysql -u root -p -h 172.17.0.2
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 8
Server version: 8.0.17 MySQL Community Server - GPL
Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> CREATEDATABASE wiki;
현재 애플리케이션 구조는 아래와 같다.
.
├── db
├── main.go
├── go.mod
├── go.sum
├── handler
│ └── handler.go
├── model
│ └── wiki.go
├── router
│ └── router.go
├── store
│ └── wiki.go
└── wiki
└── wiki.go
데이터베이스 관련된 패키지는
db 디렉토리에 만들 것이다. db.go 파일을 만들자.
package db
import (
"github.com/jinzhu/gorm"
// mysql
_ "github.com/go-sql-driver/mysql"
"joinc.co.kr/jwiki/model"
)
// New ...
func New() (*gorm.DB, error) {
db, err := gorm.Open("mysql", "root:1234@tcp(172.17.0.2:3306)/wiki")
if err != nil {
return nil, err
}
db.DB().SetMaxIdleConns(5)
db.LogMode(true)
return db, nil
}
// AutoMigration ...
func AutoMigration(db *gorm.DB) {
db.AutoMigrate(
&model.Wiki{},
)
}
- gorm 패키지를 import 한다. gorm패키지는 golang 생태계에서는 가장 많이 사용하는 ORM이다.
- mysql driver를 임포트 했다.
- New()메서드는 데이터베이스에 연결하고 gorm.DB 객체를 리턴한다.
- AutoMigration()메서드는 데이터베이스를 마이그레이션 하기 위해서 사용한다.
가장 중요한 wiki model 스트럭처는(model/wiki.go) 아래와 같다.
package model
import (
"github.com/jinzhu/gorm"
)
// Wiki ...
type Wiki struct {
gorm.Model
Name string `gorm:"column:name;size:160"`
Title string `gorm:"column:title;size:160"`
Author string `gorom:"size:80"`
Contents string `gorm:"column:contents"`
}
gorm.DB 객체는 main 함수에서 생성해서, 앞서 만들어 놓은 Store로 넘긴다. 아래는 완전한 main.go의 코드다.
package main
import (
"fmt"
"os"
"joinc.co.kr/jwiki/db"
"joinc.co.kr/jwiki/handler"
"joinc.co.kr/jwiki/router"
"joinc.co.kr/jwiki/store"
_ "github.com/jinzhu/gorm/dialects/mysql"
)
func main() {
r := router.New()
d, err := db.New()
if err != nil {
fmt.Println("DB Error", err.Error())
os.Exit(1)
}
db.AutoMigration(d)
api := r.Group("/api")
ws := store.NewWikiStore(d)
h := handler.NewHandler(ws)
h.Register(api)
fmt.Println(api)
r.Logger.Fatal(r.Start(":8888"))
}
- db.New() 메서드를 호출 데이터베이스에 연결한다.
- db.AutoMigraion() 메서드를 실행해서, wiki.model에 따라서 데이터베이스를 마이그레이션 한다. 뭔가 대단한건 아니다. 테이블이 없으면 자동으로 만들어주고, (모델이 변경되면) 테이블 스키마를 업데이트 해주겠다는 얘기다. 이렇게 해서 소프트웨어 객체와 데이터를 통합할 수 있다.
wiki model을 위한 bind 스트럭처 개발
HTTP Handler가 유저의 요청(Request)를 받아서 처리한다. 요청 데이터의 컨텐츠 타입(Contents-Type)은 application/json일 테고, wiki.model 스트럭처로 bind 하면 된다. wiki model 스트럭처는 아래와 같다. model/wiki.go 다.
package model
import (
"github.com/jinzhu/gorm"
)
// Wiki ...
type Wiki struct {
gorm.Model
Name string `gorm:"column:name;size:160"`
Title string `gorm:"column:title;size:160"`
Author string `gorom:"size:80"`
Contents string `gorm:"column:contents"`
}
model 스트럭처를 핸들러에서 직접 바인드하는 방법도 있지만, model 과 비지니스로직은 분리하는게 좋을 것 같아서 bind 스트럭처를 따로 만들었다. handler/request.go 파일이다.
package handler
import (
"fmt"
"github.com/labstack/echo/v4"
"joinc.co.kr/jwiki/model"
)
type wikiCreateRequest struct {
Wiki struct {
Name string `json:"name" validate: "required"`
Title string `json:"title" validate: "required"`
Author string `json:"author" validate: "required"`
Contents string `json:"contents" validate: "required"`
} `json:"wiki"`
}
// Data ...
type Data struct {
Name string `json:"name"`
Title string `json:"title`
Author string `json:"author"`
Contents string `json:"contents"`
}
func (w *wikiCreateRequest) bind(c echo.Context, a *model.Wiki) error {
if err := c.Bind(&w.Wiki); err != nil {
fmt.Println("Bind", w)
return err
}
a.Name = w.Wiki.Name
a.Title = w.Wiki.Title
a.Author = w.Wiki.Author
a.Contents = w.Wiki.Contents
return nil
}
비즈니스 로직의 구현을 위해서 유저의 요청을 wiki 모델에 바인딩 하기 위한 스트럭처와 bind 메서드를 만들었다. 이제 개발자는 모델의 수정 없이, 유저 요청을 처리할 수 있게 된다.
CreateWiki 핸들러는 아래와 같이 유저 요청을 바인딩 한다.
func (h *Handler) CreateWiki(c echo.Context) error {
var w model.Wiki
req := &wikiCreateRequest{}
if err := req.bind(c, &w); err != nil {
return c.JSON(http.StatusUnprocessableEntity, err)
}
err := h.wikiStore.SaveWikiPage(&w)
if err != nil {
return c.JSON(http.StatusUnprocessableEntity, err)
}
return nil
}
유저의 요청은 모델에 바인딩 되고, wikiStore.SaveWikiPage 메서드로 데이터베이스에 저장된다. store/wiki.go의 코드를 보자.
package store
import (
"github.com/jinzhu/gorm"
"joinc.co.kr/jwiki/model"
)
// WikiStore ...
type WikiStore struct {
db *gorm.DB
}
// SaveWikiPage ..
func (w *WikiStore) SaveWikiPage(wiki *model.Wiki) error {
tx := w.db.Begin()
if err := tx.Create(&wiki).Error; err != nil {
tx.Rollback()
return err
}
return tx.Commit().Error
}
테스트에 사용한 데이터파일은 아래와 같다.
{
"name": "my page",
"title": "Hello World",
"author": "yundream",
"contents": "What's your name"
}
애플리케이션이 실행되면 Mysql 테이블이 만들어진다.
# go run main.go
# mysql -u root -p -h 172.17.0.2
Enter password:
mysql> use wiki;
mysql> desc wikis;
+------------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+------------------+------+-----+---------+----------------+
| id | int(10) unsigned | NO | PRI | NULL | auto_increment |
| created_at | datetime | YES | | NULL | |
| updated_at | datetime | YES | | NULL | |
| deleted_at | datetime | YES | MUL | NULL | |
| name | varchar(160) | YES | | NULL | |
| title | varchar(160) | YES | | NULL | |
| author | varchar(255) | YES | | NULL | |
| contents | varchar(255) | YES | | NULL | |
+------------+------------------+------+-----+---------+----------------+
8 rows in set (0.01 sec)
테이블이 성공적으로 만들어졌다.
curl로 테스트를 했다.
# curl -XPOST localhost:8888/api/w/hello -d @wikipage.json -H "Content-type: application/json" -i
HTTP/1.1 200 OK
Date: Sun, 20 Sep 2020 07:41:36 GMT
Content-Length: 0
mysql 테이블을 확인해보자.
mysql> SELECT * FROM wikis\G
*************************** 1. row ***************************
id: 3
created_at: 2020-09-20 07:41:36
updated_at: 2020-09-20 07:41:36
deleted_at: NULL
name: my page
title: Hello World
author: yundream
contents: What's your name
1 row in set (0.00 sec)
이렇게 해서 유저 요청을 데이터베이스에 저장하는 기본적인 웹 애플리케이션 서버를 만들었다. 그러나 유저의 요청의 종류에 상관없이 모든 요청의 전후에 일부코드의 실행이 필요 할 수도 있다. 예를 들어 서버에 대한 모든 요청에 대한 로깅, 보안 검증 코드를 호출, 사용자가 인증이 되어었는지를 확인을 해야 할 수 있다. 이런 코드들을 각 핸들러에 두는 것은 비효율적이다. 미들웨어를 사용하면 이러한 공통작업을 효율적으로 수행 할 수 있다.
echo 프레임워크는 그룹(group)별로 미들웨어를 설치 할 수 있다. 예를 들어 admin과 관련된 페이지는 아래와 같이 인증모듈이 실행되도록 할 수 있다.
e := echo.New()
admin := e.Group("/admin". middleware.BasicAuth())
middleware 패키지를 만들기로 했다. 패키지의 위치는 meddleware/meddleware.go 다.
package middleware
import (
"github.com/labstack/echo/v4"
)
// ServerMiddleware ...
func ServerMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
if err := next(c); err != nil {
c.Error(err)
}
c.Response().Header().Set(echo.HeaderServer, "jwiki/2.0")
return nil
}
}
미들웨어의 작동방식을 확인 할 수 있는 아주 간단한 코드다. 이 코드는 응답 메시지의 헤더에 "server: jwiki/2.0"을 추가한다. main 함수를 아래와 같이 수정했다.
package main
import (
"fmt"
"os"
"joinc.co.kr/jwiki/middleware"
"joinc.co.kr/jwiki/db"
"joinc.co.kr/jwiki/handler"
"joinc.co.kr/jwiki/router"
"joinc.co.kr/jwiki/store"
_ "github.com/jinzhu/gorm/dialects/mysql"
)
func main() {
r := router.New()
d, err := db.New()
if err != nil {
fmt.Println("DB Error", err.Error())
os.Exit(1)
}
db.AutoMigration(d)
api := r.Group("/api", middleware.ServerMiddleware)
ws := store.NewWikiStore(d)
h := handler.NewHandler(ws)
h.Register(api)
r.Logger.Fatal(r.Start(":8888"))
}
/api/*를 호출 할 때, middleware.ServerMiddleware를 실행하도록 했다. 테스트해보자.
# curl -XPOST localhost:8888/api/w/hello -d @wikipage.json -H "Content-type: application/json" -i
HTTP/1.1 200 OK
Server: jwiki/2.0
Date: Sun, 20 Sep 2020 14:23:49 GMT
Content-Length: 0
클라이언트의 모든 요청로그를 저장하는 logging 미들웨어를 설치하기로 했다. 애플리케이션 전역으로 작동하는 만큼 router/router.go를 수정하기로 했다.
package router
import (
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
// New ...
func New() *echo.Echo {
e := echo.New()
e.Use(middleware.Logger())
return e
}
curl로 테스트하면 아래와 같이 로그가 출력되는 걸 확인 할 수 있다.
{"time":"2020-09-21T01:33:31.576497535+09:00","id":"","remote_ip":"127.0.0.1","host":"localhost:8888","method":"POST","uri":"/api/w/hello","user_agent":"curl/7.68.0","status":200,"error":"","latency":7167794,"latency_human":"7.167794ms","bytes_in":110,"bytes_out":0}
이 로그를 지금처럼 표준출력하거나 파일로 쌓으면 된다.
지금까지 했던 것들을 정리해보자.
- 패키지 경로를 포함한 애플리케이션 구조 정의
- ORM의 사용. 모델과 비즈니스 로직의 분리
- 로깅시스템
- 유닛테스트
얼추 뼈대는 만들어진 것 같다. 여기에 살을 (아주많이) 붙이면 wiki 시스템이 만들어질 것이다.