고 실전 #5 미들웨어 패턴
#4 DB 연동에서 안쪽 데이터를 다뤘다면 — 이번엔 다시 바깥. 요청과 응답 사이의 공통 처리.
Go의 미들웨어는 — 다른 언어의 미들웨어와 본질이 같지만, 언어가 따로 지원하는 개념이 아니라 http.Handler 인터페이스만으로 표현되는 게 특징입니다. 단순한 만큼 — 강력합니다.
미들웨어의 모양 — Handler → Handler
#
type Middleware func(http.Handler) http.Handler함수 하나가 — 핸들러를 받아 새 핸들러를 돌려줍니다. 그 안에서 원래 핸들러를 부르기 전후에 처리를 넣습니다.
가장 작은 예 — 로깅 #
func logger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start))
})
}핵심:
http.HandlerFunc어댑터로 함수를 Handler로 변환- **
next.ServeHTTP(w, r)**가 실제 핸들러 호출 — 그 앞뒤에 로직
적용 #
mux := http.NewServeMux()
mux.HandleFunc("GET /hello", helloHandler)
handler := logger(mux) // mux 전체를 로깅으로 감쌈
http.ListenAndServe(":8080", handler)mux 자체도 http.Handler — 미들웨어가 둘러싸기 자연스럽습니다.
체인 #
여러 미들웨어를 — 함수 합성처럼.
handler := logger(recoverer(authMiddleware(mux)))순서: logger → recoverer → auth → mux. 안쪽이 먼저 실행되는 게 아니라 — 바깥쪽이 먼저 ServeHTTP 진입. 호출 흐름은:
logger.ServeHTTP시작 (start 기록)- →
recoverer.ServeHTTP시작 (defer recover) - →
auth.ServeHTTP시작 (인증 검사) - →
mux.ServeHTTP(실제 라우팅) - ← 응답 후 차례로 빠져나옴 (로그 기록 등)
함수 합성 모양이라 — 순서가 중요. 보통 outer부터 inner: 로그 → 복구 → 인증 → 라우팅.
체인 헬퍼 #
손으로 중첩하면 가독성이 떨어집니다. 헬퍼 하나로 평평하게.
type Middleware func(http.Handler) http.Handler
func chain(h http.Handler, mw ...Middleware) http.Handler {
for i := len(mw) - 1; i >= 0; i-- {
h = mw[i](h)
}
return h
}
handler := chain(mux, logger, recoverer, authMiddleware)리스트의 앞쪽이 — 바깥쪽 (먼저 실행).
표준 미들웨어 모음 #
1) Recoverer — 패닉 복구 #
func recoverer(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\n%s", err, debug.Stack())
http.Error(w, "internal error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}핸들러에서 panic이 일어나면 — 서버가 죽지 않게 잡고 500 응답. 모든 핸들러를 감싸는 가장 바깥쪽 미들웨어 중 하나.
2) Request ID #
type ctxKey string
const reqIDKey ctxKey = "reqID"
func requestID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := r.Header.Get("X-Request-ID")
if id == "" {
id = uuid.New().String()
}
w.Header().Set("X-Request-ID", id)
ctx := context.WithValue(r.Context(), reqIDKey, id)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func RequestIDFrom(ctx context.Context) string {
if v, ok := ctx.Value(reqIDKey).(string); ok {
return v
}
return ""
}분산 시스템에서 한 요청을 추적하기 위한 표준입니다. context에 넣어 두고 로그에 포함합니다.
중급 #5의 ctx.Value가 어울리는 경우 중 가장 흔한 사례.
3) 응답 상태 캡처 — accesslog 향상 #
기본 ResponseWriter는 — 응답 상태를 기억하지 않습니다. 로그에 상태/크기를 넣으려면 작은 래퍼.
type statusRecorder struct {
http.ResponseWriter
status int
bytes int
}
func (r *statusRecorder) WriteHeader(code int) {
r.status = code
r.ResponseWriter.WriteHeader(code)
}
func (r *statusRecorder) Write(b []byte) (int, error) {
if r.status == 0 {
r.status = http.StatusOK
}
n, err := r.ResponseWriter.Write(b)
r.bytes += n
return n, err
}
func accessLog(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rec := &statusRecorder{ResponseWriter: w}
start := time.Now()
next.ServeHTTP(rec, r)
log.Printf("%s %s %d %dB %s",
r.Method, r.URL.Path, rec.status, rec.bytes, time.Since(start))
})
}이 패턴이 — chi, gin 같은 프레임워크 안에 똑같이 있습니다. 직접 짜봤다면 그 코드도 익숙해집니다.
4) 인증 #
type userKey struct{}
func authMiddleware(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
}
user, err := verifyToken(token)
if err != nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), userKey{}, user)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func UserFrom(ctx context.Context) (User, bool) {
u, ok := ctx.Value(userKey{}).(User)
return u, ok
}ctx 키는 — 외부에 노출 안 되는 비공개 타입으로. 다른 패키지의 같은 이름 string 키와 충돌 안 함.
5) CORS #
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, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}프로덕션은 — Origin 검증, credentials 처리 등이 더 들어갑니다. rs/cors 패키지가 표준에 가깝게 처리.
6) Rate limit #
import "golang.org/x/time/rate"
func limit(next http.Handler) http.Handler {
limiter := rate.NewLimiter(10, 30) // 10 req/sec, burst 30
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !limiter.Allow() {
http.Error(w, "too many", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}전체 서버 단위입니다. 사용자별로 제한하려면 — IP/사용자 ID별 limiter map을 사용합니다. 큰 트래픽이면 Redis 기반 분산 limiter를 검토합니다.
라우트 단위 미들웨어 — 표준 ServeMux의 한계 #
표준 ServeMux는 — 라우트 단위로 미들웨어를 끼우는 헬퍼가 없습니다. 손으로 감싸는 식.
mux.HandleFunc("GET /admin", authMiddleware(http.HandlerFunc(adminHandler)).ServeHTTP)라우트가 많아지면 보일러플레이트가 거슬립니다 — chi 같은 라우터의 r.Use가 자연스러워지는 경우입니다.
r.Group(func(r chi.Router) {
r.Use(authMiddleware)
r.Get("/admin", adminHandler)
r.Get("/me", meHandler)
})#2의 라우터 선택 트레이드오프와 같은 맥락입니다.
표준 패턴 모음 #
전형적인 서버의 미들웨어 스택:
handler := chain(mux,
requestID, // 1) 요청 ID 부여
accessLog, // 2) 액세스 로그
recoverer, // 3) 패닉 복구
cors, // 4) CORS 헤더
limit, // 5) Rate limit
// auth는 라우터별로
)순서는 — recoverer가 안쪽이면 좋겠지만, 그러면 그 위 미들웨어의 panic을 못 잡습니다. 로그 → 복구 → 그 외 순서가 보통.
함정 — r.WithContext를 안 넘김
#
func mw(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), key, val)
next.ServeHTTP(w, r) // ✗ 새 ctx 안 전달
})
}r.WithContext(ctx)로 — 새 Request를 만들어 넘겨야 컨텍스트 값이 전달됩니다.
next.ServeHTTP(w, r.WithContext(ctx))함정 — next.ServeHTTP 빼먹기
#
조건부 인증 같은 경우에 — 인증 실패하면 응답 보내고 return. 인증 성공하면 next.ServeHTTP 부르기. 두 경로가 헷갈리면 — 응답이 두 번 나가거나 아예 안 나가는 버그.
미들웨어의 좋은 디자인 #
- 단일 책임 — 한 미들웨어에 한 가지 일
- 순서 의도 명시 — 주석으로 적어 두기
- 공유 상태 최소화 — 가능하면 ctx만 통과
- 에러는 응답으로 표현 — 미들웨어 자체가 panic 하지 않게
- 미들웨어도 테스트 —
httptest로 단순 검증
마무리 #
이번 글에서 정리한 내용:
- 미들웨어 =
func(http.Handler) http.Handler - 로거, 복구, 요청 ID, 인증, CORS, rate limit — 표준 빌딩 블록
- 체인 헬퍼 — 평평한 등록
- 순서 — 바깥쪽이 먼저 실행, 보통 로그 → 복구 → 그 외
r.WithContext빼먹지 말기- status recorder로 응답 상태/크기 캡처
- 표준 mux는 라우트 단위 미들웨어 약함 — chi가 매끄러움
다음 글(#6 테스트와 배포)에서는 — httptest로 핸들러 테스트, Docker 멀티스테이지 빌드, 그리고 작은 서버를 운영 환경에 올리는 표준 패턴을 정리합니다.