Go 구조체가 이미 API를 정의합니다. 문서도 자동으로 생성하게 하세요.

발행: (2026년 6월 7일 PM 01:04 GMT+9)
9 분 소요
원문: Dev.to

Source: Dev.to

당신의 Go 구조체는 이미 API를 설명하고 있습니다. 이제 문서도 자동으로 작성하게 하세요.

Go 로 HTTP API 를 만들다 보면 다음과 같은 상황을 겪게 됩니다. 같은 엔드포인트가 세 곳에서 수작업으로 정의되고, 세 곳 모두가 일치해야 합니다.

핸들러는 본문을 읽고, 쿼리 파라미터와 헤더를 파싱합니다 — 바인딩.

밸리데이터는 본문이 올바른 형식인지 확인합니다 — required, email, 범위 등.

OpenAPI 문서는 클라이언트에게 엔드포인트가 기대하고 반환하는 내용을 알려줍니다.

코드 자체가 이미 이 모든 정보를 알고 있습니다. 요청을 위한 Go 타입은 어떤 필드가 존재하고, 타입은 무엇이며, 어떤 필드가 필수인지 정확히 명시합니다. 그런데 문서는 YAML 로, 밸리데이터는 태그로, 그리고 이 둘을 동기화해 주는 장치는 없습니다. 필드 이름을 바꾸고 배포해도 문서는 조용히 거짓 정보를 제공하게 되며, 누군가 버그를 제기하기 전까지는 알 수 없습니다.

이러한 상황이 수십 개의 라우트에 걸쳐, gin, chi, net/http 등 각각의 프레임워크마다 조금씩 다르게 연결돼 있다면, “문서”는 별도의 코드베이스가 되어 규율만으로 유지해야 하는 부담이 됩니다.

oapi는 그 세 곳 중 두 곳을 없앱니다. 요청과 응답을 타입이 지정된 Go 구조체 하나로 정의하면, 바인딩, 검증, OpenAPI 3 문서가 모두 같은 구조체 태그를 읽어 사용합니다. 따라서 문서가 타입과 동기화되지 않을 가능성이 사라집니다.

GitHub: github.com/antlss/oapigo get github.com/antlss/oapi


한 줄 서명에 담긴 아이디어

모든 핸들러는 같은 형태를 가집니다: 타입이 지정된 요청을 받고, 타입이 지정된 응답을 반환합니다.

func(ctx context.Context, req oapi.Request[Header, Param, Query, Body]) (*Response, error)

Request[Header, Param, Query, Body] 가 전체 계약(contract)입니다. 각 파트는 서로 다른 소스에서 바인딩되며, 필요 없는 파트는 struct{} 로 표시합니다:

Header -> `header:"..."`   Param -> `uri:"..."`
Query  -> `form:"..."`     Body  -> `json:"..."` (또는 multipart/urlencoded 를 위한 `form:"..."`)

그게 전부입니다. 핸들러는 이미 파싱·검증·타입 지정된 데이터를 받게 됩니다. c.ShouldBindJSON(&x), c.Param("id") 같은 수작업 바인딩이나 if err != nil 같은 보일러플레이트 코드를 매 함수마다 작성할 필요가 없습니다.


타입이 제공하는 이점

1. 타입 자체가 전체 데이터 모델

요청을 한 번만 정의하면 됩니다. 구조체 태그는 동시에 세 가지 역할을 수행합니다:

type CreateProductBody struct {
    Name      string   `json:"name"      binding:"required,min=2,max=120"                   example:"Mechanical Keyboard"`
    SKU       string   `json:"sku"       binding:"required,uuid"                            example:"5f9c2e3a-1b4d-4c8e-9f0a-2b3c4d5e6f70"`
    Price     float64  `json:"price"     binding:"required,gt=0"                            example:"49.90"`
    Currency  string   `json:"currency"  binding:"required,oneof=USD EUR JPY VND"           example:"USD"`
    Category  string   `json:"category"  binding:"required,oneof=book electronics food toy" example:"electronics"`
    Website   string   `json:"website"   binding:"omitempty,url"                            example:"https://example.com"`
    Tags      []string `json:"tags"      binding:"omitempty,max=10"                         example:"new,featured"`
    Warehouse Address  `json:"warehouse"`
}
  • json:"name" — 본문이 어떻게 디코딩되는지 (바인딩)
  • binding:"required,min=2,max=120" — 검증 규칙이자, 문서에 들어갈 스키마 제약 (필수, minLength/maxLength)
  • example:"..." — Swagger UI / Redoc 에서 클라이언트가 보는 샘플 값

Warehouse Address 와 같은 중첩 구조체도 재귀적으로 처리되어, 그 규칙과 예시가 문서에 포함됩니다. 필드명을 바꾸거나, min=2min=3 으로 바꾸거나, 새 필드를 추가하면 바인딩, 검증, 스키마가 모두 동시에 업데이트됩니다. 왜냐하면 이 세 가지가 동일 선언에 기반하기 때문입니다.

2. 문서는 스스로 생성되고, 항상 정확합니다

Registry 가 라우트를 수집해 검증된 OpenAPI 3 문서(JSON 혹은 YAML) 로 변환합니다. 별도의 스펙 파일이 아니라 캡처된 Go 타입을 읽어들이기 때문에, 문서는 핸들러가 실제로 바인딩하는 내용과 정확히 일치합니다.

아래는 위 구조체를 손으로 작성한 OpenAPI 없이 렌더링한 결과입니다.

자동으로 반영된 내용에 주목하세요:

  • categoryenum 형태로 oneof 값들을 보여줍니다.
  • skuuuid 로 문서화됩니다.
  • name[2..120] 문자 길이 제한을 표시합니다.
  • websiteurl 로 표시됩니다.
  • tags 는 최대 10개의 아이템을 갖는 배열로 나타납니다.
  • 요청 샘플은 실제 example 값들을 사용해 "string" 같은 자리표시자가 없습니다.

이 모든 것이 타입에서 직접 읽어온 결과이며, 손으로 작성된 것이 전혀 없습니다.

3. 다섯 가지 프레임워크에서 동일한 요청·응답

동일한 []Routenet/http, gin, Fiber v2, chi, Echo v4 에서 그대로 동작합니다. 핵심 로직은 프레임워크에 독립적이며, 각 어댑터는 얇은 연결 고리일 뿐입니다. 프레임워크를 선택하거나 나중에 교체해도 핸들러 코드를 다시 작성할 필요가 없습니다.

성공 응답은 기본적으로 {"data": ...} 라는 단일 래퍼를 사용하고, 페이지네이션이 필요한 엔드포인트는 {"meta": ...} 를 추가합니다. 따라서 API 전반에 걸쳐 일관된 응답 형태를 제공하므로 클라이언트는 이를 신뢰하고 활용할 수 있습니다.

4. 의견이 반영되는 부분은 여전히 여러분이 직접 관리

표준화가 구속을 의미하지는 않습니다. 프로젝트마다 다르게 구현되는 부분은 플러그인 가능한 접점이며, 라이브러리는 자체 정책을 강제하지 않습니다.

  • 검증은 인터페이스로 제공됩니다. 코어는 어떤 검증 라이브러리에도 의존하지 않으며, go-playground/validator(예시 구현 포함) 혹은 직접 만든 검증기를 연결할 수 있습니다.
  • 응답 래퍼는 교체 가능합니다. 기본 {"data": ...} 를 유지하거나, 전체 API에 {"success": true, "data": ...} 형태로 바꾸고, 라우트별로 오버라이드하거나, 래퍼
0 조회
Back to Blog

관련 글

더 보기 »