미들웨어 심화
미들웨어(Middleware)는 HTTP 요청이 핸들러에 도달하기 전후에 실행되는 함수입니다. 인증, 로깅, CORS, Rate Limiting 등 횡단 관심사(cross-cutting concerns)를 핸들러 로직과 분리해 코드 중복을 줄이고 유지보수성을 높입니다.
미들웨어 패턴
net/http 스타일 (함수 래핑)
package main
import (
"fmt"
"log"
"net/http"
"time"
)
// Middleware 타입 정의
type Middleware func(http.Handler) http.Handler
// Chain 여러 미들웨어를 순서대로 적용
func Chain(h http.Handler, middlewares ...Middleware) http.Handler {
// 역순으로 적용해야 올바른 실행 순서가 됨
for i := len(middlewares) - 1; i >= 0; i-- {
h = middlewares[i](h)
}
return h
}
// LoggingMiddleware 요청/응답 로깅
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// 응답 상태코드를 캡처하기 위한 래퍼
rw := &responseWriter{ResponseWriter: w, status: http.StatusOK}
next.ServeHTTP(rw, r)
log.Printf("%s %s %d %v", r.Method, r.URL.Path, rw.status, time.Since(start))
})
}
// 응답 상태코드 캡처용 래퍼
type responseWriter struct {
http.ResponseWriter
status int
}
func (rw *responseWriter) WriteHeader(status int) {
rw.status = status
rw.ResponseWriter.WriteHeader(status)
}
// RecoveryMiddleware panic 복구
func RecoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("panic: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /hello", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, World!")
})
// 미들웨어 체인: Recovery → Logging → Handler 순으로 실행
handler := Chain(mux, RecoveryMiddleware, LoggingMiddleware)
http.ListenAndServe(":8080", handler)
}
JWT 인증 미들웨어
package main
import (
"context"
"net/http"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
)
var jwtSecret = []byte("your-secret-key-change-in-production")
// Claims JWT 클레임 구조체
type Claims struct {
UserID int64 `json:"user_id"`
Email string `json:"email"`
Role string `json:"role"`
jwt.RegisteredClaims
}
// contextKey 컨텍스트 키 타입 (충돌 방지)
type contextKey string
const claimsKey contextKey = "claims"
// GenerateToken JWT 토큰 발급
func GenerateToken(userID int64, email, role 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()),
Issuer: "my-app",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(jwtSecret)
}
// JWTMiddleware JWT 인증 미들웨어 (net/http 스타일)
func JWTMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Authorization: Bearer <token> 헤더 파싱
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, `{"error":{"code":"UNAUTHORIZED","message":"인증 토큰이 필요합니다"}}`,
http.StatusUnauthorized)
return
}
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
http.Error(w, `{"error":{"code":"INVALID_TOKEN","message":"Bearer 토큰 형식이 아닙니다"}}`,
http.StatusUnauthorized)
return
}
// 토큰 파싱 및 검증
claims := &Claims{}
token, err := jwt.ParseWithClaims(parts[1], claims, func(t *jwt.Token) (any, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, jwt.ErrSignatureInvalid
}
return jwtSecret, nil
})
if err != nil || !token.Valid {
http.Error(w, `{"error":{"code":"INVALID_TOKEN","message":"유효하지 않은 토큰입니다"}}`,
http.StatusUnauthorized)
return
}
// 클레임을 컨텍스트에 저장
ctx := context.WithValue(r.Context(), claimsKey, claims)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// GetClaims 컨텍스트에서 클레임 추출
func GetClaims(r *http.Request) *Claims {
claims, _ := r.Context().Value(claimsKey).(*Claims)
return claims
}
// AdminOnly 관리자 전용 미들웨어 (JWTMiddleware 이후 사용)
func AdminOnly(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
claims := GetClaims(r)
if claims == nil || claims.Role != "admin" {
http.Error(w, `{"error":{"code":"FORBIDDEN","message":"관리자 권한이 필요합니다"}}`,
http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
CORS 미들웨어
package main
import "net/http"
// CORSConfig CORS 설정
type CORSConfig struct {
AllowOrigins []string
AllowMethods []string
AllowHeaders []string
ExposeHeaders []string
MaxAge int // Preflight 캐시 시간(초)
}
// CORSMiddleware CORS 미들웨어
func CORSMiddleware(config CORSConfig) func(http.Handler) http.Handler {
allowOriginSet := make(map[string]bool)
for _, o := range config.AllowOrigins {
allowOriginSet[o] = true
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
// Origin 허용 여부 확인
if allowOriginSet["*"] || allowOriginSet[origin] {
w.Header().Set("Access-Control-Allow-Origin", origin)
}
w.Header().Set("Access-Control-Allow-Methods",
joinStrings(config.AllowMethods, ", "))
w.Header().Set("Access-Control-Allow-Headers",
joinStrings(config.AllowHeaders, ", "))
if len(config.ExposeHeaders) > 0 {
w.Header().Set("Access-Control-Expose-Headers",
joinStrings(config.ExposeHeaders, ", "))
}
// Preflight 요청 처리
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
}
func joinStrings(ss []string, sep string) string {
result := ""
for i, s := range ss {
if i > 0 {
result += sep
}
result += s
}
return result
}
// 사용 예시
func newCORSMiddleware() func(http.Handler) http.Handler {
return CORSMiddleware(CORSConfig{
AllowOrigins: []string{"https://example.com", "http://localhost:3000"},
AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Content-Type", "Authorization", "X-Request-ID"},
ExposeHeaders: []string{"X-Request-ID"},
MaxAge: 86400,
})
}
요청 로깅 미들웨어 (slog + 요청 ID)
package main
import (
"context"
"log/slog"
"net/http"
"time"
"github.com/google/uuid"
)
type requestIDKey struct{}
// RequestIDMiddleware 요청 ID 생성 및 전파
func RequestIDMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 클라이언트가 전달하거나 새로 생성
requestID := r.Header.Get("X-Request-ID")
if requestID == "" {
requestID = uuid.New().String()
}
w.Header().Set("X-Request-ID", requestID)
ctx := context.WithValue(r.Context(), requestIDKey{}, requestID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// StructuredLoggingMiddleware slog 기반 구조화 로깅
func StructuredLoggingMiddleware(logger *slog.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rw := &responseWriter{ResponseWriter: w, status: http.StatusOK}
next.ServeHTTP(rw, r)
requestID, _ := r.Context().Value(requestIDKey{}).(string)
logger.Info("request",
slog.String("request_id", requestID),
slog.String("method", r.Method),
slog.String("path", r.URL.Path),
slog.String("remote_addr", r.RemoteAddr),
slog.Int("status", rw.status),
slog.Duration("duration", time.Since(start)),
)
})
}
}
Rate Limiting 미들웨어
package main
import (
"net/http"
"sync"
"time"
"golang.org/x/time/rate"
)
// IPRateLimiter IP별 토큰 버킷 Rate Limiter
type IPRateLimiter struct {
limiters map[string]*rate.Limiter
mu sync.RWMutex
r rate.Limit // 초당 허용 요청 수
b int // 버스트 크기
}
func NewIPRateLimiter(r rate.Limit, b int) *IPRateLimiter {
return &IPRateLimiter{
limiters: make(map[string]*rate.Limiter),
r: r,
b: b,
}
}
func (rl *IPRateLimiter) getLimiter(ip string) *rate.Limiter {
rl.mu.RLock()
limiter, ok := rl.limiters[ip]
rl.mu.RUnlock()
if !ok {
rl.mu.Lock()
limiter = rate.NewLimiter(rl.r, rl.b)
rl.limiters[ip] = limiter
rl.mu.Unlock()
}
return limiter
}
// RateLimitMiddleware IP 기반 Rate Limiting 미들웨어
func RateLimitMiddleware(rl *IPRateLimiter) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ip := r.RemoteAddr // 실제로는 X-Real-IP 또는 X-Forwarded-For 사용
if !rl.getLimiter(ip).Allow() {
w.Header().Set("Retry-After", "1")
http.Error(w,
`{"error":{"code":"RATE_LIMITED","message":"요청이 너무 많습니다. 잠시 후 다시 시도하세요"}}`,
http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
}
타임아웃 미들웨어
package main
import (
"context"
"net/http"
"time"
)
// TimeoutMiddleware 요청 처리 타임아웃
func TimeoutMiddleware(timeout time.Duration) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), timeout)
defer cancel()
// 타임아웃 감지를 위한 채널
done := make(chan struct{})
tw := &timeoutResponseWriter{ResponseWriter: w}
go func() {
next.ServeHTTP(tw, r.WithContext(ctx))
close(done)
}()
select {
case <-done:
// 정상 완료
case <-ctx.Done():
tw.mu.Lock()
if !tw.wroteHeader {
http.Error(w, `{"error":{"code":"TIMEOUT","message":"요청 처리 시간이 초과되었습니다"}}`,
http.StatusGatewayTimeout)
}
tw.mu.Unlock()
}
})
}
}
type timeoutResponseWriter struct {
http.ResponseWriter
mu sync.Mutex
wroteHeader bool
}
func (tw *timeoutResponseWriter) WriteHeader(status int) {
tw.mu.Lock()
defer tw.mu.Unlock()
tw.wroteHeader = true
tw.ResponseWriter.WriteHeader(status)
}
실전 예제: 프로덕션 수준 Gin 서버
package main
import (
"log/slog"
"net/http"
"os"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"golang.org/x/time/rate"
)
func setupRouter() *gin.Engine {
r := gin.New() // gin.Default() 대신 gin.New()로 미들웨어 직접 제어
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
// 1. Recovery (가장 바깥쪽에 위치)
r.Use(gin.CustomRecoveryWithWriter(os.Stderr, func(c *gin.Context, err any) {
logger.Error("panic recovered", slog.Any("error", err))
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"error": gin.H{"code": "INTERNAL_ERROR", "message": "서버 내부 오류가 발생했습니다"},
})
}))
// 2. 요청 ID 주입
r.Use(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()
})
// 3. 구조화 로깅
r.Use(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)),
)
})
// 4. CORS
r.Use(func(c *gin.Context) {
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS")
c.Header("Access-Control-Allow-Headers", "Content-Type,Authorization,X-Request-ID")
if c.Request.Method == http.MethodOptions {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
})
// 5. Rate Limiting (초당 10 요청, 버스트 20)
limiter := NewIPRateLimiter(rate.Limit(10), 20)
r.Use(func(c *gin.Context) {
if !limiter.getLimiter(c.ClientIP()).Allow() {
c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
"error": gin.H{"code": "RATE_LIMITED", "message": "잠시 후 다시 시도하세요"},
})
return
}
c.Next()
})
// 공개 라우트
r.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "ok", "time": time.Now()})
})
// 인증이 필요한 라우트 그룹
api := r.Group("/api/v1")
api.Use(func(c *gin.Context) {
// JWT 검증 (간략화)
token := c.GetHeader("Authorization")
if token == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": gin.H{"code": "UNAUTHORIZED", "message": "인증이 필요합니다"},
})
return
}
c.Next()
})
{
api.GET("/users/me", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"user": gin.H{"id": 1, "name": "홍길동"}})
})
}
return r
}
func main() {
r := setupRouter()
r.Run(":8080")
}
미들웨어 실행 순서
요청 →
[Recovery 시작] →
[RequestID 시작] →
[Logging 시작] →
[CORS 시작] →
[RateLimit 시작] →
[JWT 시작] →
Handler 실행
[JWT 종료] →
[RateLimit 종료] →
[CORS 종료] →
[Logging 종료] (상태코드, 응답시간 기록) →
[RequestID 종료] →
[Recovery 종료] →
← 응답
고수 팁
1. 미들웨어는 얇게: 각 미들웨어는 단일 책임만 가져야 합니다
2. c.Abort() 사용: Gin에서 다음 미들웨어 실행을 막으려면 c.Abort()를 사용하세요
3. 컨텍스트로 데이터 전달: 미들웨어 간 데이터는 c.Set/c.Get 또는 context.WithValue로 전달합니다
4. 미들웨어 순서가 중요: Recovery는 항상 가장 바깥쪽에, Logging은 그 다음에 위치해야 모든 에러와 응답 시간을 정확하게 기록할 수 있습니다