Gin 기초 #5 미들웨어

4 분 소요

지난 글에서 응답 처리를 다뤘습니다. 이번 글은 미들웨어입니다. 로깅, 복구, 인증처럼 여러 핸들러에 공통으로 필요한 처리를, 핸들러마다 반복하지 않고 한곳에 묶는 방법입니다.

  • #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 기반 인증은 중급 시리즈에서 이 패턴 위에 쌓아 올리겠습니다.

자주 쓰는 외부 미들웨어 #

직접 만들지 않아도 되는 흔한 처리는 공식 또는 커뮤니티 미들웨어가 있습니다.

  • CORSgithub.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를 만들겠습니다.

X