미들웨어와 요청 처리
Go 미들웨어의 시그니처는 func(http.Handler) http.Handler — Handler를 받아서 Handler를 반환하는 함수다. 이 단순한 형태로 로깅, 인증, CORS, panic recovery까지 구현할 수 있다.
기본 구조
Express 미들웨어가 next callback으로 제어를 넘기는 것과 달리, Go 미들웨어는 Handler를 감싸는 함수 합성이다:
func logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("%s %s", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
// next.ServeHTTP 이후 코드도 실행된다
log.Println("응답 완료")
})
}
next.ServeHTTP(w, r)은 일반 함수 호출이다. 호출하면 다음 핸들러가 실행되고, 반환되면 실행이 끝난 것이다. Express의 next()가 callback chain인 것과 달리, Go 미들웨어는 call stack이다.
실행 순서
미들웨어를 체이닝하면 실행 순서가 중요해진다:
func first(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println("first: 요청 진입")
next.ServeHTTP(w, r)
log.Println("first: 응답 완료")
})
}
func second(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println("second: 요청 진입")
next.ServeHTTP(w, r)
log.Println("second: 응답 완료")
})
}
이 두 미들웨어를 적용한다:
handler := first(second(mux))
출력은 다음과 같다:
first: 요청 진입
second: 요청 진입
(핸들러 실행)
second: 응답 완료
first: 응답 완료
함수 호출이 중첩되므로 요청은 바깥에서 안으로, 응답은 안에서 바깥으로 흐른다. first가 second를 감싸고, second가 실제 핸들러를 감싼다.
chain 함수를 만들면 읽기 순서와 실행 순서가 일치한다:
func chain(h http.Handler, middlewares ...func(http.Handler) http.Handler) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
h = middlewares[i](h)
}
return h
}
handler := chain(mux, first, second)
// first -> second -> mux 순서로 실행
응답을 가로채는 ResponseWriter wrapper
미들웨어에서 응답 상태 코드나 본문 크기를 알고 싶을 때가 있다. http.ResponseWriter는 한 번 쓰면 읽을 수 없다. wrapper를 만들어 해결한다:
type statusRecorder struct {
http.ResponseWriter
statusCode int
}
func (r *statusRecorder) WriteHeader(code int) {
r.statusCode = code
r.ResponseWriter.WriteHeader(code)
}
http.ResponseWriter를 embed하면 Header()와 Write() 메서드는 그대로 위임된다. WriteHeader만 오버라이드하여 상태 코드를 기록한다.
로깅 미들웨어
실용적인 로깅 미들웨어는 응답 상태 코드와 처리 시간도 기록해야 한다:
func logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rec := &statusRecorder{ResponseWriter: w, statusCode: http.StatusOK}
next.ServeHTTP(rec, r)
log.Printf("%s %s %d %s",
r.Method, r.URL.Path, rec.statusCode, time.Since(start))
})
}
next.ServeHTTP 호출이 동기적이므로, 그 이후에 바로 처리 시간을 계산하면 된다. Express에서는 res.on('finish', ...)로 응답 완료 이벤트를 잡아야 하는 것과 대조적이다.
인증 미들웨어
인증 미들웨어는 요청을 검증하고, 실패하면 다음 핸들러를 호출하지 않는다:
func auth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return // next.ServeHTTP를 호출하지 않는다
}
userID, err := validateToken(token)
if err != nil {
http.Error(w, "invalid token", http.StatusUnauthorized)
return
}
// 인증된 사용자 정보를 context에 저장
ctx := context.WithValue(r.Context(), userIDKey{}, userID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
next.ServeHTTP를 호출하지 않고 return하면 요청이 거기서 멈춘다. Express의 next() 생략과 달리, return으로 의도가 명시적이다.
context로 값 전달
Express의 req.user = decoded처럼 요청에 값을 붙이려면 context.WithValue를 사용한다:
// key 타입 정의 — 패키지 간 충돌 방지
type userIDKey struct{}
// 값 저장
ctx := context.WithValue(r.Context(), userIDKey{}, "user-123")
r = r.WithContext(ctx)
// 값 꺼내기
userID, ok := r.Context().Value(userIDKey{}).(string)
key에 빈 struct를 쓰는 이유는 타입 자체가 고유한 식별자가 되기 때문이다. 문자열 key를 쓰면 서로 다른 패키지에서 같은 문자열을 사용했을 때 충돌한다. Express의 req.user에서 다른 미들웨어가 같은 속성명을 덮어쓰는 문제를 타입 시스템으로 방지한다.
CORS 미들웨어
CORS 미들웨어는 응답 헤더를 설정하고, preflight 요청(OPTIONS)을 처리한다:
func cors(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
preflight 요청은 헤더만 설정하고 204 No Content로 응답한 뒤, 핸들러를 호출하지 않고 끝낸다.
Recovery 미들웨어
Go의 HTTP 서버는 핸들러에서 panic이 발생하면 해당 goroutine이 종료된다. 서버 자체는 죽지 않지만, 클라이언트는 빈 응답을 받는다. recovery 미들웨어로 panic을 잡아서 500 응답을 반환한다:
func recovery(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 recovered: %v\n%s", err, debug.Stack())
http.Error(w, "internal server error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
defer와 recover의 조합이다. 10편에서 다뤘듯 recover는 defer 내부에서만 동작한다. next.ServeHTTP 실행 중 panic이 발생하면 defer 함수가 실행되고, recover가 panic 값을 잡는다. debug.Stack()으로 스택 트레이스도 기록한다.
Express에서는 인자가 4개인 미들웨어((err, req, res, next))로 에러를 처리한다. Go는 defer/recover라는 언어 수준의 메커니즘을 사용한다.
미들웨어 조합
지금까지 만든 미들웨어를 조합하여 서버를 구성한다:
func main() {
mux := http.NewServeMux()
// 공개 엔드포인트
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
})
// 인증이 필요한 엔드포인트
mux.Handle("GET /me", auth(http.HandlerFunc(handleMe)))
mux.Handle("POST /posts", auth(http.HandlerFunc(handleCreatePost)))
// 전역 미들웨어 적용
handler := chain(mux, recovery, logging, cors)
http.ListenAndServe(":8080", handler)
}
두 가지 수준에서 미들웨어를 적용했다:
- 전역 미들웨어 —
chain으로 모든 요청에 적용. recovery, logging, CORS. - 라우트별 미들웨어 —
auth(handler)형태로 특정 핸들러에만 적용.
Express의 app.use()(전역)와 app.get('/me', authMiddleware, handler)(라우트별) 구분과 같다. Go에서 라우트별 미들웨어가 mux.Handle을 사용하는 것에 주의한다. mux.HandleFunc은 함수를 받지만, 미들웨어가 반환하는 것은 http.Handler이므로 mux.Handle을 써야 한다.
요청 lifecycle 정리
요청이 서버에 도착해서 응답이 나가기까지의 전체 흐름:
클라이언트 요청
-> recovery (defer 설정)
-> logging (시작 시간 기록)
-> cors (헤더 설정, OPTIONS이면 여기서 반환)
-> ServeMux (경로 매칭)
-> auth (토큰 검증, 실패하면 여기서 반환)
-> 핸들러 (비즈니스 로직, 응답 작성)
<- auth
<- ServeMux
<- cors
<- logging (처리 시간 계산, 로그 출력)
<- recovery (panic이 있었으면 여기서 처리)
클라이언트 응답
이 흐름은 함수 call stack 그 자체다. 미들웨어마다 next.ServeHTTP 호출 전에 요청 전처리를, 호출 후에 응답 후처리를 수행한다.
서드파티 미들웨어와의 호환
func(http.Handler) http.Handler 시그니처는 Go 생태계의 사실상 표준이다. 서드파티 라이브러리도 이 시그니처를 따른다:
import "github.com/rs/cors"
// rs/cors 라이브러리의 Handler 메서드는 func(http.Handler) http.Handler와 같은 역할
c := cors.New(cors.Options{
AllowedOrigins: []string{"https://example.com"},
AllowedMethods: []string{"GET", "POST"},
})
handler := c.Handler(mux)
chi, gorilla, alice 등 대부분의 라우터와 미들웨어 라이브러리가 http.Handler를 기반으로 동작한다. 표준 라이브러리의 interface에서 비롯된 관례이므로 서드파티 간 호환성이 높다.
| ← HTTP 서버 | Context → |