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

Contents

소개

현재 운영중인 joinc 사이트는 go 언어로 개발했다. moniwiki(php)로 된걸 커스터마이징 해서 사용해왔는데, 4년 전쯤에 go 언어로 변경했다. 아래와 같은 특징을 가지고 있다.

moniwiki로 작성했던 문서들을 그대로 이용했다. 이를 위해서 moniwiki의 wiki 문서형식을 처리하기 위한 코드를 개발했다. 문자열가지고 삽질하는 영역이다.

플러그인기능. Moniwiki는 플러그인 기능을 제공한다. 위키문서에 포함된 특정한 문자열이 있으면 이를 읽어서 해당 함수를 호출한다.
오늘은 [[Date]] 입니다.
이렇게 작성하면 function function_Date($data) 함수를 실행하고 아래와 같이 그 결과를 출력한다.
오늘은 2020년 08월 07일 입니다.
PHP는 서버사이드 스크립트언어로 인터프리터가 소스 코드를 매번 실행한다. 따라서 서비스 운영중이라고 하더라도 자유롭게 플러그인을 추가 해서 서비스를 확장 할 수 있다.

이 기능을 go 로 구현해야 했다. 나는 reflect로 구현했다. 기본적으로는 moniwiki에서 Plugin을 찾는 방법을 그대로 사용했다.
func (p *Plugin) New() *Plugin {
    p.pluginList = make(map[string]reflect.Value)
    p.processorList = make(map[string]reflect.Value)
    method := reflect.TypeOf(&Plugin{})
    for i := 0; i < method.NumMethod(); i++ {
        name := method.Method(i).Name
        if strings.Index(name, "Function_") == 0 {
            reflectMethod := reflect.ValueOf(p).MethodByName(name)
            FuncMapKey := strings.ToLower(strings.Split(name, "Function_")[1])
            p.pluginList[FuncMapKey] = reflectMethod
        }
        if strings.Index(name, "Processor_") == 0 {
            reflectMethod := reflect.ValueOf(p).MethodByName(name)
            FuncMapKey := strings.ToLower(strings.Split(name, "Processor_")[1])
            p.processorList[FuncMapKey] = reflectMethod
        }
    }
    return p
}
Plugin 패키지를 만들고 이 Plugin 패키지의 메서드 형태로 플러그인을 개발한다. 메서드의 이름이 Function_으로 시작하면 reflect해서 map에 추가 한 다음 불러오는 방식이다. 예를 들어 oAuth 플러그인이라면 이 플러그인은 아래와 같이 구현한다.
func (p *Plugin) Function_oAuth(data string) interface{} {
   // .... 구현
}
이 방식은 구현이 간단하기는 하지만 수정이 있을 때, 새로 빌드해서 배포해야 한다는 문제가 있다. C/C++ 세계에서라면 공유라이브러리(.so)를 사용 할 건데 내가 joinc wiki를 개발할 당시에는 공유라이브러리를 만들 수 있는 기능이 없었다. 지금은 plugin패키지를 이용해서 공유라이브러리를 사용 할 수 있다.

  • 이번 개발에서는 plugin 패키지를 이용해서 개발이 가능한지를 살펴볼 계획이다.
  • gorilla 패키지와 net/http 패키지로 개발했다. 이번에는 echo 프레임워크를 사용할 생각이다.
  • UnitTest를 넣지 못했다. 이번에는 플러그인을 포함한 모든 코드를 테스트가능하게 만들 계획이다.
  • 프론트엔드는 Vue.js 로 한다. 지금 joinc wiki는 jquery와 바닐라, vue.js 가 마구 섞여 있다.
  • database/sql, github.com/go-sql-driver/mysql 패키지를 이용해서 소위 말하는 SQL 날코딩을 했다. 이번에는 GORM을 사용할 계획이다.
  • html/template 를 사용했는데, 다른 것도 찾아본다.

개발환경

# cat /etc/issue
Ubuntu 20.04.1 LTS \n \l

# uname -a
Linux yundream 5.4.0-44-generic #48-Ubuntu SMP Tue Aug 11 06:38:48 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

# go version
go version go1.15 linux/amd64
에디터로는 vscode를 사용한다.

  • 코드는 github에서 참고 할 수 있다.

애플리케이션 구조 만들기

애플리케이션 구조는 아래와 같다.
.
├── db
├── main.go
├── go.mod
├── go.sum
├── handler
│   └── handler.go
├── model
│   └── wiki.go
├── router
│   └── router.go
├── store
│   └── wiki.go
└── wiki
    └── wiki.go
  • main.go : main function이 위치한다.
  • handler : API 핸들러. 유저 요청을 처리하는 모든 코드가 들어간다.
  • router : 유저 요청을 미들웨어로 보내고 핸들러로 라우팅 한다.
  • model : ORM을 위한 모델이 위치한다. wiki와 user에 대한 모델이 위치하게 될 것이다. ORM으로 gorm을 사용한다.
  • wiki : wiki model과 store를 연결하기 위한 인터페이스(interface)를 가진다.
  • store : 각 모델을 직접 사용하지 않고, store를 통해서 사용 한다. 여기에는 wiki store interface를 구현한다.

model과 store

model과 store 구현은 아래와 같다.
  1. wiki model 스트럭처를 구성한 다음
  2. wiki 모델의 store 인터페이스를 만든다.
  3. wiki store 구현을 만든다.
이렇게 구성을 하면 개발자는 model을 신경쓰지 않고 인터페이스만 구현할 수 있다. 지금은 프로그램의 골격을 짜는게 목적이므로 테스트 가능한 수준에서 최소한으로만 구성한다.

wiki store 인터페이스를 구성한다. wiki/wiki.go
package wiki

import (
	"joinc.co.kr/jwiki/model"
)

// Store ...
type Store interface {
	GetByID(int) (*model.Wiki, error)
}

// PageSave ... 등은 프로젝트 골격이 만들어지면 개발 한다.

wiki 모델의 store 인터페이스를 구현한다.
package model

import (
	"github.com/jinzhu/gorm"
)

// Wiki ...
type Wiki struct {
	gorm.Model
	Name     string `gorm:"column:name;size:160" json:"name"`
	Title    string `gorm:"column:title;size:160" json:"title"`
	Author   string `gorom:"size:80" json:"author"`
	Contents string `gorm:"column:contents" json:"contents"`
}

wiki 모델에 대한 store를 만든다. store/wiki.go
package store

import (
	"joinc.co.kr/jwiki/model"
)

// WikiStore ...
type WikiStore struct {
	Name string
}

// NewWikiStore ...
func NewWikiStore(name string) *WikiStore {
	return &WikiStore{Name: name}
}

// GetByID ...
func (w *WikiStore) GetByID(id int) (*model.Wiki, error) {
	return &model.Wiki{Name: "yundream"}, nil
}

handler 구현

handler/handler.go
package handler

import (
	"fmt"
	"net/http"

	"github.com/labstack/echo/v4"
	"joinc.co.kr/jwiki/wiki"
)

// Handler ...
type Handler struct {
	wikiStore wiki.Store
}

// NewHandler ...
func NewHandler(ws wiki.Store) *Handler {
	return &Handler{
		wikiStore: ws,
	}
}

// Register ...
func (h *Handler) Register(api *echo.Group) {

	wiki := api.Group("/w/:filename")
	wiki.POST("", h.GetWikiFromID)
}

// StoreGet ...
func (h *Handler) StoreGet() {
	a, _ := h.wikiStore.GetByID(1)
	fmt.Println(a)
}

// GetWikiFromID ...
func (h *Handler) GetWikiFromID(c echo.Context) error {
	data, _ := h.wikiStore.GetByID(1)
	return c.JSON(http.StatusOK, data)
}

main 함수

이제 main 함수를 만든다.
package main

import (
	"fmt"

	"joinc.co.kr/jwiki/handler"
	"joinc.co.kr/jwiki/router"
	"joinc.co.kr/jwiki/store"
)

func main() {
	r := router.New()

	api := r.Group("/api")

	ws := store.NewWikiStore("Joinc")
	h := handler.NewHandler(ws)
	h.Register(api)
	fmt.Println(api)
	r.Logger.Fatal(r.Start(":8888"))
}
코드를 분석해보자.
  1. router.New()를 호출하면 echo.New()의 실행결과인 *echo.Echo를 리턴한다.
  2. echo.Group() 메서드는 API의 그룹을 설정하기 위해서 사용한다. 예를 들어 음악과 관련된 api는 /music, 유저와 관련된 API는 /user로 묶을 수 있다. 일단 나는 /api로 설정했다.
  3. store.NewWikiStore를 호출해서 wikiStore 객체를 생성한다.
  4. handler.NewHandler(ws)를 호출해서 핸들러객체를 생성한다. 이때 wiki.Store의 구현체인 wikiStore를 넘긴다. 이제 핸들러는 wikiStore를 이용해서 wiki model을 제어 할 수 있다. 지금은 gorm ORM을 사용하지 않았는데, 다음 번 문서에서 gorm을 사용 할 것이다.
  5. h.Register 메서드를 호출해서 api 그룹에 핸들러를 등록한다.
  6. echo.Start 핸들러로 서버를 실행한다.
애플리케이션을 실행해서 테스트해보자.
# go run main.go           
&{{}  /api [] 0xc00000c1e0}

   ____    __
  / __/___/ /  ___
 / _// __/ _ \/ _ \
/___/\__/_//_/\___/ v4.1.17
High performance, minimalist Go web framework
https://echo.labstack.com
____________________________________O/_______
                                    O\
⇨ http server started on [::]:8888

curl로 테스트
# curl -XPOST localhost:8888/api/w/hello
{"text":"Hello world"}

유닛 테스트

유닛 테스트 구조까지 만들고 끝내자. handler/handler_test.go 파일을 만들었다.
package handler

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/labstack/echo/v4"
	"github.com/stretchr/testify/assert"

	"joinc.co.kr/jwiki/model"
	"joinc.co.kr/jwiki/router"
	"joinc.co.kr/jwiki/store"
	"joinc.co.kr/jwiki/wiki"
)

var (
	ws wiki.Store
	h  *Handler
	e  *echo.Echo
)

func setup() {
	ws = store.NewWikiStore("joinc")
	h = NewHandler(ws)
	e = router.New()
}

func Test_GetWikiFromID(t *testing.T) {
	setup()
	req := httptest.NewRequest(echo.POST, "/api/w/hello", nil)
	req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
	rec := httptest.NewRecorder()
	c := e.NewContext(req, rec)
	assert.NoError(t, h.GetWikiFromID(c))
	if assert.Equal(t, http.StatusOK, rec.Code) {
		var w model.Wiki
		err := json.Unmarshal(rec.Body.Bytes(), &w)
		assert.NoError(t, err)
		assert.Equal(t, w.Name, "yundream")
	}
}
테스트를 해보자. 지금은 하드코딩 한 값이라서 무조건 성공해야 한다.
$ go test -v
=== RUN   Test_GetWikiFromID
--- PASS: Test_GetWikiFromID (0.00s)
PASS
ok      joinc.co.kr/jwiki/handler       0.008s

정리

  • 애플리케이션 구조를 만들었다.
  • 유닛테스트 할 수 있는 환경을 만들었다.
  • 이제 이 구조에서 필요한 코드만 계속 만들어가면 된다.
  • 다음 번에는 gorm 으로 데이터베이스를 직접 제어 할 것이다. 그리고 위키 페이지를 만들기 위한 "위키문서 편집", "위키 문서 읽기", "위키 문서 저장" 메서드를 만들 것이다. 템플릿 엔진도 붙이고, 로그인 시스템도 붙인다. 여기까지만 하고 이 문서를 마무리 한다. 이 후에는 실제 wiki 엔진 개발 작업이라서 문서화 하기는 어렵다.
  • 다은문서 : jwiki ORM을 이용한 데이터베이스 연결