Go 구조체가 이미 API를 정의합니다. 문서도 자동으로 생성하게 하세요.
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/oapi — go 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=2 를 min=3 으로 바꾸거나, 새 필드를 추가하면 바인딩, 검증, 스키마가 모두 동시에 업데이트됩니다. 왜냐하면 이 세 가지가 동일 선언에 기반하기 때문입니다.
2. 문서는 스스로 생성되고, 항상 정확합니다
Registry 가 라우트를 수집해 검증된 OpenAPI 3 문서(JSON 혹은 YAML) 로 변환합니다. 별도의 스펙 파일이 아니라 캡처된 Go 타입을 읽어들이기 때문에, 문서는 핸들러가 실제로 바인딩하는 내용과 정확히 일치합니다.
아래는 위 구조체를 손으로 작성한 OpenAPI 없이 렌더링한 결과입니다.
자동으로 반영된 내용에 주목하세요:
category가 enum 형태로oneof값들을 보여줍니다.sku가uuid로 문서화됩니다.name은[2..120]문자 길이 제한을 표시합니다.website은url로 표시됩니다.tags는 최대 10개의 아이템을 갖는 배열로 나타납니다.- 요청 샘플은 실제
example값들을 사용해"string"같은 자리표시자가 없습니다.
이 모든 것이 타입에서 직접 읽어온 결과이며, 손으로 작성된 것이 전혀 없습니다.
3. 다섯 가지 프레임워크에서 동일한 요청·응답
동일한 []Route 가 net/http, gin, Fiber v2, chi, Echo v4 에서 그대로 동작합니다. 핵심 로직은 프레임워크에 독립적이며, 각 어댑터는 얇은 연결 고리일 뿐입니다. 프레임워크를 선택하거나 나중에 교체해도 핸들러 코드를 다시 작성할 필요가 없습니다.
성공 응답은 기본적으로 {"data": ...} 라는 단일 래퍼를 사용하고, 페이지네이션이 필요한 엔드포인트는 {"meta": ...} 를 추가합니다. 따라서 API 전반에 걸쳐 일관된 응답 형태를 제공하므로 클라이언트는 이를 신뢰하고 활용할 수 있습니다.
4. 의견이 반영되는 부분은 여전히 여러분이 직접 관리
표준화가 구속을 의미하지는 않습니다. 프로젝트마다 다르게 구현되는 부분은 플러그인 가능한 접점이며, 라이브러리는 자체 정책을 강제하지 않습니다.
- 검증은 인터페이스로 제공됩니다. 코어는 어떤 검증 라이브러리에도 의존하지 않으며,
go-playground/validator(예시 구현 포함) 혹은 직접 만든 검증기를 연결할 수 있습니다. - 응답 래퍼는 교체 가능합니다. 기본
{"data": ...}를 유지하거나, 전체 API에{"success": true, "data": ...}형태로 바꾸고, 라우트별로 오버라이드하거나, 래퍼

