Go API에 Swagger 추가하기

코드 주석에서 자동으로 OpenAPI 문서 생성

Page content

API 문서화는 현대 애플리케이션에 있어 매우 중요하며, Go API Swagger (OpenAPI)는 산업 표준이 되었습니다. Go 개발자에게는 swaggo가 코드 주석에서 직접 포괄적인 API 문서를 생성하는 우아한 솔루션을 제공합니다.

swagger api specs on agile board 이 멋진 이미지는 AI 모델 Flux 1 dev에 의해 생성되었습니다.

Go API에 대한 Swagger의 중요성

REST API를 구축할 때, 코드가 진화함에 따라 문서가 오래되어 버리는 경우가 많습니다. Swagger는 이 문제를 해결하기 위해 소스 코드에서 문서를 생성함으로써 문서가 구현과 동기화되도록 합니다. 상호작용형 Swagger UI는 개발자가 브라우저에서 직접 엔드포인트를 테스트할 수 있게 해주어 개발자 경험을 크게 향상시킵니다.

마이크로서비스나 공개 API를 구축하는 팀에게는 Swagger 문서가 다음에 필수적입니다:

  • 클라이언트 생성: 여러 언어로 클라이언트 라이브러리를 자동 생성
  • 계약 테스트: 정의된 스키마에 대해 요청 및 응답을 검증
  • 팀 협업: API 계약에 대한 단일 진실의 출처 제공
  • 개발자 온보딩: 새로운 팀원이 API를 상호작용적으로 탐색할 수 있도록 함

swaggo 사용 시작

swaggo 라이브러리는 Go 애플리케이션에 Swagger 지원을 추가하는 가장 인기 있는 도구입니다. 이는 코드 내의 특별한 주석을 파싱하여 OpenAPI 3.0 명세 파일을 생성합니다.

설치

먼저 swag CLI 도구를 설치합니다:

go install github.com/swaggo/swag/cmd/swag@latest

그런 다음 프레임워크에 맞는 적절한 Swagger 미들웨어 패키지를 추가합니다. Gin을 사용하는 경우:

go get -u github.com/swaggo/gin-swagger
go get -u github.com/swaggo/files

Echo를 사용하는 경우:

go get -u github.com/swaggo/echo-swagger

Fiber를 사용하는 경우:

go get -u github.com/gofiber/swagger

기본 설정

main.go 파일에 일반적인 API 정보를 추가하여 시작합니다. Go에서 REST API 구현과 유사하게 주석은 명확하고 설명적이어야 합니다:

// @title           Product API
// @version         1.0
// @description     Swagger 문서화가 포함된 제품 관리 API
// @termsOfService  http://swagger.io/terms/

// @contact.name   API 지원
// @contact.url    http://www.swagger.io/support
// @contact.email  support@swagger.io

// @license.name  Apache 2.0
// @license.url   http://www.apache.org/licenses/LICENSE-2.0.html

// @host      localhost:8080
// @BasePath  /api/v1

// @securityDefinitions.apikey Bearer
// @in header
// @name Authorization
// @description "Bearer"를 입력한 후 공백과 JWT 토큰을 추가하세요.

func main() {
    // 애플리케이션 코드
}

Gin 프레임워크를 사용한 구현

Gin을 사용하여 완전한 예제를 구현해 보겠습니다. 먼저 구조체 태그와 함께 데이터 모델을 정의합니다:

type Product struct {
    ID          int     `json:"id" example:"1"`
    Name        string  `json:"name" example:"Laptop" binding:"required"`
    Description string  `json:"description" example:"High-performance laptop"`
    Price       float64 `json:"price" example:"999.99" binding:"required,gt=0"`
    Stock       int     `json:"stock" example:"50"`
}

type ErrorResponse struct {
    Error   string `json:"error" example:"Invalid input"`
    Message string `json:"message" example:"Product name is required"`
}

이제 핸들러 함수에 주석을 추가합니다. 데이터베이스 작업을 수행할 때, 이러한 주석은 데이터 흐름을 문서화하는 데 도움이 됩니다:

// GetProduct godoc
// @Summary      ID로 제품 가져오기
// @Description  고유 식별자로 단일 제품을 검색합니다.
// @Tags         products
// @Accept       json
// @Produce      json
// @Param        id   path      int  true  "제품 ID"
// @Success      200  {object}  Product
// @Failure      400  {object}  ErrorResponse
// @Failure      404  {object}  ErrorResponse
// @Router       /products/{id} [get]
func GetProduct(c *gin.Context) {
    id := c.Param("id")
    // 구현
    c.JSON(200, Product{ID: 1, Name: "Laptop", Price: 999.99})
}

// CreateProduct godoc
// @Summary      새 제품 생성
// @Description  카탈로그에 새 제품을 추가합니다.
// @Tags         products
// @Accept       json
// @Produce      json
// @Param        product  body      Product  true  "제품 객체"
// @Success      201      {object}  Product
// @Failure      400      {object}  ErrorResponse
// @Security     Bearer
// @Router       /products [post]
func CreateProduct(c *gin.Context) {
    var product Product
    if err := c.ShouldBindJSON(&product); err != nil {
        c.JSON(400, ErrorResponse{Error: "Bad Request", Message: err.Error()})
        return
    }
    // 데이터베이스에 저장
    c.JSON(201, product)
}

문서 생성

코드에 주석을 추가한 후 Swagger 문서를 생성합니다:

swag init

이렇게 하면 docs 폴더에 swagger.json, swagger.yaml, Go 파일이 생성됩니다. Swagger 엔드포인트를 임포트하고 등록합니다:

package main

import (
    "github.com/gin-gonic/gin"
    swaggerFiles "github.com/swaggo/files"
    ginSwagger "github.com/swaggo/gin-swagger"
    
    _ "yourproject/docs" // 생성된 문서 임포트
)

func main() {
    r := gin.Default()
    
    // API 라우트
    v1 := r.Group("/api/v1")
    {
        v1.GET("/products/:id", GetProduct)
        v1.POST("/products", CreateProduct)
    }
    
    // Swagger 엔드포인트
    r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
    
    r.Run(":8080")
}

이제 http://localhost:8080/swagger/index.html에서 상호작용형 API 문서를 확인할 수 있습니다.

Echo 프레임워크를 사용한 구현

Echo 사용자는 유사한 패턴을 따르지만 Echo 전용 미들웨어를 사용합니다:

package main

import (
    "github.com/labstack/echo/v4"
    echoSwagger "github.com/swaggo/echo-swagger"
    
    _ "yourproject/docs"
)

func main() {
    e := echo.New()
    
    // API 라우트
    api := e.Group("/api/v1")
    api.GET("/products/:id", getProduct)
    api.POST("/products", createProduct)
    
    // Swagger 엔드포인트
    e.GET("/swagger/*", echoSwagger.WrapHandler)
    
    e.Start(":8080")
}

Fiber 프레임워크를 사용한 구현

Fiber의 구현도 매우 간단합니다:

package main

import (
    "github.com/gofiber/fiber/v2"
    "github.com/gofiber/swagger"
    
    _ "yourproject/docs"
)

func main() {
    app := fiber.New()
    
    // API 라우트
    api := app.Group("/api/v1")
    api.Get("/products/:id", getProduct)
    api.Post("/products", createProduct)
    
    // Swagger 엔드포인트
    app.Get("/swagger/*", swagger.HandlerDefault)
    
    app.Listen(":8080")
}

고급 Swagger 주석

복잡한 요청 본문 문서화

중첩 구조체 또는 배열을 위한 예시:

type CreateOrderRequest struct {
    CustomerID int           `json:"customer_id" example:"123" binding:"required"`
    Items      []OrderItem   `json:"items" binding:"required,min=1"`
    ShippingAddress Address  `json:"shipping_address" binding:"required"`
}

type OrderItem struct {
    ProductID int `json:"product_id" example:"1" binding:"required"`
    Quantity  int `json:"quantity" example:"2" binding:"required,min=1"`
}

type Address struct {
    Street  string `json:"street" example:"123 Main St" binding:"required"`
    City    string `json:"city" example:"New York" binding:"required"`
    ZipCode string `json:"zip_code" example:"10001" binding:"required"`
}

// CreateOrder godoc
// @Summary      새 주문 생성
// @Description  여러 항목과 배송 정보를 포함한 주문 생성
// @Tags         orders
// @Accept       json
// @Produce      json
// @Param        order  body      CreateOrderRequest  true  "주문 세부 정보"
// @Success      201    {object}  Order
// @Failure      400    {object}  ErrorResponse
// @Failure      422    {object}  ErrorResponse
// @Security     Bearer
// @Router       /orders [post]
func CreateOrder(c *gin.Context) {
    // 구현
}

파일 업로드 문서화

// UploadImage godoc
// @Summary      제품 이미지 업로드
// @Description  제품에 대한 이미지 파일 업로드
// @Tags         products
// @Accept       multipart/form-data
// @Produce      json
// @Param        id    path      int   true  "제품 ID"
// @Param        file  formData  file  true  "이미지 파일"
// @Success      200   {object}  map[string]string
// @Failure      400   {object}  ErrorResponse
// @Security     Bearer
// @Router       /products/{id}/image [post]
func UploadImage(c *gin.Context) {
    file, _ := c.FormFile("file")
    // 업로드 처리
}

쿼리 매개변수 및 페이징

// ListProducts godoc
// @Summary      페이징과 함께 제품 목록 보기
// @Description  필터링 옵션을 포함한 제품의 페이징된 목록 가져오기
// @Tags         products
// @Accept       json
// @Produce      json
// @Param        page      query     int     false  "페이지 번호" default(1)
// @Param        page_size query     int     false  "페이지 당 항목 수" default(10)
// @Param        category  query     string  false  "카테고리로 필터링"
// @Param        min_price query     number  false  "최소 가격"
// @Param        max_price query     number  false  "최대 가격"
// @Success      200       {array}   Product
// @Failure      400       {object}  ErrorResponse
// @Router       /products [get]
func ListProducts(c *gin.Context) {
    // 페이징을 포함한 구현
}

인증 및 보안

API에 다양한 인증 방법을 문서화합니다. 다중 테넌트 애플리케이션을 위해 적절한 인증 문서화가 필수적입니다:

Bearer 토큰 인증

// @securityDefinitions.apikey Bearer
// @in header
// @name Authorization
// @description "Bearer"를 입력한 후 공백과 JWT 토큰을 추가하세요.

API 키 인증

// @securityDefinitions.apikey ApiKeyAuth
// @in header
// @name X-API-Key
// @description 인증을 위한 API 키

OAuth2 인증

// @securitydefinitions.oauth2.application OAuth2Application
// @tokenUrl https://example.com/oauth/token
// @scope.write 쓰기 권한 부여
// @scope.admin 관리 정보에 대한 읽기 및 쓰기 권한 부여

기본 인증

// @securityDefinitions.basic BasicAuth

특정 엔드포인트에 보안 적용:

// @Security Bearer
// @Security ApiKeyAuth

Swagger UI 맞춤화

Swagger UI의 외관 및 행동을 맞춤화할 수 있습니다:

// 커스텀 구성
url := ginSwagger.URL("http://localhost:8080/swagger/doc.json")
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler, url))

// 커스텀 제목으로
r.GET("/swagger/*any", ginSwagger.WrapHandler(
    swaggerFiles.Handler,
    ginSwagger.URL("http://localhost:8080/swagger/doc.json"),
    ginSwagger.DefaultModelsExpandDepth(-1),
))

생산 환경에서 Swagger 비활성화:

if os.Getenv("ENV") != "production" {
    r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
}

CI/CD 통합

CI/CD 파이프라인에서 Swagger 문서 생성을 자동화합니다:

# GitHub Actions 예시
name: Swagger 문서 생성
on: [push]

jobs:
  swagger:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Go 설정
        uses: actions/setup-go@v4
        with:
          go-version: '1.21'
      
      - name: swag 설치
        run: go install github.com/swaggo/swag/cmd/swag@latest
      
      - name: Swagger 문서 생성
        run: swag init
      
      - name: 문서 커밋
        run: |
          git config user.name github-actions
          git config user.email github-actions@github.com
          git add docs/
          git commit -m "Swagger 문서 업데이트" || exit 0
          git push          

최고의 실천 방법

1. 일관된 주석 스타일 유지

모든 엔드포인트에서 일관된 포맷을 유지하세요:

// HandlerName godoc
// @Summary      간단한 설명 (50자 미만)
// @Description  엔드포인트가 수행하는 작업에 대한 자세한 설명
// @Tags         리소스 이름
// @Accept       json
// @Produce      json
// @Param        이름  위치  유형  필수  "설명"
// @Success      200   {object}  응답 유형
// @Failure      400   {object}  ErrorResponse
// @Router       /경로 [메서드]

2. 설명적인 예시 사용

API 사용자에게 도움이 되는 실제 예시를 추가하세요:

type User struct {
    ID        int       `json:"id" example:"1"`
    Email     string    `json:"email" example:"user@example.com"`
    CreatedAt time.Time `json:"created_at" example:"2025-01-15T10:30:00Z"`
}

3. 모든 응답 코드 문서화

모든 가능한 HTTP 상태 코드를 포함하세요:

// @Success      200  {object}  Product
// @Success      201  {object}  Product
// @Failure      400  {object}  ErrorResponse "Bad Request"
// @Failure      401  {object}  ErrorResponse "Unauthorized"
// @Failure      403  {object}  ErrorResponse "Forbidden"
// @Failure      404  {object}  ErrorResponse "Not Found"
// @Failure      422  {object}  ErrorResponse "Validation Error"
// @Failure      500  {object}  ErrorResponse "Internal Server Error"

4. API 버전 관리

기본 경로에 적절한 버전 관리를 사용하세요:

// @BasePath  /api/v1

코드를 다음과 같이 구성하세요:

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

5. 관련 엔드포인트 그룹화

논리적으로 엔드포인트를 구성하기 위해 태그를 사용하세요:

// @Tags products
// @Tags orders
// @Tags users

6. 문서 업데이트 유지

각 커밋 전에 swag init을 실행하거나 빌드 프로세스에 통합하세요:

#!/bin/bash
# pre-commit 훅
swag init
git add docs/

Swagger 문서 테스트

AWS Lambda와 같은 서버리스 아키텍처와 함께 작업할 때 API 문서 테스트는 더욱 중요해집니다:

func TestSwaggerGeneration(t *testing.T) {
    // swagger.json이 존재하는지 확인
    _, err := os.Stat("./docs/swagger.json")
    if err != nil {
        t.Fatal("swagger.json not found, run 'swag init'")
    }
    
    // swagger 엔드포인트가 응답하는지 확인
    r := setupRouter()
    w := httptest.NewRecorder()
    req, _ := http.NewRequest("GET", "/swagger/index.html", nil)
    r.ServeHTTP(w, req)
    
    assert.Equal(t, 200, w.Code)
}

일반적인 문제 및 해결 방법

문서가 업데이트되지 않음

변경사항이 반영되지 않으면 문서를 다시 생성하세요:

swag init --parseDependency --parseInternal

--parseDependency 플래그는 외부 종속성을 파싱하고 --parseInternal은 내부 패키지를 파싱합니다.

커스텀 타입이 인식되지 않음

외부 패키지의 타입을 사용하는 경우 swaggertype 태그를 사용하세요:

type CustomTime struct {
    time.Time
}

func (CustomTime) SwaggerDoc() map[string]string {
    return map[string]string{
        "time": "RFC3339 타임스탬프",
    }
}

또는 swaggertype 태그를 사용하세요:

type Product struct {
    ID        int        `json:"id"`
    UpdatedAt CustomTime `json:"updated_at" swaggertype:"string" format:"date-time"`
}

배열 및 열거형

배열 타입 및 열거형을 문서화하세요:

type Filter struct {
    Status []string `json:"status" enums:"active,inactive,pending"`
    Tags   []string `json:"tags"`
}

// @Param  status  query  string  false  "상태 필터" Enums(active, inactive, pending)

대안 접근 방법

swaggo가 가장 인기 있는 선택이지만, 다른 옵션도 존재합니다:

go-swagger

더 많은 기능을 제공하지만 복잡한 대안입니다:

brew install go-swagger
swagger generate spec -o ./swagger.json

수동 OpenAPI 파일

완전한 제어를 위해 YAML로 OpenAPI 명세를 수동으로 작성할 수 있습니다:

openapi: 3.0.0
info:
  title: Product API
  version: 1.0.0
paths:
  /products:
    get:
      summary: 제품 목록 보기
      responses:
        '200':
          description: 성공

그런 다음 다음과 같이 제공하세요:

r.StaticFile("/openapi.yaml", "./openapi.yaml")

AI 및 LLM과의 통합

AI 서비스와 통합하는 API를 구축할 때, 문서화는 매우 중요합니다. 예를 들어, 구조화된 LLM 출력을 사용할 때, Swagger는 복잡한 요청 및 응답 스키마를 문서화하는 데 도움이 됩니다:

type LLMRequest struct {
    Prompt      string            `json:"prompt" example:"이 텍스트를 요약하세요"`
    Model       string            `json:"model" example:"qwen2.5:latest"`
    Temperature float64           `json:"temperature" example:"0.7" minimum:"0" maximum:"2"`
    MaxTokens   int               `json:"max_tokens" example:"1000" minimum:"1"`
    Schema      map[string]interface{} `json:"schema,omitempty"`
}

// GenerateStructured godoc
// @Summary      구조화된 LLM 출력 생성
// @Description  제약된 출력 스키마를 사용한 텍스트 생성
// @Tags         llm
// @Accept       json
// @Produce      json
// @Param        request  body      LLMRequest  true  "LLM 매개변수"
// @Success      200      {object}  map[string]interface{}
// @Failure      400      {object}  ErrorResponse
// @Router       /llm/generate [post]
func GenerateStructured(c *gin.Context) {
    // 구현
}

성능 고려사항

Swagger 문서화는 거의 성능에 영향을 미치지 않습니다:

  • 빌드 시간: 대부분의 프로젝트에서 swag init은 1-3초가 소요됩니다.
  • 런타임: 문서는 시작 시에만 로드됩니다.
  • 메모리: 일반적으로 이진 파일 크기에 1-2MB를 추가합니다.
  • 응답 시간: API 엔드포인트 자체에는 영향을 주지 않습니다.

100개 이상의 엔드포인트가 있는 매우 큰 API의 경우 다음을 고려하세요:

  • 여러 개의 Swagger 파일로 분할
  • Swagger UI 자산을 지연 로딩
  • 문서를 별도 서비스에서 제공

보안 고려사항

Swagger 문서를 노출할 때 다음을 고려하세요:

  1. 생산 환경에서 비활성화 (내부 API인 경우):
if os.Getenv("ENV") == "production" {
    // Swagger 엔드포인트를 등록하지 않음
    return
}
  1. 인증 추가:
authorized := r.Group("/swagger")
authorized.Use(AuthMiddleware())
authorized.GET("/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
  1. 엔드포인트에 대한 레이트 제한:
r.GET("/swagger/*any", RateLimitMiddleware(), ginSwagger.WrapHandler(swaggerFiles.Handler))
  1. 내부 세부 정보 노출 금지:
  • 내부 엔드포인트를 문서화하지 않음
  • 데이터베이스 스키마를 직접 노출하지 않음
  • 문서화에서 오류 메시지를 정화

결론

Go API에 Swagger 문서를 추가하면 개발자 경험을 추측에서 지도된 탐색으로 바꿉니다. swaggo 라이브러리는 코드 주석에서 포괄적인 OpenAPI 문서를 생성함으로써 이 과정을 간단하게 만듭니다.

핵심 요약:

  • 기본 주석으로 시작하고 점진적으로 확장
  • CI/CD를 통해 문서와 코드를 동기화
  • 개발 중에 상호작용형 테스트를 위해 Swagger UI 사용
  • 인증, 오류, 경계 사례를 철저히 문서화
  • 문서를 노출할 때 보안 고려사항을 고려

마이크로서비스, 공개 API, 내부 도구를 구축하던지간에 Swagger 문서는 지원 부담 감소, 더 빠른 온보딩, 더 나은 API 설계에 이점을 제공합니다. 학습 주석 구문에 대한 초기 투자금은 빠르게 일상이 되며, 자동 생성은 문서가 구현을 따라가지 않도록 보장합니다.

Go 개발자에게 강한 타이핑, 코드 생성 및 swaggo의 주석 시스템의 조합은 API 문서화를 개발 과정의 자연스러운 일부로 만들어주는 강력한 워크플로우를 생성합니다.

유용한 링크

외부 자원