🐹
Go 웹 프레임워크 가이드

Gin 완전 가이드

👥 방문자 수

Go 생태계에서 가장 인기 있는 HTTP 웹 프레임워크. 라우팅, 미들웨어, JSON 바인딩, GORM ORM, JWT 인증, Docker 배포까지 실무 중심으로 정리했습니다.

Gin v1 REST API GORM JWT 인증 Docker

1. Gin이란?

Gin은 Go로 작성된 고성능 HTTP 웹 프레임워크입니다. HttpRouter 기반으로 초당 수십만 요청을 처리할 수 있으며, 직관적인 API로 REST API를 빠르게 구축할 수 있습니다.

특징설명
성능Martini보다 40배 빠른 라우터 (HttpRouter 기반)
미들웨어Logger, Recovery, CORS 등 내장 미들웨어
바인딩JSON, XML, 폼 데이터, 쿼리 파라미터 자동 바인딩
검증go-playground/validator 통합

2. 프로젝트 설정

mkdir myapi && cd myapi
go mod init github.com/username/myapi

# Gin 및 주요 의존성 설치
go get github.com/gin-gonic/gin
go get gorm.io/gorm
go get gorm.io/driver/postgres
go get github.com/golang-jwt/jwt/v5
go get github.com/joho/godotenvbash
# 프로젝트 구조
myapi/
├── main.go
├── config/         # 환경 설정
├── handler/        # HTTP 핸들러
├── middleware/     # 미들웨어
├── model/          # GORM 모델
├── repository/     # DB 쿼리
├── service/        # 비즈니스 로직
└── router/         # 라우터 설정text

3. 라우팅

package main

import (
    "net/http"
    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default() // Logger + Recovery 미들웨어 포함

    // ── 기본 라우팅 ────────────────────────────────
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{"message": "pong"})
    })

    // ── 경로 파라미터 ───────────────────────────────
    r.GET("/users/:id", func(c *gin.Context) {
        id := c.Param("id")
        c.JSON(http.StatusOK, gin.H{"id": id})
    })

    // ── 쿼리 파라미터 ───────────────────────────────
    r.GET("/users", func(c *gin.Context) {
        page := c.DefaultQuery("page", "1")
        size := c.DefaultQuery("size", "20")
        c.JSON(http.StatusOK, gin.H{"page": page, "size": size})
    })

    // ── 라우터 그룹 ─────────────────────────────────
    api := r.Group("/api/v1")
    {
        users := api.Group("/users")
        users.GET("",     listUsers)
        users.GET("/:id", getUser)
        users.POST("",    createUser)
        users.PUT("/:id", updateUser)
        users.DELETE("/:id", deleteUser)
    }

    r.Run(":8080")
}go

4. 미들웨어

package middleware

import (
    "net/http"
    "time"

    "github.com/gin-gonic/gin"
)

// 커스텀 로거 미들웨어
func Logger() gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()
        c.Next() // 다음 핸들러 실행
        duration := time.Since(start)

        log.Printf("%s %s %d %v",
            c.Request.Method,
            c.Request.URL.Path,
            c.Writer.Status(),
            duration,
        )
    }
}

// CORS 미들웨어
func CORS() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Header("Access-Control-Allow-Origin",  "*")
        c.Header("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS")
        c.Header("Access-Control-Allow-Headers", "Content-Type,Authorization")

        if c.Request.Method == "OPTIONS" {
            c.AbortWithStatus(http.StatusNoContent)
            return
        }
        c.Next()
    }
}

// Rate Limiting (간단한 예시)
func RateLimit(rps int) gin.HandlerFunc {
    limiter := rate.NewLimiter(rate.Limit(rps), rps)
    return func(c *gin.Context) {
        if !limiter.Allow() {
            c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
                "error": "요청 한도 초과",
            })
            return
        }
        c.Next()
    }
}

// 적용
r := gin.New()
r.Use(middleware.Logger())
r.Use(middleware.CORS())
r.Use(gin.Recovery()) // panic 복구go

5. 요청 바인딩 & 검증

// 요청 DTO
type CreateUserRequest struct {
    Name     string `json:"name"     binding:"required,min=2,max=50"`
    Email    string `json:"email"    binding:"required,email"`
    Password string `json:"password" binding:"required,min=8"`
    Age      int    `json:"age"      binding:"required,gte=0,lte=130"`
}

func createUser(c *gin.Context) {
    var req CreateUserRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        // 검증 오류를 사람이 읽기 좋은 형태로 변환
        var ve validator.ValidationErrors
        if errors.As(err, &ve) {
            out := make([]gin.H, len(ve))
            for i, fe := range ve {
                out[i] = gin.H{"field": fe.Field(), "message": fe.Tag()}
            }
            c.JSON(http.StatusBadRequest, gin.H{"errors": out})
            return
        }
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    user, err := userService.Create(c.Request.Context(), req)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }
    c.JSON(http.StatusCreated, user)
}go

6. 응답 처리

>// 표준화된 API 응답 구조체
type Response[T any] struct {
    Success bool   `json:"success"`
    Data    T      `json:"data,omitempty"`
    Error   string `json:"error,omitempty"`
    Meta    *Meta  `json:"meta,omitempty"`
}

type Meta struct {
    Page     int   `json:"page"`
    PageSize int   `json:"page_size"`
    Total    int64 `json:"total"`
}

// 헬퍼 함수
func OK[T any](c *gin.Context, data T) {
    c.JSON(http.StatusOK, Response[T]{Success: true, Data: data})
}
func Created[T any](c *gin.Context, data T) {
    c.JSON(http.StatusCreated, Response[T]{Success: true, Data: data})
}
func Fail(c *gin.Context, code int, msg string) {
    c.JSON(code, Response[any]{Success: false, Error: msg})
}

// 사용
func getUser(c *gin.Context) {
    user, err := userService.FindByID(c.Param("id"))
    if errors.Is(err, gorm.ErrRecordNotFound) {
        Fail(c, http.StatusNotFound, "사용자를 찾을 수 없습니다")
        return
    }
    OK(c, user)
}go

7. GORM + 데이터베이스

package model

import (
    "time"
    "gorm.io/gorm"
)

type User struct {
    gorm.Model            // ID, CreatedAt, UpdatedAt, DeletedAt
    Name     string       `gorm:"not null"`
    Email    string       `gorm:"uniqueIndex;not null"`
    Password string       `gorm:"not null"`
    Role     string       `gorm:"default:user"`
    Posts    []Post       `gorm:"foreignKey:UserID"`
}

// DB 연결
func InitDB(dsn string) (*gorm.DB, error) {
    db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
        Logger: logger.Default.LogMode(logger.Info),
    })
    if err != nil { return nil, err }

    // 자동 마이그레이션
    db.AutoMigrate(&User{}, &Post{})
    return db, nil
}

// Repository 패턴
type UserRepository struct { db *gorm.DB }

func (r *UserRepository) FindAll(page, size int) ([]User, int64, error) {
    var users []User
    var total int64
    err := r.db.Model(&User{}).
        Count(&total).
        Offset((page - 1) * size).
        Limit(size).
        Find(&users).Error
    return users, total, err
}

func (r *UserRepository) FindByEmail(email string) (*User, error) {
    var user User
    err := r.db.Where("email = ?", email).First(&user).Error
    return &user, err
}

func (r *UserRepository) Create(user *User) error {
    return r.db.Create(user).Error
}go

8. JWT 인증

>package middleware

import (
    "net/http"
    "strings"

    "github.com/gin-gonic/gin"
    "github.com/golang-jwt/jwt/v5"
)

type Claims struct {
    UserID uint   `json:"user_id"`
    Email  string `json:"email"`
    Role   string `json:"role"`
    jwt.RegisteredClaims
}

func GenerateToken(userID uint, email, role, secret string) (string, error) {
    claims := Claims{
        UserID: userID,
        Email:  email,
        Role:   role,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
            IssuedAt:  jwt.NewNumericDate(time.Now()),
        },
    }
    return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).
        SignedString([]byte(secret))
}

func JWTAuth(secret string) gin.HandlerFunc {
    return func(c *gin.Context) {
        auth := c.GetHeader("Authorization")
        if !strings.HasPrefix(auth, "Bearer ") {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "토큰이 없습니다"})
            return
        }
        tokenStr := strings.TrimPrefix(auth, "Bearer ")

        claims := &Claims{}
        token, err := jwt.ParseWithClaims(tokenStr, claims, func(t *jwt.Token) (any, error) {
            return []byte(secret), nil
        })
        if err != nil || !token.Valid {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "유효하지 않은 토큰"})
            return
        }

        c.Set("userID", claims.UserID)
        c.Set("email",  claims.Email)
        c.Next()
    }
}

// 로그인 핸들러
func (h *AuthHandler) Login(c *gin.Context) {
    var req LoginRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        Fail(c, http.StatusBadRequest, err.Error()); return
    }
    user, err := h.service.Authenticate(req.Email, req.Password)
    if err != nil {
        Fail(c, http.StatusUnauthorized, "이메일 또는 비밀번호가 잘못됐습니다"); return
    }
    token, _ := GenerateToken(user.ID, user.Email, user.Role, h.secret)
    OK(c, gin.H{"token": token, "user": user})
}go

9. 테스트

>package handler_test

import (
    "bytes"
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "testing"

    "github.com/gin-gonic/gin"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/mock"
)

func TestGetUser_Success(t *testing.T) {
    gin.SetMode(gin.TestMode)

    // Mock 서비스
    mockSvc := &MockUserService{}
    mockSvc.On("FindByID", "1").Return(&model.User{ID: 1, Name: "김철수"}, nil)

    r := gin.New()
    h := NewUserHandler(mockSvc)
    r.GET("/users/:id", h.GetUser)

    req, _ := http.NewRequest("GET", "/users/1", nil)
    w := httptest.NewRecorder()
    r.ServeHTTP(w, req)

    assert.Equal(t, http.StatusOK, w.Code)
    var resp Response[model.User]
    json.Unmarshal(w.Body.Bytes(), &resp)
    assert.Equal(t, "김철수", resp.Data.Name)
    mockSvc.AssertExpectations(t)
}go

10. Docker 배포

>FROM golang:1.22-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /app ./cmd/server

FROM scratch
COPY --from=builder /app /app
COPY --from=builder /etc/ssl/certs /etc/ssl/certs
EXPOSE 8080
ENTRYPOINT ["/app"]dockerfile
>services:
  api:
    build: .
    ports: ["8080:8080"]
    environment:
      DATABASE_URL: postgres://user:${DB_PASS}@db:5432/mydb?sslmode=disable
      JWT_SECRET: ${JWT_SECRET}
    depends_on:
      db: { condition: service_healthy }

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: ${DB_PASS}
      POSTGRES_DB: mydb
    volumes: [pgdata:/var/lib/postgresql/data]
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "user"]
      interval: 5s
      retries: 5

volumes:
  pgdata:yaml

11. 다음 단계

🚀
Gin 심화 로드맵

gRPC: Protocol Buffers 기반 고성능 RPC
Redis 캐싱: go-redis로 응답 캐싱
Uber Zap 로깅: 고성능 구조화 로그
OpenTelemetry: 분산 추적 (Jaeger 연동)
Wire: 컴파일 타임 의존성 주입
Fiber: Gin의 대안 (Express 스타일)

연계 가이드: Go 가이드 · Docker 가이드 · Kubernetes 가이드