Gin 기초 #5 미들웨어
지난 글에서 응답 처리를 다뤘습니다. 이번 글은 미들웨어입니다. 로깅, 복구, 인증처럼 여러 핸들러에 공통으로 필요한 처리를, 핸들러마다 반복하지 않고 한곳에 묶는 방법입니다.
- #1 시작과 첫 서버
- #2 라우팅과 핸들러
- #3 요청 바인딩과 검증
- #4 응답 처리 — JSON, 상태 코드, 에러
- #5 미들웨어 ← 이번 글
- #6 데이터베이스 연동 (GORM)
- #7 프로젝트 구조와 미니 REST API
미들웨어란? #
미들웨어는 핸들러가 실행되기 전후로 끼어드는 함수입니다. 요청이 들어오면 미들웨어를 순서대로 거쳐 핸들러에 도달하고, 응답이 나갈 때 그 역순으로 다시 미들웨어를 거칩니다.
요청 → 미들웨어A → 미들웨어B → 핸들러 → 미들웨어B → 미들웨어A → 응답이 구조 덕분에 “모든 요청에 로그를 남긴다”, “특정 그룹은 인증을 먼저 확인한다” 같은 공통 처리를 핸들러 본문과 분리할 수 있습니다.
이미 쓰고 있던 미들웨어 — Logger, Recovery #
#1에서 gin.Default()로 라우터를 만들었습니다. 이때 두 미들웨어가 자동으로 붙습니다.
- Logger — 요청마다 메서드, 경로, 상태 코드, 처리 시간을 콘솔에 출력
- Recovery — 핸들러에서 panic이 나도 서버를 죽이지 않고 500으로 응답
gin.New()로 만들면 이 둘이 없는 빈 엔진이 됩니다. 필요한 미들웨어를 직접 붙이고 싶을 때 이렇게 시작합니다.
r := gin.New()
r.Use(gin.Logger())
r.Use(gin.Recovery())r.Use가 미들웨어를 엔진 전체에 등록하는 함수입니다.
커스텀 미들웨어 만들기 #
미들웨어는 핸들러와 같은 gin.HandlerFunc 타입입니다. 즉 func(c *gin.Context) 형태면 됩니다. 요청 처리 시간을 측정하는 미들웨어를 만들어 보겠습니다.
func Timer() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next() // 다음 미들웨어와 핸들러 실행
elapsed := time.Since(start)
log.Printf("%s %s — %v", c.Request.Method, c.Request.URL.Path, elapsed)
}
}핵심은 c.Next()입니다. 이 호출을 기준으로 위쪽은 핸들러 실행 전, 아래쪽은 후에 동작합니다. c.Next()로 다음 단계를 실행한 뒤 다시 돌아와 경과 시간을 찍습니다.
등록은 다른 미들웨어와 같습니다.
r := gin.Default()
r.Use(Timer())미들웨어를 함수로 한 번 감싸는 이유는, 등록 시점에 설정값을 받을 수 있게 하기 위해서입니다. 예를 들어 Timer(threshold)처럼 인자를 받아 클로저로 가둬 둘 수 있습니다.
c.Next() 와 c.Abort() #
미들웨어에서 흐름을 제어하는 두 메서드입니다.
c.Next()— 다음 미들웨어와 핸들러로 진행c.Abort()— 이후 단계를 실행하지 않고 중단
Abort는 응답을 보내지 않습니다. 그래서 보통 응답과 함께 쓰는 c.AbortWithStatusJSON을 활용합니다.
func RequireAPIKey() gin.HandlerFunc {
return func(c *gin.Context) {
if c.GetHeader("X-API-Key") != "secret" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "유효하지 않은 키"})
return
}
c.Next()
}
}키가 틀리면 401을 응답하고 return으로 미들웨어 함수를 끝냅니다. 이때 Abort가 걸려 있어서 뒤따르는 핸들러는 실행되지 않습니다. 키가 맞으면 c.Next()로 통과시킵니다.
적용 범위 — 전역, 그룹, 라우트 #
미들웨어는 거는 위치에 따라 적용 범위가 달라집니다.
r := gin.Default()
// 1) 전역 — 모든 라우트
r.Use(Timer())
// 2) 그룹 — 그룹 안의 라우트만
admin := r.Group("/admin", RequireAPIKey())
{
admin.GET("/stats", showStats)
}
// 3) 라우트 — 그 라우트 하나만
r.GET("/profile", RequireAPIKey(), showProfile)#2에서 본 라우터 그룹과 결합하면, 인증이 필요한 엔드포인트만 한 그룹에 모아 미들웨어를 한 번에 거는 구조가 자연스럽게 나옵니다. 공개 API와 보호된 API를 그룹으로 나누는 방식입니다.
미들웨어에서 핸들러로 값 넘기기 #
미들웨어가 계산한 값을, 뒤따르는 핸들러에서 쓰고 싶을 때가 있습니다. 인증 미들웨어가 알아낸 사용자 정보가 대표적입니다. c.Set으로 저장하고 c.Get으로 꺼냅니다.
func Auth() gin.HandlerFunc {
return func(c *gin.Context) {
userID := 42 // 실제로는 토큰에서 추출
c.Set("userID", userID)
c.Next()
}
}
func profile(c *gin.Context) {
userID, exists := c.Get("userID")
if !exists {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "인증 필요"})
return
}
c.JSON(http.StatusOK, gin.H{"userID": userID})
}c.Get은 값과 존재 여부를 함께 돌려줍니다. 저장된 값의 타입은 any이므로, 꺼낼 때 타입 단언이 필요할 수 있습니다. 타입을 지정해 꺼내는 c.GetInt, c.GetString 같은 헬퍼도 있습니다.
userID := c.GetInt("userID")본격적인 JWT 기반 인증은 중급 시리즈에서 이 패턴 위에 쌓아 올리겠습니다.
자주 쓰는 외부 미들웨어 #
직접 만들지 않아도 되는 흔한 처리는 공식 또는 커뮤니티 미들웨어가 있습니다.
- CORS —
github.com/gin-contrib/cors - gzip 압축 —
github.com/gin-contrib/gzip - 세션 —
github.com/gin-contrib/sessions
설치 후 r.Use로 등록하는 방식은 동일합니다. CORS와 rate limit 같은 운영용 미들웨어는 중급 시리즈에서 다시 다루겠습니다.
마무리 #
이번 글에서 정리한 내용입니다.
- 미들웨어는 핸들러 전후로 끼어드는
func(c *gin.Context)함수 gin.Default()는 Logger와 Recovery를 자동으로 포함- 등록은
r.Use, 커스텀 미들웨어는 보통 클로저로 감싸 설정값을 받음 c.Next()기준으로 핸들러 실행 전후가 나뉨c.Abort계열로 이후 단계를 막아 인증 게이트를 구현- 전역, 그룹, 라우트 단위로 적용 범위를 정할 수 있음
- 미들웨어와 핸들러 사이의 값 전달은
c.Set/c.Get
다음 글(#6 데이터베이스 연동)에서는 GORM을 붙여 실제 데이터를 다루는 CRUD API를 만들겠습니다.