Post

echo-swagger

Swagger는 API의 문서화를 자동으로 처리해주는 오픈 프로젝트다.
API가 변하면 자동으로 변한 API에 맞춰 문서화를 해주어 매우 편리하다.

시작

🖐️Swaggo

Swaggo는 이런 Swagger를 Golang에서 잘 활용할 수 있도록 도와준다.

  • swaggo/swag : RESTful API 문서 자동생성
  • swaggo/echo-swagger : Echo 프레임워크의 미들웨어로 동작하며 Swagger를 사용할 수 있도록 함.

🖐️Usage

우선 필요한 패키지의 설치를 한다.

1
2
go install github.com/swaggo/swag/cmd/swag@latest
go get -u github.com/swaggo/echo-swagger

이제 API 소스 코드에 Declartive Comments를 작성한다.
작성 방법은 공식 문서를 참고한다.

https://github.com/swaggo/swag?tab=readme-ov-file#declarative-comments-format

나는 BSMG 프로젝트에서 이렇게 사용하였다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
main.go

// @title BSMG Swagger API
// @version 1.0
// @host localhost:3000
// @BasePath /bsmg

// @license.name  Apache 2.0
// @license.url   http://www.apache.org/licenses/LICENSE-2.0.html
func main() {
    ...
    e := echo.New()
    ...
    // Swagger Docs 추가
	e.GET("/swagger/*", func(c echo.Context) error {
		// CORS 헤더 추가
		c.Response().Header().Set("Access-Control-Allow-Origin", "*")
		c.Response().Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
		c.Response().Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
		c.Response().Header().Set("Access-Control-Max-Age", "86400")

		return echoSwagger.WrapHandler(c)
	})
}

참고로 Nginx등을 이용하여 프록시 혹은 리버스 프록시를 하고 있다면, @host에도 맞게 변경해주어야 한다. (그렇지 않으면 Swagger 사용 시 Nginx conf를 아무리 고쳐도 CORS가 발생한다.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
handler.go

// @Summary user's login check
// @Description check user is logined
// @Tags Login
// @Accept json
// @Produce json
// @Success 200 {object} define.BsmgMemberResponse
// @Router /login/chkLogin [get]
func (h *BsmgHandler) GetChkLoginRequest(c echo.Context) error {
	log.Println("getChkLogin Req")
	// JWT 검증  ------------------------------------------
	apiResponse, resultCode := h.uc.CheckLoginIng(c)
	if resultCode != define.Success {
		apiResponse.Result.ResultCode = int32(resultCode)
		return c.JSON(http.StatusOK, apiResponse)
	}

	return c.JSON(http.StatusOK, apiResponse)
}


// @Summary get report List summary
// @Description combo means 0:all, 1:title, 2:content, 3:reporter
// @Tags Report
// @Accept json
// @Produce json
// @Param searchData query define.SearchData true "Search Condition"
// @Param offset query int true "offset"
// @Param limit query int true "limit"
// @Success 200 {object} define.BsmgReportListResponse
// @Router /report/reportList [get]
func (h *BsmgHandler) GetReportSearchReq(c echo.Context) (err error) {
	log.Println("getReportSearchReq")

	// server 변수를 현재 context에서 꺼내쓰는데 타입에 대해서 불명확하기때문에 이건 지양해야 하는 방향.
	server, _ := c.Get("Server").(*server.ServerProcessor)
	server.Mutex.Lock()
	defer server.Mutex.Unlock()

	var searchData define.SearchData

	searchCombo := c.Request().FormValue("@d1#search_combo")
	combo, _ := strconv.Atoi(searchCombo)
	searchData.SearchCombo = int32(combo)
	searchData.SearchInput = c.Request().FormValue("@d1#search_input")

	offset, _ := strconv.Atoi(c.Request().FormValue("offset"))
	limit, _ := strconv.Atoi(c.Request().FormValue("limit"))

	pageInfo := define.PageInfo{
		Offset: int32(offset),
		Limit:  int32(limit),
	}

	apiResponse := h.uc.SelectReportListReq(searchData, pageInfo)

	return c.JSON(http.StatusOK, apiResponse)
}

handler에서 Parameter로 콤보박스등을 지정하려면,

1
2
3
4
5
6
7
8
type SearchData struct {
	// * 0 - All
	// * 1 - Title
	// * 2 - Content
	// * 3 - Reporter
	SearchCombo int32  `json:"@d1#search_combo" enums:"0,1,2,3"`
	SearchInput string `json:"@d1#search_input"`
}

이렇게 정의에서 enums 태그를 달아주면 된다.

🖐️Docs 변환

main과 handler함수에 Declartive Comments를 달아주었으면, Swagger 문서로 변환할 차례다.

1
swag init

이 명령을 실행하면, 프로젝트의 root 디렉토리에 /docs 디렉토리가 생성된다.

1
2
3
4
5
mjy@mjyui-MacBookAir docs % tree
docs
├── docs.go
├── swagger.json
└── swagger.yaml

생성된 docs 패키지를 main.go에서 import 한다.

1
2
3
import (
    _ "BsmgRefactoring/docs" 
)

🖐️Swagger 확인

웹 URL에 /swagger/index.html을 붙이면 된다.

API에 변화를 주었거나, Declartive Comments를 변경하였다면,

1
swag init

을 통해서 Swagger 문서를 다시 생성해야 한다.

🖐️Trouble Shooting

BSMG 프로젝트는 SPA로 개발하여 index.html이 존재하고, 모든 정적파일을 Echo 프레임워크에 올려놓고 사용한다.

1
2
3
4
e.Use(middleware.Static("views/webRoot")) // eXBuilder6 의존성 파일 추가
e.GET("/", func(c echo.Context) error {
    return c.File("index.html")
})

내겐 이 부분이 문제가 되었다.
localhost/swagger/index.html로 접속하면 계속 SPA의 시작 index 파일이 호출되었다.

처음엔 라우팅 순서 문제인줄 알고 여러 시도를 해보았지만 아니었다.

1
2
3
4
5
echo의 미들웨어는 먼저 작성된 순서를 따라가지만, 그보다 우선순위가 있었다.

// Swagger Docs 추가
e.GET("/swagger/*", echoSwagger.WrapHandler)
e.Use(middleware.Static("views/webRoot")) // eXBuilder6 의존성 파일 추가

문제는 echo 프레임워크의 우선 순위 문제였다. echo 프레임워크는 정적 파일 서빙을 우선시 하여, echo-swagger의 라우팅을 다음과 같이 먼저 하여도 swagger가 아닌 정적 파일을 먼저 불러온다.

swagger UI의 index.html은 서버가 시작하고 생성된다.
echo-swagger를 사용하며 swagger UI를 들어가기 위해선 /{특정 URL}/index.html이 고정값이다.
echo-swagger 패키지의 코드를 고치고 싶지 않아서 고민하다, 라우팅에 hook을 추가하여 해결하였다.

1
2
3
4
5
6
7
8
9
10
11
// Swagger Docs 추가
e.GET("/swagger/*", echoSwagger.WrapHandler)
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
    return func(c echo.Context) error {
        // Swagger UI를 불러오는 URL (/swagger/index.html)이 정적파일의 index.html과 충돌하여 동작하지 않으므로 후킹 추가
        if c.Request().URL.Path == swaggerIndexURL {
            return echoSwagger.WrapHandler(c)
        }
        return next(c)
    }
})
This post is licensed under CC BY 4.0 by the author.