Go 백엔드의 미들웨어 패턴, JWT 인증 구현, CORS 설정, 레이트 리미팅, 요청 유효성 검사, OpenAPI/Swagger 문서 생성, log/slog를 활용한 구조화된 로깅을 다룹니다.
Go에서 미들웨어는 http.Handler를 받아 http.Handler를 반환하는 함수입니다. 이 패턴을 통해 요청 처리 전후에 공통 로직을 삽입할 수 있습니다.
// 미들웨어 타입 정의
type Middleware func(http.Handler) http.Handler
// 미들웨어 체이닝
func Chain(handler http.Handler, middlewares ...Middleware) http.Handler {
// 역순으로 적용하여 올바른 실행 순서 보장
for i := len(middlewares) - 1; i >= 0; i-- {
handler = middlewares[i](handler)
}
return handler
}
// 사용 예시
handler := Chain(
finalHandler,
LoggingMiddleware,
AuthMiddleware,
CORSMiddleware,
RateLimitMiddleware,
)모든 요청에 고유 식별자를 부여하여 분산 시스템에서의 추적을 가능하게 합니다.
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()
}
ctx := context.WithValue(r.Context(), requestIDKey, requestID)
w.Header().Set("X-Request-ID", requestID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}응답 상태 코드와 바이트 수를 캡처하려면 http.ResponseWriter를 래핑해야 합니다.
type responseWriter struct {
http.ResponseWriter
statusCode int
bytes int
}
func newResponseWriter(w http.ResponseWriter) *responseWriter {
return &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
func (rw *responseWriter) Write(b []byte) (int, error) {
n, err := rw.ResponseWriter.Write(b)
rw.bytes += n
return n, err
}JWT(JSON Web Token)는 세 부분으로 구성됩니다: 헤더(Header), 페이로드(Payload), 서명(Signature).
eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxMjM0fQ.signature
|--- Header ---|.|----- Payload -----|.|- Signature -|import "github.com/golang-jwt/jwt/v5"
type Claims struct {
UserID int64 `json:"user_id"`
Email string `json:"email"`
Role string `json:"role"`
jwt.RegisteredClaims
}
type JWTService struct {
secretKey []byte
accessExpiry time.Duration
refreshExpiry time.Duration
}
func NewJWTService(secret string) *JWTService {
return &JWTService{
secretKey: []byte(secret),
accessExpiry: 15 * time.Minute,
refreshExpiry: 7 * 24 * time.Hour,
}
}
// 액세스 토큰 생성
func (s *JWTService) GenerateAccessToken(userID int64, email, role string) (string, error) {
claims := Claims{
UserID: userID,
Email: email,
Role: role,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(s.accessExpiry)),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "myapp",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(s.secretKey)
}
// 토큰 검증
func (s *JWTService) ValidateToken(tokenStr string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenStr, &Claims{},
func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("예상하지 못한 서명 방식: %v", token.Header["alg"])
}
return s.secretKey, nil
},
)
if err != nil {
return nil, fmt.Errorf("토큰 파싱 실패: %w", err)
}
claims, ok := token.Claims.(*Claims)
if !ok || !token.Valid {
return nil, errors.New("유효하지 않은 토큰")
}
return claims, nil
}func AuthMiddleware(jwtService *JWTService) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
writeError(w, http.StatusUnauthorized, "인증 토큰이 필요합니다")
return
}
// "Bearer <token>" 형식 파싱
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
writeError(w, http.StatusUnauthorized, "잘못된 인증 형식입니다")
return
}
claims, err := jwtService.ValidateToken(parts[1])
if err != nil {
writeError(w, http.StatusUnauthorized, "유효하지 않은 토큰입니다")
return
}
// 컨텍스트에 사용자 정보 저장
ctx := context.WithValue(r.Context(), userClaimsKey, claims)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// 핸들러에서 사용자 정보 추출
func GetUserClaims(ctx context.Context) (*Claims, bool) {
claims, ok := ctx.Value(userClaimsKey).(*Claims)
return claims, ok
}JWT 시크릿 키는 환경 변수로 관리하고, 코드에 하드코딩하지 마세요. 프로덕션에서는 RS256(비대칭 키) 알고리즘과 키 로테이션을 고려해야 합니다.
CORS(Cross-Origin Resource Sharing)는 브라우저의 동일 출처 정책(Same-Origin Policy)을 우회하여 다른 도메인에서 API를 호출할 수 있게 합니다.
func CORSMiddleware(allowedOrigins []string) Middleware {
originSet := make(map[string]bool)
for _, o := range allowedOrigins {
originSet[o] = true
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
if originSet[origin] {
w.Header().Set("Access-Control-Allow-Origin", origin)
}
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
w.Header().Set("Access-Control-Max-Age", "86400")
// Preflight 요청 처리
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
}import "github.com/go-playground/validator/v10"
var validate = validator.New()
type CreateUserRequest struct {
Name string `json:"name" validate:"required,min=2,max=50"`
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required,min=8,max=72"`
Age int `json:"age" validate:"omitempty,gte=0,lte=150"`
}
func decodeAndValidate[T any](r *http.Request) (*T, error) {
var req T
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, fmt.Errorf("요청 본문 파싱 실패: %w", err)
}
if err := validate.Struct(&req); err != nil {
var validationErrors validator.ValidationErrors
if errors.As(err, &validationErrors) {
messages := make([]string, 0, len(validationErrors))
for _, e := range validationErrors {
messages = append(messages,
fmt.Sprintf("필드 '%s': %s 조건 미충족", e.Field(), e.Tag()))
}
return nil, &ValidationError{Fields: messages}
}
return nil, err
}
return &req, nil
}Go 1.21에서 도입된 log/slog는 구조화된 로깅을 표준 라이브러리 수준에서 지원합니다.
import "log/slog"
func setupLogger(env string) *slog.Logger {
var handler slog.Handler
switch env {
case "production":
handler = slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
})
default:
handler = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
})
}
logger := slog.New(handler)
slog.SetDefault(logger)
return logger
}func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rw := newResponseWriter(w)
next.ServeHTTP(rw, r)
duration := time.Since(start)
requestID, _ := r.Context().Value(requestIDKey).(string)
slog.Info("HTTP 요청 처리",
"method", r.Method,
"path", r.URL.Path,
"status", rw.statusCode,
"duration_ms", duration.Milliseconds(),
"bytes", rw.bytes,
"request_id", requestID,
"remote_addr", r.RemoteAddr,
"user_agent", r.UserAgent(),
)
})
}{
"time": "2026-08-25T14:30:00.000Z",
"level": "INFO",
"msg": "HTTP 요청 처리",
"method": "GET",
"path": "/api/v1/users/42",
"status": 200,
"duration_ms": 12,
"bytes": 256,
"request_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}프로덕션 환경에서는 JSON 형식의 구조화된 로그를 사용하세요. 이 형식은 Elasticsearch, Loki, CloudWatch 같은 로그 수집 시스템에서 파싱과 검색이 용이합니다.
// @Summary 사용자 조회
// @Description ID로 사용자 정보를 조회합니다
// @Tags users
// @Accept json
// @Produce json
// @Param id path int true "사용자 ID"
// @Success 200 {object} User
// @Failure 404 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Security BearerAuth
// @Router /api/v1/users/{id} [get]
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
// 구현
}# swag 도구 설치
go install github.com/swaggo/swag/cmd/swag@latest
# 문서 생성
swag init -g cmd/server/main.go -o docs/swagger실전 프로젝트에서의 미들웨어 스택 조립 예시입니다.
func setupRouter(cfg *Config, jwtSvc *JWTService) http.Handler {
r := chi.NewRouter()
// 글로벌 미들웨어 -- 모든 요청에 적용
r.Use(RequestIDMiddleware)
r.Use(LoggingMiddleware)
r.Use(RecoveryMiddleware)
r.Use(CORSMiddleware(cfg.AllowedOrigins))
// 공개 라우트
r.Group(func(r chi.Router) {
r.Post("/api/v1/auth/login", loginHandler)
r.Post("/api/v1/auth/register", registerHandler)
r.Get("/api/v1/health", healthHandler)
})
// 인증 필요 라우트
r.Group(func(r chi.Router) {
r.Use(AuthMiddleware(jwtSvc))
r.Use(RateLimitMiddleware(100, time.Minute)) // 분당 100회
r.Route("/api/v1/users", func(r chi.Router) {
r.Get("/", listUsers)
r.Get("/{id}", getUser)
r.Put("/{id}", updateUser)
})
// 관리자 전용
r.Group(func(r chi.Router) {
r.Use(RequireRole("admin"))
r.Delete("/api/v1/users/{id}", deleteUser)
})
})
return r
}이번 장에서 살펴본 핵심 내용을 정리합니다.
http.Handler를 래핑하는 패턴으로, 체이닝을 통해 요청 처리 파이프라인을 구성합니다9장에서는 테스트 전략을 다룹니다. testing 패키지의 테이블 기반 테스트, testify, httptest를 활용한 HTTP 핸들러 테스트, 목(Mock) 생성, testcontainers-go를 활용한 통합 테스트, 벤치마크, 퍼징 테스트를 살펴봅니다.
이 글이 도움이 되셨나요?
Go에서 데이터베이스를 다루는 방법을 database/sql, sqlx, GORM, pgx를 통해 비교하고, 커넥션 풀링, 트랜잭션, 마이그레이션, 쿼리 빌더 패턴을 실전 예제로 다룹니다.
Go의 testing 패키지를 중심으로 테이블 기반 테스트, testify, httptest, 목(Mock) 생성, testcontainers-go 통합 테스트, 벤치마크, 퍼징 테스트 전략을 다룹니다.
Protocol Buffers 정의부터 Go에서의 gRPC 서버/클라이언트 구현, 유니어리/스트리밍 통신, 인터셉터, gRPC-Gateway를 통한 REST 변환, 헬스 체킹과 서비스 디스커버리를 다룹니다.