Gin 프레임워크
Gin은 Go에서 가장 널리 사용되는 HTTP 웹 프레임워크입니다. httprouter 기반의 고성능 라우터를 사용하며, 풍부한 미들웨어 생태계와 간결한 API로 빠르게 REST API를 구축할 수 있습니다. 벤치마크에서 net/http에 근접한 성능을 보이면서도 개발 생산성이 훨씬 높습니다.
설치 및 기본 사용
go get github.com/gin-gonic/gin
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
// gin.Default(): Logger + Recovery 미들웨어 포함
// gin.New(): 미들웨어 없이 순수한 엔진
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
// 서버 시작 (기본 :8080)
r.Run(":8080")
}
라우팅
package main
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
// HTTP 메서드별 라우트 등록
r.GET("/users", listUsers)
r.POST("/users", createUser)
r.PUT("/users/:id", updateUser)
r.PATCH("/users/:id", patchUser)
r.DELETE("/users/:id", deleteUser)
r.OPTIONS("/users", optionsUsers)
r.Any("/any", anyHandler) // 모든 메서드 처리
// 경로 파라미터
r.GET("/users/:id", func(c *gin.Context) {
id := c.Param("id") // :id 값 추출
c.String(http.StatusOK, "사용자 ID: %s", id)
})
// 와일드카드 파라미터 (*로 시작, 나머지 경로 전체 매칭)
r.GET("/files/*filepath", func(c *gin.Context) {
filepath := c.Param("filepath")
c.String(http.StatusOK, "파일 경로: %s", filepath)
})
// 쿼리 파라미터
r.GET("/search", func(c *gin.Context) {
q := c.Query("q") // 단일 쿼리 파라미터
page := c.DefaultQuery("page", "1") // 기본값 지정
tags := c.QueryArray("tag") // 다중 값 (tag=go&tag=api)
fmt.Printf("q=%s, page=%s, tags=%v\n", q, page, tags)
c.JSON(http.StatusOK, gin.H{"q": q, "page": page, "tags": tags})
})
r.Run(":8080")
}
func listUsers(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"users": []string{}}) }
func createUser(c *gin.Context) { c.JSON(http.StatusCreated, gin.H{"id": 1}) }
func updateUser(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"updated": true}) }
func patchUser(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"patched": true}) }
func deleteUser(c *gin.Context) { c.Status(http.StatusNoContent) }
func optionsUsers(c *gin.Context) { c.Status(http.StatusNoContent) }
func anyHandler(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"method": c.Request.Method}) }
라우터 그룹
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
// v1 그룹
v1 := r.Group("/api/v1")
{
users := v1.Group("/users")
{
users.GET("", listUsersV1)
users.POST("", createUserV1)
users.GET("/:id", getUserV1)
}
posts := v1.Group("/posts")
{
posts.GET("", listPostsV1)
posts.POST("", createPostV1)
}
}
// v2 그룹 (인증 미들웨어 적용)
v2 := r.Group("/api/v2")
v2.Use(authMiddleware())
{
v2.GET("/users", listUsersV2)
}
r.Run(":8080")
}
func authMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": gin.H{"code": "UNAUTHORIZED", "message": "인증이 필요합니다"},
})
return
}
c.Next()
}
}
func listUsersV1(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"version": "v1"}) }
func createUserV1(c *gin.Context) { c.JSON(http.StatusCreated, gin.H{"version": "v1"}) }
func getUserV1(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"id": c.Param("id")}) }
func listPostsV1(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"posts": []any{}}) }
func createPostV1(c *gin.Context) { c.JSON(http.StatusCreated, gin.H{"version": "v1"}) }
func listUsersV2(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"version": "v2"}) }
요청 바인딩 및 유효성 검사
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
// CreateUserRequest 사용자 생성 요청 구조체
type CreateUserRequest struct {
Name string `json:"name" binding:"required,min=1,max=50"`
Email string `json:"email" binding:"required,email"`
Age int `json:"age" binding:"omitempty,min=1,max=150"`
Role string `json:"role" binding:"omitempty,oneof=admin user guest"`
}
// QueryRequest 쿼리 파라미터 바인딩
type QueryRequest struct {
Page int `form:"page" binding:"omitempty,min=1"`
Limit int `form:"limit" binding:"omitempty,min=1,max=100"`
Q string `form:"q"`
}
// PathRequest 경로 파라미터 바인딩
type PathRequest struct {
ID int64 `uri:"id" binding:"required,min=1"`
}
func main() {
r := gin.Default()
// JSON 바인딩
r.POST("/users", func(c *gin.Context) {
var req CreateUserRequest
// ShouldBind: 에러 반환 (BindJSON: 에러 시 자동으로 400 응답)
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusUnprocessableEntity, gin.H{
"error": gin.H{
"code": "VALIDATION_ERROR",
"message": err.Error(),
},
})
return
}
c.JSON(http.StatusCreated, gin.H{"data": req})
})
// 쿼리 파라미터 바인딩
r.GET("/users", func(c *gin.Context) {
var q QueryRequest
if err := c.ShouldBindQuery(&q); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if q.Page == 0 { q.Page = 1 }
if q.Limit == 0 { q.Limit = 20 }
c.JSON(http.StatusOK, gin.H{"page": q.Page, "limit": q.Limit, "q": q.Q})
})
// 경로 파라미터 바인딩
r.GET("/users/:id", func(c *gin.Context) {
var p PathRequest
if err := c.ShouldBindUri(&p); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "유효하지 않은 ID"})
return
}
c.JSON(http.StatusOK, gin.H{"id": p.ID})
})
r.Run(":8080")
}
미들웨어
package main
import (
"log/slog"
"net/http"
"os"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
// RequestIDMiddleware 요청 ID 주입
func RequestIDMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
id := c.GetHeader("X-Request-ID")
if id == "" {
id = uuid.New().String()
}
c.Set("request_id", id)
c.Header("X-Request-ID", id)
c.Next() // 다음 핸들러/미들웨어 실행
}
}
// SlogMiddleware slog 기반 구조화 로깅
func SlogMiddleware(logger *slog.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
logger.Info("request",
slog.String("request_id", c.GetString("request_id")),
slog.String("method", c.Request.Method),
slog.String("path", c.Request.URL.Path),
slog.Int("status", c.Writer.Status()),
slog.Duration("latency", time.Since(start)),
slog.String("ip", c.ClientIP()),
)
}
}
// c.Abort() vs c.Next()
// c.Next(): 다음 핸들러 실행 후 현재 핸들러로 돌아옴
// c.Abort(): 이후 핸들러 실행 중단 (현재 핸들러는 계속 실행)
// c.AbortWithStatus()/c.AbortWithStatusJSON(): Abort + 응답 동시 처리
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
r := gin.New() // 미들웨어 없이 시작
r.Use(
gin.CustomRecovery(func(c *gin.Context, err any) {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"error": gin.H{"code": "INTERNAL_ERROR", "message": "서버 오류가 발생했습니다"},
})
}),
RequestIDMiddleware(),
SlogMiddleware(logger),
)
r.GET("/users", func(c *gin.Context) {
// c.Set으로 저장된 값 꺼내기
requestID := c.GetString("request_id")
c.JSON(http.StatusOK, gin.H{
"request_id": requestID,
"users": []any{},
})
})
r.Run(":8080")
}
실전 예제: JWT 인증이 포함된 User CRUD API
package main
import (
"net/http"
"strconv"
"sync"
"time"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
)
var secret = []byte("my-secret-key")
type User struct {
ID int64 `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
CreatedAt time.Time `json:"created_at"`
}
type UserStore struct {
mu sync.RWMutex
users map[int64]*User
nextID int64
}
func NewUserStore() *UserStore {
s := &UserStore{users: make(map[int64]*User), nextID: 1}
// 테스트용 초기 데이터
s.users[1] = &User{ID: 1, Name: "관리자", Email: "admin@example.com", CreatedAt: time.Now()}
s.nextID = 2
return s
}
func (s *UserStore) List() []*User {
s.mu.RLock()
defer s.mu.RUnlock()
users := make([]*User, 0, len(s.users))
for _, u := range s.users {
users = append(users, u)
}
return users
}
func (s *UserStore) Get(id int64) (*User, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
u, ok := s.users[id]
return u, ok
}
func (s *UserStore) Create(name, email string) *User {
s.mu.Lock()
defer s.mu.Unlock()
u := &User{ID: s.nextID, Name: name, Email: email, CreatedAt: time.Now()}
s.users[s.nextID] = u
s.nextID++
return u
}
func (s *UserStore) Delete(id int64) bool {
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.users[id]; !ok {
return false
}
delete(s.users, id)
return true
}
// JWT 토큰 발급 (로그인)
func loginHandler(c *gin.Context) {
var body struct {
Email string `json:"email" binding:"required"`
Password string `json:"password" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": gin.H{"code": "BAD_REQUEST", "message": err.Error()}})
return
}
// 실제로는 DB에서 사용자 확인
if body.Email != "admin@example.com" || body.Password != "password" {
c.JSON(http.StatusUnauthorized, gin.H{
"error": gin.H{"code": "INVALID_CREDENTIALS", "message": "이메일 또는 비밀번호가 올바르지 않습니다"},
})
return
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"user_id": 1,
"email": body.Email,
"exp": time.Now().Add(24 * time.Hour).Unix(),
})
tokenString, _ := token.SignedString(secret)
c.JSON(http.StatusOK, gin.H{"token": tokenString})
}
// JWT 검증 미들웨어
func jwtAuth() gin.HandlerFunc {
return func(c *gin.Context) {
auth := c.GetHeader("Authorization")
if len(auth) < 8 || auth[:7] != "Bearer " {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": gin.H{"code": "UNAUTHORIZED", "message": "Bearer 토큰이 필요합니다"},
})
return
}
claims := jwt.MapClaims{}
t, err := jwt.ParseWithClaims(auth[7:], claims, func(t *jwt.Token) (any, error) {
return secret, nil
})
if err != nil || !t.Valid {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": gin.H{"code": "INVALID_TOKEN", "message": "유효하지 않은 토큰"},
})
return
}
c.Set("user_id", claims["user_id"])
c.Next()
}
}
func main() {
store := NewUserStore()
r := gin.Default()
// 공개 라우트
r.POST("/api/login", loginHandler)
// 인증 필요 라우트
api := r.Group("/api/v1")
api.Use(jwtAuth())
{
users := api.Group("/users")
users.GET("", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"data": store.List()})
})
users.POST("", func(c *gin.Context) {
var body struct {
Name string `json:"name" binding:"required"`
Email string `json:"email" binding:"required,email"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusUnprocessableEntity, gin.H{
"error": gin.H{"code": "VALIDATION_ERROR", "message": err.Error()},
})
return
}
c.JSON(http.StatusCreated, gin.H{"data": store.Create(body.Name, body.Email)})
})
users.GET("/:id", func(c *gin.Context) {
id, _ := strconv.ParseInt(c.Param("id"), 10, 64)
u, ok := store.Get(id)
if !ok {
c.JSON(http.StatusNotFound, gin.H{
"error": gin.H{"code": "NOT_FOUND", "message": "사용자를 찾을 수 없습니다"},
})
return
}
c.JSON(http.StatusOK, gin.H{"data": u})
})
users.DELETE("/:id", func(c *gin.Context) {
id, _ := strconv.ParseInt(c.Param("id"), 10, 64)
if !store.Delete(id) {
c.JSON(http.StatusNotFound, gin.H{
"error": gin.H{"code": "NOT_FOUND", "message": "사용자를 찾을 수 없습니다"},
})
return
}
c.Status(http.StatusNoContent)
})
}
r.Run(":8080")
}
고수 팁
1. gin.New() vs gin.Default(): 프로덕션에서는 gin.New()로 시작해 필요한 미들웨어만 직접 추가하세요. gin.Default()의 기본 로거는 구조화 로깅을 지원하지 않습니다.
2. ShouldBind vs Bind: Bind는 실패 시 자동으로 400 응답을 쓰고 c.Abort()를 호출합니다. 커스텀 에러 응답이 필요하면 반드시 ShouldBind를 사용하세요.
3. c.Abort()로 미들웨어 체인 중단: 인증 실패 시 return만 하면 다음 핸들러가 실행될 수 있습니다. 반드시 c.AbortWithStatusJSON()을 사용하세요.
4. gin.H는 map[string]any의 별칭: 짧고 편리하지만, 복잡한 응답은 구조체를 정의해 타입 안전성을 확보하세요.
5. 릴리즈 모드: GIN_MODE=release 환경변수를 설정하면 불필요한 디버그 로그가 사라지고 성능이 향상됩니다.