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 가이드
• gRPC: Protocol Buffers 기반 고성능 RPC
• Redis 캐싱: go-redis로 응답 캐싱
• Uber Zap 로깅: 고성능 구조화 로그
• OpenTelemetry: 분산 추적 (Jaeger 연동)
• Wire: 컴파일 타임 의존성 주입
• Fiber: Gin의 대안 (Express 스타일)
연계 가이드: Go 가이드 · Docker 가이드 · Kubernetes 가이드