웹 프레임워크 미들웨어 언박싱 - 책임 연쇄 패턴 심층 분석
Daniel Hayes
Full-Stack Engineer · Leapcell

소개
백엔드 개발의 세계에서 들어오는 요청을 처리하는 것은 종종 인증, 로깅, 데이터 파싱, 오류 처리 등 독립적이면서도 순차적인 여러 작업으로 이루어집니다. 이러한 관심사를 모든 개별 라우트 핸들러에 수동으로 엮는 것은 복잡하고 유지보수하기 어려운 코드로 이어집니다. 바로 여기서 미들웨어가 빛을 발하며, 이러한 관심사를 분리하는 구조적이고 우아한 솔루션을 제공합니다. 단순한 편의성을 넘어, 미들웨어의 근본적인 아키텍처를 이해하는 것은 강력하고 확장 가능하며 확장 가능한 웹 애플리케이션을 구축하는 데 매우 중요합니다. 이 아티클에서는 Express(Node.js), Gin(Go), Axum(Rust)과 같은 인기 프레임워크가 미들웨어를 어떻게 구현하는지 분석하여, 이를 책임 연쇄(Chain of Responsibility) 디자인 패턴의 전형적인 예로 보여줄 것입니다.
핵심 개념 이해
프레임워크 자체에 들어가기 전에, 주요 용어에 대한 공통된 이해를 확립해 봅시다.
- 미들웨어(Middleware): 일반적으로 웹 서버와 애플리케이션 로직(또는 다른 미들웨어) 사이에서 요청을 처리하는 소프트웨어 컴포넌트입니다. 코드를 실행하거나, 요청/응답 객체를 수정하거나, 요청을 종료하거나, 체인의 다음 미들웨어로 요청을 전달할 수 있습니다.
 - 책임 연쇄(Chain of Responsibility) 패턴: 요청을 핸들러 체인을 따라 전달할 수 있도록 하는 행동 디자인 패턴입니다. 각 핸들러는 요청을 처리하거나 체인의 다음 핸들러로 전달하기로 결정합니다. 이 패턴은 요청 발신자와 수신자 간의 느슨한 결합을 촉진합니다.
 - 요청/응답 객체(Request/Response Object): 들어오는 클라이언트 요청 세부 정보(헤더, 본문, URL 등)와 나가는 서버 응답(상태, 헤더, 본문)을 캡슐화하는 데이터 구조입니다. 미들웨어는 일반적으로 이러한 객체를 조작하거나 잠재적으로 수정합니다.
 - 다음 함수/핸들러(Next Function/Handler): 미들웨어에 제공되는 메커니즘(종종 함수 또는 클로저)으로, 호출되면 실행 체인의 후속 미들웨어 또는 최종 라우트 핸들러로 제어권을 넘깁니다.
 
책임 연쇄 동작 방식
미들웨어의 우아함은 주로 책임 연쇄로 구현된 데서 비롯됩니다. 각 미들웨어는 체인에서 "핸들러" 역할을 합니다. 요청이 도착하면 첫 번째 핸들러로 들어갑니다. 이 핸들러는 요청을 처리한 다음 다음 핸들러로 명시적으로 전달하거나, 요청을 완전히 처리하고 응답을 보내 체인을 종료할 수 있습니다.
Express (Node.js)
Express.js는 강력한 미들웨어 시스템으로 널리 알려진 프레임워크 중 하나입니다.
// 간단한 로깅 미들웨어 function logger(req, res, next) { console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`); next(); // 다음 미들웨어 또는 라우트 핸들러로 제어 전달 } // 인증 미들웨어 function authenticate(req, res, next) { const token = req.headers.authorization; if (token === 'Bearer mysecrettoken') { req.user = { id: 1, name: 'Alice' }; // 요청에 사용자 정보 첨부 next(); } else { res.status(401).send('Unauthorized'); } } // Express 애플리케이션 const express = require('express'); const app = express(); app.use(logger); // 로거 미들웨어를 전역으로 적용 app.use(express.json()); // JSON 본문 파싱을 위한 내장 미들웨어 app.get('/protected', authenticate, (req, res) => { // 이 라우트는 authenticate 미들웨어가 next()를 호출해야만 도달할 수 있습니다. res.json({ message: `Welcome, ${req.user.name}!`, data: 'Secret info' }); }); app.get('/', (req, res) => { res.send('Hello World!'); }); app.listen(3000, () => { console.log('Server running on port 3000'); });
Express에서는 app.use()를 사용하여 미들웨어를 등록합니다. next() 함수는 명시적입니다. 없으면 요청 주기가 중단됩니다. 이 디자인은 각 logger 또는 authenticate 함수가 요청을 전달할지 아니면 요청을 완료할지를 결정하는 핸들러인 책임 연쇄를 직접 반영합니다.
Gin (Go)
Go용 인기 HTTP 웹 프레임워크인 Gin도 미들웨어 패턴을 적극적으로 채택하고 있습니다.
package main import ( "fmt" "log" "net/http" "time" "github.com/gin-gonic/gin" ) // 간단한 로깅 미들웨어 func LoggerMiddleware() gin.HandlerFunc { return func(c *gin.Context) { t := time.Now() // 요청 처리 c.Next() // 다음 미들웨어 또는 라우트 핸들러로 제어 전달 // 요청 처리 후 latency := time.Since(t) log.Printf("Request -> %s %s %s took %v", c.Request.Method, c.Request.URL.Path, c.ClientIP(), latency) } } // 인증 미들웨어 func AuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { token := c.GetHeader("Authorization") if token == "Bearer mysecrettoken" { c.Set("user", "Alice") // 컨텍스트에 사용자 정보 저장 c.Next() } else { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) // AbortWithStatusJSON은 체인을 중단하고 응답을 보냅니다. } } } func main() { router := gin.Default() // gin.Default()는 기본적으로 로거 및 복구 미들웨어를 포함합니다. router.Use(LoggerMiddleware()) // 사용자 정의 로거 적용 // 특정 라우트 그룹에 인증 적용 protected := router.Group("/protected") protected.Use(AuthMiddleware()) { protected.GET("/", func(c *gin.Context) { user, exists := c.Get("user") if !exists { c.JSON(http.StatusInternalServerError, gin.H{"error": "User not found in context"}) return } c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("Welcome, %v!", user), "data": "Secret info"}) }) } router.GET("/", func(c *gin.Context) { c.String(http.StatusOK, "Hello World!") }) router.Run(":8080") }
Gin에서 미들웨어 함수는 gin.HandlerFunc 타입입니다. c.Next()는 Express의 next()와 유사하게 체인을 진행합니다. c.AbortWithStatusJSON()은 명시적으로 실행 체인을 중지하고 응답을 보내, 요청을 종료할 수 있는 핸들러로서의 역할을 강화합니다.
Axum (Rust)
상대적으로 새로운 Rust용 웹 프레임워크인 Axum은 Tokio 에코시스템을 기반으로 하며, Rust의 타입 시스템을 활용하여 고성능 및 타입 안전성이 뛰어난 애플리케이션을 구축합니다. 미들웨어 시스템은 tower::Service 트레이트를 사용하여 구현됩니다.
use axum::*; use axum::http::header::{AUTHORIZATION, CONTENT_TYPE}; use axum::http::HeaderValue; use axum::http::StatusCode; use axum::middleware::{self, Next}; use axum::response::Response; use axum::routing::get; use std::time::Instant; use tower_http::trace::TraceLayer; // Axum/Tower의 내장 미들웨어 예시 #[derive(Clone, FromRef)] // AppState에서 State 파생을 위해 FromRef 사용 struct AppState {} // 간단한 로깅 미들웨어 (시연을 위한 사용자 정의 구현) async fn log_middleware(req: Request, next: Next) -> Response { let start = Instant::now(); println!("Request -> {} {}", req.method(), req.uri()); let response = next.run(req).await; // 다음 미들웨어 또는 라우트 핸들러로 제어 전달 println!( "Response <- {} {} took ? ", response.status(), response.body().size_hint().exact(), start.elapsed() ); response } // 인증 미들웨어 async fn auth_middleware(State(_app_state): State<AppState>, mut req: Request, next: Next) -> Result<Response, StatusCode> { let auth_header = req.headers().get(AUTHORIZATION).and_then(|header| header.to_str().ok()); match auth_header { Some(token) if token == "Bearer mysecrettoken" => { // 사용자 정보 첨부 (예: 확장을 사용하여) req.extensions_mut().insert("Alice".to_string()); // 사용자 정보 저장 Ok(next.run(req).await) } _ => Err(StatusCode::UNAUTHORIZED), // 체인을 중지하기 위해 오류 상태 코드 반환 } } #[tokio::main] async fn main() { let app = Router::new() .route("/", get(handler_root)) .route("/protected", get(handler_protected)) .route_layer(middleware::from_fn(auth_middleware)) // auth_middleware를 이 라우트 및 후속 라우트에 적용 .layer(middleware::from_fn(log_middleware)) // log_middleware를 전역으로 적용 .layer(TraceLayer::new_for_http()); // Axum의 내장 TraceLayer 적용 let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); println!("listening on {}", listener.local_addr().unwrap()); axum::serve(listener, app).await.unwrap(); } async fn handler_root() -> String { "Hello, Axum!".to_string() } async fn handler_protected(State(_app_state): State<AppState>, username: axum::extract::Extension<String>) -> String { format!("Welcome, {}! Secret info.", username.0) }
Axum은 middleware::from_fn을 사용하여 비동기 함수를 미들웨어로 변환합니다. next.run(req).await 호출은 명시적으로 요청을 체인을 따라 전달합니다. 미들웨어가 Err (예: auth_middleware의 Err(StatusCode::UNAUTHORIZED))을 반환하면 체인을 단락시켜 추가 핸들러가 실행되지 않도록 합니다. 이 비동기, 결과 중심 접근 방식은 매우 동시적인 환경에서 책임 연쇄 패턴을 강화합니다.
응용 및 이점
미들웨어를 통해 구현된 책임 연쇄 패턴은 다양한 이점을 제공합니다.
- 디커플링(Decoupling): 각 미들웨어는 단일 관심사에 집중하여 다른 미들웨어와 독립적입니다. 이로 인해 코드를 더 쉽게 이해하고, 테스트하고, 유지보수할 수 있습니다.
 - 모듈성(Modularity): 미들웨어 컴포넌트는 핵심 애플리케이션 로직에 영향을 주지 않고 쉽게 추가, 제거 또는 재정렬할 수 있습니다.
 - 재사용성(Reusability): 인증, 로깅 또는 캐싱과 같은 일반적인 기능은 재사용 가능한 미들웨어로 패키지화되어 다양한 라우트 또는 애플리케이션에서 적용할 수 있습니다.
 - 유연성(Flexibility): 미들웨어 실행 순서를 동적으로 제어할 수 있어 요청 처리에 대한 세밀한 제어가 가능합니다.
 - 확장성(Extensibility): 새로운 미들웨어를 생성하여 기존 코드를 수정하지 않고도 새로운 기능을 추가할 수 있습니다.
 
미들웨어의 일반적인 사용 사례는 다음과 같습니다.
- 인증 및 권한 부여: 사용자 자격 증명 및 권한 확인.
 - 로깅 및 모니터링: 디버깅 및 분석을 위한 요청 세부 정보 기록.
 - 데이터 파싱: JSON, URL 인코딩 또는 멀티파트 폼 데이터 처리.
 - 오류 처리: 오류를 일관되게 포착하고 형식 지정.
 - 캐싱: 자주 요청되는 리소스 저장 및 제공.
 - CORS(Cross-Origin Resource Sharing): 브라우저 보안 정책 관리.
 - 속도 제한: 클라이언트 요청 제한을 통해 남용 방지.
 
결론
Express, Gin, Axum과 같은 최신 웹 프레임워크의 미들웨어는 책임 연쇄 디자인 패턴이라는 강력한 기반 위에 구축된 강력하고 보편적인 기능입니다. 이 근본 원리를 이해함으로써 개발자는 더 모듈적이고, 유지보수 가능하며, 확장 가능한 백엔드 애플리케이션을 작성하여 복잡한 요청 흐름을 우아하고 정밀하게 오케스트레이션할 수 있습니다. 이는 탄력적인 소프트웨어 아키텍처를 형성하는 데 있어 디자인 패턴의 지속적인 힘을 입증합니다.