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

Contents

REST API의 문서화

Go 언어로 만든 REST API의 문서화가 절실하다. GoDoc은 좋은 문서화 툴이긴 하지만 패키지 단위라서, API 레벨의 문서화에는 적당하지 않다. 주석을 문서화 하는 툴들은 API 스펙의 변화를 반영하지 못하기 때문에, 문서의 품질은 위키로 한땀 한땀 노가다 뛰는 것과 큰 차이가 없다.

그러다가 찾은게 swagger다. 개발자가 API의 스펙을 만들면, 스펙으로 부터 서버와 클라이언트 코드를 만든다. swagger은 스펙을 만들기 위한 Swagger editor, 스펙으로 부터 코드를 만드는 Swagger codegen , 스펙으로 부터 API 문서를 보여주는 Swagger UI를 제공한다.

go-swagger 설치

$ go get github.com/go-swagger/go-swagger/cmd/swagger

swagger 테스트

swagger툴이 설치된다. 여기에는 스펙문서를 검증(validate)하는 기능도 포함하고 있다. swagger에서 제공하는 예제 프로그램을 이용해서, swagger의 기능을 간단히 테스트 해보자.

테스트할 셈플 프로그램은 https://github.com/go-swagger/go-swagger/blob/master/examples/todo-list 다.

swagger는 REST API의 스펙문서로 부터 코드를 생성한다. 스펙문서는 yml로 만들어진다. 따라서 스펙문서 규칙을 따라야 한다. swagger validate로 스펙을 검증해보자.
$ swagger validate https://raw.githubusercontent.com/go-swagger/go-swagger/master/examples/todo-list/swagger.yml
The swagger spec at "https://raw.githubusercontent.com/go-swagger/go-swagger/master/examples/todo-list/swagger.yml" is valid against swagger specification 2.0
스펙 문서가 올바름을 확인했다. 스팩문서를 다운로드해서 TodoList 서버를 만들어보자.
$ wget https://raw.githubusercontent.com/go-swagger/go-swagger/master/examples/todo-list/swagger.yml
$ $ swagger generate server -A TodoList -f ./swagger.yml 
2017/08/26 21:12:09 building a plan for generation
2017/08/26 21:12:09 planning definitions
2017/08/26 21:12:09 planning operations
2017/08/26 21:12:09 grouping operations into packages
2017/08/26 21:12:09 planning meta data and facades
......
2017/08/26 21:12:09 creating "doc.go" in "restapi" as doc
Generation completed!

For this generation to compile you need to have some packages in your GOPATH:

  * github.com/go-openapi/runtime
  * github.com/tylerb/graceful
  * github.com/jessevdk/go-flags
서버 코드를 성공적으로 만들었다. 서버코드를 컴파일 하기 위해서 몇 가지 패키지를 다운로드 하라고 가이드 한다. 디렉토리 구조를 살펴보면, 스펙문서에 기술한 내용대로 A코드들이 만들어진 걸 확인 할 수 있다.
$ tree
.
├── cmd
│   └── todo-list-server
│       └── main.go
├── models
│   ├── error.go
│   └── item.go
├── restapi
│   ├── configure_todo_list.go
│   ├── doc.go
│   ├── embedded_spec.go
│   ├── operations
│   │   ├── todo_list_api.go
│   │   └── todos
│   │       ├── add_one.go
│   │       ├── add_one_parameters.go
│   │       ├── add_one_responses.go
│   │       ├── add_one_urlbuilder.go
│   │       ├── destroy_one.go
│   │       ├── destroy_one_parameters.go
│   │       ├── destroy_one_responses.go
│   │       ├── destroy_one_urlbuilder.go
│   │       ├── find.go
│   │       ├── find_parameters.go
│   │       ├── find_responses.go
│   │       ├── find_urlbuilder.go
│   │       ├── update_one.go
│   │       ├── update_one_parameters.go
│   │       ├── update_one_responses.go
│   │       └── update_one_urlbuilder.go
│   └── server.go
└── swagger.yml
서버를 올려서 테스트 해보자.
$ go run cmd/todo-list-server/main.go --socket-path=/tmp/todo.sock --host=127.0.0.1 --port=9000
curl로 테스트를 했다.
$ curl -XGET localhost:9000/ -H "x-todolist-token: 12345"
{"code":501,"message":"api key auth (key) x-todolist-token from header param [x-todolist-token] has not yet been implemented"}
swagger.yml은 REST API 서버의 골격을 잡아줄 뿐이다. 실제 구현은 개발자의 몫이다. 지금은 라우팅은 되고 있으나 API에 대한 구현이 없어서 에러가 출력될 것이다.

구현은 restapi/configure_todo_list.go파일을 이용한다. 여기에 각 API 별로 코드를 입력하면 된다.

스펙으로 부터, API 문서를 만들어보자.
$ swagger generate spec -i swagger.yml -o ./swagger.json
이제 swagger-ui에서 swagger.json을 읽는 것으로 API 문서를 읽을 수 있다. 애초에 스펙으로 부터 REST API가 만들어지고, 이 스펙에서 API 문서가 만들어지기 때문에, 코드를 정확히 반영한다. 뿐만 아니라 테스트를 위한 UI까지 제공한다.

대략 테스트가 끝난 것 같다. 이제 실제 작동하는 애플리케이션을 만들면서 swagger를 익혀봐야 겠다.

User 정보 API 개발

유저 정보를 저장하고 조회하기 위한 위한 REST API 서버를 만들기로 했다. 이 서버가 제공하는 API는 아래와 같다.
  • GET /search : user id를 키로 유저 정보를 읽어온다.
  • POST /user : user 정보를 인서트 한다.
  • DELETE /user : userid로 유저 정보를 삭제한다.
  • PUT /user : 유저 정보를 업데이트 한다.

개발 환경

Swagger는 C#, cpp, dart, python, java, scala, html ... 다양한 언어를 지원한다. 나는 go 언어 기반으로 환경을 만들기로 했다. 언어가 달라지더라도 언어 문법의 차이만 있을 뿐, 구성에는 별 문제가 없을 (아마도)거다. 나중에 파이선에도 한번 적용해보긴 해야 겠다.
  • 언제나 그렇듯이 우분투 리눅스다. 버전 17.04
  • Go 버전 1.8.3
  • 사용한 코드는 GitHub에서 확인 할 수 있다.

스펙 문서

GET /search API만을 포함한 스펙이다. 이걸로 스펙의 주요 요소들을 살펴보자. 설명은 주석으로 충분할 것 같다. 유효성 검증을 해보자.
$ swagger validate ./swagger.yml 
The swagger spec at "./swagger.yml" is valid against swagger specification 2.0
서버코드를 만든다.
$ swagger generate server -A User -f ./swagger.yml 
서버를 실행하고
$ go run cmd/user-server/main.go --port=10000
2017/08/27 01:56:54 Serving user at http://127.0.0.1:10000
curl로 테스트를 했다.
$ curl -XGET -I localhost:10000/search 
HTTP/1.1 501 Not Implemented
Content-Type: application/joinc.user-address.v1+json
Date: Sat, 26 Aug 2017 16:59:20 GMT
Content-Length: 56
구현체가 없는 이유로 501 코드를 반환한다. 이제 구현체를 만들어보자.

구현하기

GET /search를 구현해보자. restapi/configure_user.go 문서를 열어보자. configureAPI 함수가 보이는데, 여기에 코드를 구현하면 된다.
func configureAPI(api *operations.UserAPI) http.Handler {
    // configure the api here  
    api.ServeError = errors.ServeError
    
    // Set your custom logger if needed. Default one is log.Printf
    // Expected interface func(string, ...interface{})
    //
    // Example:
    // api.Logger = log.Printf 

    api.JSONConsumer = runtime.JSONConsumer()

    api.JSONProducer = runtime.JSONProducer()

    // 이 함수를 구현하면 된다. 
    api.UserGetSearchHandler = user.GetSearchHandlerFunc(func(params user.GetSearchParams) middleware.Responder {
        return middleware.NotImplemented("operation user.GetSearch has not yet been implemented")
    })

    api.ServerShutdown = func() {}  

    return setupGlobalMiddleware(api.Serve(setupMiddlewares))
}
지금은 GET /search API를 호출하면 middleware.NotImplemented 코드가 작동해서 405 에러가 떨어질 것이다.
$ curl localhost:33729/search -X GET
"operation user.GetSearch has not yet been implemented"

요청 데이터 읽어오기

swagger는 스펙의 definitions설정을 기반으로 모델을 만든다. 이 정보는 models/user.go에 저장이 된다. 파일을 열어보자.
type User struct {

    // user address
    Address string `json:"address,omitempty"`

    // user email
    Email string `json:"email,omitempty"`

    // user name
    Name string `json:"name,omitempty"`                                                                                        

    // user id
    // Required: true
    UserID *string `json:"user_id"`
}
요청 데이터는 이 모델을 따라서 만들면 된다. GET /search API는 user_id를 필요로 하기 때문에, 위의 User 모델을 따르는 Json 데이터를 전송하면 된다. 이제 요청 데이터에서 user_id를 읽어오도록 configure_user.go를 수정해 보자. 데이터베이스에서 읽어서 출력해야 겠지만 1. 유저 요청을 읽어오는 방법, 2. 응답을 하는 방법을 알아보는 차원에서 하드코딩했다.
    api.UserGetSearchHandler = user.GetSearchHandlerFunc(func(params user.GetSearchParams) middleware.Responder {
        // 이 부분에서 DB 검색을 해야 할 것이다.
        // 일단은 생략
        info := models.User{Address: "Seoul",
            Email:  "yundream@gmail.com",
            Name:   "Yun",
            UserID: params.Body.UserID}
        return &UserInfoOK{Body: info}
    })
이 함수는 middleware.Responder를 반환한다. middleware.Responder은 인터페이스로 WriteResponse를 구현해야 한다.
type Responder interface {
    WriteResponse(http.ResponseWriter, runtime.Producer)
}
아래와 같이 구현했다.
type UserInfoOK struct {
    Body models.User `json:"body"`
}

func (u *UserInfoOK) WriteResponse(w http.ResponseWriter, producer runtime.Producer) {
    producer.Produce(w, u.Body)
}   
테스트해보자.
$ curl localhost:9000/search -XGET -d '{"user_id":"yundream"}' -H "content-type: application/json"
{"address":"Seoul","email":"yundream@gmail.com","name":"Yun","user_id":"yundream"}

데이터베이스 연동

MySQL 데이터베이스를 연동해서 그럴듯한 애플리케이션으로 만들어보자. 테스트용 테이블을 만들고 데이터를 입력했다.
mysql> create database userInfo;
mysql> create table user(id varchar(32), name varchar(32), email varchar(64), address varchar(128));
mysql> insert into user set id="yundream", name="yun", email="yundream@gmail.com", address="seoul";
configureAPI함수를 수정했다.
    // ......
    db, err := sql.Open("mysql", "root:gkwlak@unix(/var/run/mysqld/mysqld.sock)/userInfo")
    if err != nil {
        panic(err)             
    }

    api.UserGetSearchHandler = user.GetSearchHandlerFunc(func(params user.GetSearchParams) middleware.Responder {
        var (
            name    string
            email   string
            address string
        )
        _ = db.QueryRow("SELECT name, email, address FROM user WHERE id=?", *params.Body.UserID).
            Scan(&name, &email, &address)   
        info := models.User{Address: address,
            Email:  email,
            Name:   name,
            UserID: params.Body.UserID}     
        return &UserInfoOK{Body: info}  
    })

Path로 API 호출하기

유저를 찍어서 가져오는 GET /user/{id} API를 추가해 보자. swagger에 스팩을 추가하고 서버 코드를 다시 만들었다. 파라메터의 이름은 "id"다. 이 파라메터는 URL 패스로 전달되므로 "in: path"로 설정했다. tags로 "user"를 설정했다. swagger는 GET /usr/{id} 코드를 /restapi/operations/user 디렉토리 밑에 만든다. 여기에서 우리는 "targs"가 API를 카테고리로 묶기 위해서 사용하는 키워드라는 것을 알 수 있다. user 디렉토리 밑에는 user tags를 가지는 파일들이 위치한다.
$ ls restapi/operations/user
get_search.go             get_search_responses.go   get_user_id.go             get_user_id_responses.go
get_search_parameters.go  get_search_urlbuilder.go  get_user_id_parameters.go  get_user_id_urlbuilder.go
이제 핸들러를 구현해보자.
    api.UserGetUserIDHandler = user.GetUserIDHandlerFunc(func(params user.GetUserIDParams) middleware.Responder {
        var (
            name    string
            email   string
            address string
        )
        _ = db.QueryRow("SELECT name, email, address FROM user WHERE id=?", params.ID).
            Scan(&name, &email, &address)
        info := models.User{Address: address,
            Email:  email,
            Name:   name,
            UserID: &params.ID}
        return &UserInfoOK{Body: info}                                                                                                                                    
    })
  • user.GetUserIDHandlerFunc는 restapi/operations/user/get_user_id.go 파일에서 찾을 수 있다.
  • api.UserGetUserIDHandler는 restapi/operations/user_api.go 파일에서 찾을 수 있다.
  • user.GetUserIDParams는 restapi/operations/user/get_user_id_parameters.go에서 찾을 수 있다.
프로그램을 테스트해보자.
$ curl localhost:9000/user/yundream -XGET 
{"address":"seoul","email":"yundream@gmail.com","name":"yun","user_id":"yundream"}

데이터 POST 하기

유저 데이터 입력도 그냥 데이터베이스 Insert 이겠지만, 여기까지 왔으니 POST /user 도 구현하기로 했다. 스펙파일을 수정한다. 두어개 만들어본 경험이 있다고, 요령이 생겨서 쉽게 만들 수 있었다. 요청 바디(body)로 user 스키마에 설정된 JSON 포멧의 데이터를 받겠다는 얘기가 되겠다. swagger로 코드를 만들고 configure_user.go에 API를 구현한다.
type UserInfoError struct {
    Body models.Error `json:"body"`
}

func (u *UserInfoError) WriteResponse(w http.ResponseWriter, producer runtime.Producer) {
    producer.Produce(w, u.Body)
}
func configureAPI(api *operations.UserAPI) http.Handler {
    // 생략 ... 
    api.UserPostUserHandler = user.PostUserHandlerFunc(func(params user.PostUserParams) middleware.Responder {
        _, err := db.Exec("INSERT INTO user SET name=?, email=?, address=?, id=?",                                                                                        
            params.Body.Name,
            params.Body.Email,
            params.Body.Address,            
            params.Body.UserID)
        if err != nil {
            return &UserInfoError{Body: models.Error{Code: 500, Message: err.Error()}}                                                                                    
        }
        return &UserInfoOK{Body: *params.Body}                                                                                                                            
    })
    // 생략 ....
}
에러 코드를 추가한 것외에 별다른 것 없다. 테스트를 수행했다.
$ curl localhost:9000/user -XPOST -d '{"user_id":"foo","email":"foo@example.com","address":"Seoul Samsung", "name":"foo micle"}' \
-H "content-type: application/joinc.user-address.v1+json"
{"address":"Seoul Samsung","email":"foo@example.com","name":"foo micle","user_id":"foo"}
$ curl localhost:9000/user/foo -XGET 
{"address":"Seoul Samsung","email":"foo@example.com","name":"foo micle","user_id":"foo"}

미들웨어

... 정리 중

API 테스트

... 정리 중