Recommanded Free YOUTUBE Lecture: <% selectedImage[1] %>

Contents

소개

GoLanb로 wiki 만들기의 후속 문서다. 지난 번에 한 일은 아래와 같다.
  • 애플리케이션 목표 설정
  • 애플리케이션 구조 만들기
이번에 할 일은 아래와 같다.
  1. 데이터베이스 연동 : Mysql 데이터베이스를 연동한다. ORM 패키지인 gorm을 사용 할 것이다.
  2. Wiki 문서를 생성하는 API를 만들어서 테스트한다.
  3. 미들웨어를 작성한다.
원본은 jwiki2에서 확인 할 수 있다.

MySQL 데이터베이스 실행 과 준비

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;

GORM을 이용한 데이터베이스 연결

현재 애플리케이션 구조는 아래와 같다.
.
├── 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)

미들웨어 만들기

이렇게 해서 유저 요청을 데이터베이스에 저장하는 기본적인 웹 애플리케이션 서버를 만들었다. 그러나 유저의 요청의 종류에 상관없이 모든 요청의 전후에 일부코드의 실행이 필요 할 수도 있다. 예를 들어 서버에 대한 모든 요청에 대한 로깅, 보안 검증 코드를 호출, 사용자가 인증이 되어었는지를 확인을 해야 할 수 있다. 이런 코드들을 각 핸들러에 두는 것은 비효율적이다. 미들웨어를 사용하면 이러한 공통작업을 효율적으로 수행 할 수 있다.

 Middleware

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 미들웨어

클라이언트의 모든 요청로그를 저장하는 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 시스템이 만들어질 것이다.