# Laravel Middleware 완벽 가이드: 인증, 속도 제한, 커스텀 미들웨어 구축 > Laravel 미들웨어의 파이프라인 구조부터 인증, 속도 제한(Rate Limiting), 커스텀 미들웨어 작성, 프로덕션 패턴까지 실무 코드 예제와 함께 체계적으로 정리합니다. - Published: 2026-04-16 - Updated: 2026-04-16 - Author: SharpSkill - Tags: laravel, middleware, authentication, rate-limiting, php - Reading time: 10 min --- 미들웨어는 모든 견고한 Laravel 애플리케이션의 핵심 기반입니다. HTTP 요청과 애플리케이션 로직 사이에 위치하는 필터 계층으로서, 접근 권한을 검증하고 요청 빈도를 제한하며 데이터를 변환하는 역할을 수행합니다. 컨트롤러가 단 한 줄의 코드도 실행하기 전에 이 모든 처리가 완료됩니다. 인증부터 속도 제한, 맞춤형 검증 로직까지 Laravel을 프로덕션 환경에서 운영하려면 미들웨어 파이프라인을 깊이 이해해야 합니다. 이 글에서는 미들웨어 파이프라인의 작동 원리, Laravel이 제공하는 내장 미들웨어, 그리고 특정 요구사항에 맞는 커스텀 미들웨어 작성 방법을 실제 코드 예제와 함께 살펴봅니다. > **미들웨어가 필수적인 이유** > > 미들웨어는 인증, 로깅, 입력 검증과 같은 횡단 관심사(cross-cutting concerns)를 비즈니스 로직으로부터 분리합니다. 모든 컨트롤러에서 보안 검사를 반복하는 대신, 미들웨어에서 한 번 정의하면 관련 라우트에 자동으로 적용됩니다. 결과적으로 컨트롤러는 간결해지고, 보안 메커니즘은 일관되며, 코드베이스 유지보수가 훨씬 수월해집니다. ## 미들웨어 파이프라인의 작동 원리 Laravel의 미들웨어 파이프라인은 동심원 형태의 계층 구조로 이해할 수 있습니다. 모든 HTTP 요청은 바깥 계층에서 안쪽으로 각 미들웨어를 순차적으로 통과하며 컨트롤러에 도달합니다. 응답은 같은 계층을 역순으로 거슬러 올라갑니다. 이 원리는 요청 처리 전후 모두에서 코드를 실행하는 미들웨어를 살펴보면 명확하게 드러납니다. 다음 예제는 요청의 처리 시간을 측정합니다. `$next($request)` 호출 전의 코드는 요청이 컨트롤러에 도달하기 전에 실행되고, 호출 후의 코드는 응답이 생성된 이후에 실행됩니다. 이것이 바로 파이프라인의 양파 껍질(onion) 아키텍처입니다. ```php // app/Http/Middleware/LogRequestTime.php namespace App\Http\Middleware; use Closure; use Illuminate\Http\Request; use Illuminate\Support\Facades\Log; use Symfony\Component\HttpFoundation\Response; class LogRequestTime { public function handle(Request $request, Closure $next): Response { $start = microtime(true); // Capture start time $response = $next($request); // Pass to next middleware $duration = microtime(true) - $start; Log::info('Request completed', [ 'url' => $request->url(), 'method' => $request->method(), 'duration' => round($duration * 1000, 2) . 'ms', ]); return $response; // Return response up the stack } } ``` 핵심은 `$next($request)`에 있습니다. 이 호출이 요청을 파이프라인의 다음 미들웨어로 전달합니다. 이 호출 이전의 코드는 Before 미들웨어 로직이고, 이후의 코드는 After 미들웨어 로직입니다. 만약 인증 검사 실패 등의 이유로 `$next()`가 호출되지 않으면 요청은 컨트롤러에 도달하지 못합니다. ## 인증 미들웨어 인증은 Laravel에서 미들웨어를 가장 빈번하게 활용하는 영역입니다. 내장 `auth` 미들웨어는 인증된 사용자만 보호된 라우트에 접근할 수 있도록 보장합니다. 인증되지 않은 사용자는 웹 요청의 경우 로그인 페이지로 리다이렉트되고, API 요청의 경우 401 응답을 받게 됩니다. 라우트는 개별적으로 또는 그룹 단위로 보호할 수 있습니다. 그룹 방식이 권장되는데, 라우트 정의를 깔끔하게 유지하고 실수로 라우트가 보호되지 않는 상황을 방지하기 때문입니다. ```php // routes/web.php use App\Http\Controllers\DashboardController; use App\Http\Controllers\ProfileController; // Single route protection Route::get('/dashboard', [DashboardController::class, 'index']) ->middleware('auth'); // Group protection for multiple routes Route::middleware('auth')->group(function () { Route::get('/profile', [ProfileController::class, 'show']); Route::put('/profile', [ProfileController::class, 'update']); Route::delete('/profile', [ProfileController::class, 'destroy']); }); ``` ### 다중 가드(Multi-Guard) 인증 Laravel은 여러 인증 가드를 동시에 지원합니다. 가드는 사용자가 어떤 방식으로 인증되는지를 정의합니다. 세션, API 토큰, 또는 커스텀 메커니즘이 될 수 있습니다. 콜론 뒤에 가드 이름을 지정하면 특정 라우트 그룹에 적용할 인증 방식을 제어할 수 있습니다. ```php // routes/api.php // API routes use the 'sanctum' guard Route::middleware('auth:sanctum')->group(function () { Route::get('/user', fn (Request $request) => $request->user()); Route::apiResource('/orders', OrderController::class); }); // routes/web.php // Admin routes use a custom 'admin' guard Route::middleware('auth:admin')->prefix('admin')->group(function () { Route::get('/dashboard', [AdminController::class, 'index']); Route::get('/users', [AdminController::class, 'users']); }); ``` 이렇게 분리하면 API 라우트는 Sanctum 토큰으로, 관리자 라우트는 별도의 가드로 보호할 수 있습니다. 각 가드는 고유한 사용자 모델, 프로바이더, 드라이버를 사용할 수 있어 복잡한 애플리케이션에서 유연한 아키텍처를 구현할 수 있습니다. > **guest 미들웨어** > > `auth` 미들웨어 외에 반대 동작을 수행하는 `guest` 미들웨어가 존재합니다. 인증되지 않은 사용자만 통과시키는 역할을 합니다. 대표적인 활용 사례는 로그인 및 회원가입 페이지로, 이미 인증된 사용자를 대시보드로 자동 리다이렉트해야 하는 경우입니다. 사용법은 동일합니다: `Route::middleware('guest')->get('/login', ...)`. ## Throttle을 활용한 속도 제한 속도 제한은 애플리케이션을 악의적 사용, 무차별 대입 공격, 의도치 않은 과부하로부터 보호합니다. Laravel의 `throttle` 미들웨어는 클라이언트가 정해진 시간 내에 보낼 수 있는 요청 수를 제한합니다. 한도를 초과하면 Laravel이 자동으로 HTTP 429(Too Many Requests) 상태 코드로 응답합니다. 가장 간단한 설정 방식은 라우트 정의에서 직접 지정하는 것입니다. 첫 번째 매개변수는 최대 요청 수, 두 번째는 분 단위의 시간 창입니다. ```php // routes/api.php // Allow 60 requests per minute per user Route::middleware('throttle:60,1')->group(function () { Route::get('/posts', [PostController::class, 'index']); Route::get('/posts/{post}', [PostController::class, 'show']); }); // Stricter limit for write operations Route::middleware(['auth:sanctum', 'throttle:10,1'])->group(function () { Route::post('/posts', [PostController::class, 'store']); Route::put('/posts/{post}', [PostController::class, 'update']); }); ``` 이 기본 설정은 단순한 사용 사례에 충분합니다. 사용자 그룹별로 차등 제한이 필요한 경우처럼 보다 세밀한 요구사항에는 Laravel의 명명된 Rate Limiter를 활용합니다. ### 명명된 Rate Limiter 명명된 Rate Limiter는 서비스 프로바이더에서 정의하며 동적이고 문맥 기반의 설정을 가능하게 합니다. 다음 예제는 계층형 시스템을 보여줍니다. 엔터프라이즈 고객은 일반 사용자보다 더 높은 한도를 제공받고, 익명 접근은 가장 엄격하게 제한됩니다. ```php // app/Providers/AppServiceProvider.php use Illuminate\Cache\RateLimiting\Limit; use Illuminate\Support\Facades\RateLimiter; use Illuminate\Http\Request; public function boot(): void { // API rate limiter with tiered access RateLimiter::for('api', function (Request $request) { $user = $request->user(); if ($user?->hasSubscription('enterprise')) { return Limit::perMinute(500)->by($user->id); // Enterprise: 500/min } if ($user) { return Limit::perMinute(100)->by($user->id); // Authenticated: 100/min } return Limit::perMinute(20)->by($request->ip()); // Anonymous: 20/min }); // Login limiter to prevent brute force RateLimiter::for('login', function (Request $request) { return Limit::perMinute(5) ->by($request->ip()) // Key by IP address ->response(function () { // Custom exceeded response return response()->json([ 'message' => 'Too many login attempts. Try again in a minute.', ], 429); }); }); } ``` 명명된 Limiter는 숫자 값을 직접 지정하는 대신 라우트 정의에서 이름으로 참조합니다. ```php // routes/api.php Route::middleware('throttle:api')->group(function () { Route::apiResource('/posts', PostController::class); }); // routes/web.php Route::middleware('throttle:login') ->post('/login', [AuthController::class, 'login']); ``` 이 접근 방식의 장점은 중앙 집중식 관리에 있습니다. 한도를 조정하면 해당 Limiter를 사용하는 모든 라우트의 동작이 자동으로 변경됩니다. `by()` 메서드는 요청을 어떤 기준으로 그룹화할지 제어합니다. 사용자 ID, IP 주소, 또는 임의의 조합이 가능합니다. ## 커스텀 미들웨어 작성 내장 미들웨어로 충분하지 않을 때에는 단일 Artisan 명령으로 새로운 미들웨어 클래스를 생성할 수 있습니다. ```bash php artisan make:middleware EnsureUserHasRole ``` 이 명령은 `app/Http/Middleware/` 디렉터리에 `handle` 메서드가 준비된 클래스를 생성합니다. 이후 원하는 로직을 구현하면 됩니다. ### 역할 기반 접근 제어 커스텀 미들웨어의 대표적인 활용 사례는 역할 기반 인가(authorization)입니다. 다음 미들웨어는 인증된 사용자가 요구되는 역할 중 하나를 보유하고 있는지 확인합니다. 스프레드 연산자(`...$roles`)를 통해 가변 개수의 역할을 매개변수로 받을 수 있습니다. ```php // app/Http/Middleware/EnsureUserHasRole.php namespace App\Http\Middleware; use Closure; use Illuminate\Http\Request; use Symfony\Component\HttpFoundation\Response; class EnsureUserHasRole { public function handle(Request $request, Closure $next, string ...$roles): Response { $user = $request->user(); if (! $user || ! $user->hasAnyRole($roles)) { abort(403, 'Insufficient permissions.'); } return $next($request); } } ``` `bootstrap/app.php`에서 별칭(alias)으로 등록하면 라우트 정의에서 짧고 직관적인 이름으로 사용할 수 있습니다. 매개변수는 콜론 뒤에 전달하며, 여러 개는 쉼표로 구분합니다. ```php // bootstrap/app.php ->withMiddleware(function (Middleware $middleware) { $middleware->alias([ 'role' => \App\Http\Middleware\EnsureUserHasRole::class, ]); }) // routes/web.php Route::middleware('role:admin')->group(function () { Route::get('/admin', [AdminController::class, 'index']); }); // Multiple roles: admin OR editor can access Route::middleware('role:admin,editor')->group(function () { Route::resource('/articles', ArticleController::class); }); ``` ### API 요청 정제(Sanitization) 또 다른 실용적인 사례는 수신 API 요청의 유효성 검사와 정제입니다. 다음 미들웨어는 POST 요청이 올바른 Content-Type을 사용하는지 확인하고, 모든 문자열 입력값에서 불필요한 공백을 제거합니다. ```php // app/Http/Middleware/ApiRequestSanitizer.php namespace App\Http\Middleware; use Closure; use Illuminate\Http\Request; use Symfony\Component\HttpFoundation\Response; class ApiRequestSanitizer { public function handle(Request $request, Closure $next): Response { // Reject non-JSON requests on API routes if (! $request->expectsJson() && $request->isMethod('POST')) { return response()->json( ['error' => 'Content-Type must be application/json'], 415 ); } // Trim all string inputs $input = $request->all(); array_walk_recursive($input, function (&$value) { if (is_string($value)) { $value = trim($value); } }); $request->merge($input); return $next($request); } } ``` 이 미들웨어는 API 미들웨어 그룹의 가장 앞에 배치하는 것이 이상적입니다. 이후 실행되는 모든 미들웨어와 컨트롤러가 이미 정제된 데이터를 받을 수 있기 때문입니다. ## Laravel 12의 미들웨어 등록 Laravel 11부터 미들웨어 설정 방식이 근본적으로 변경되었습니다. 기존의 `App\Http\Kernel` 대신 `bootstrap/app.php`에서 글로벌 미들웨어, 그룹 미들웨어, 별칭, 실행 우선순위를 모두 중앙에서 정의합니다. ```php // bootstrap/app.php use Illuminate\Foundation\Application; use Illuminate\Foundation\Configuration\Middleware; return Application::configure(basePath: dirname(__DIR__)) ->withMiddleware(function (Middleware $middleware) { // Global middleware (runs on every request) $middleware->append( \App\Http\Middleware\LogRequestTime::class ); // Add to the 'web' middleware group $middleware->web(append: [ \App\Http\Middleware\TrackPageViews::class, ]); // Add to the 'api' middleware group $middleware->api(prepend: [ \App\Http\Middleware\ApiRequestSanitizer::class, ]); // Register aliases for route-level use $middleware->alias([ 'role' => \App\Http\Middleware\EnsureUserHasRole::class, 'subscribed' => \App\Http\Middleware\EnsureUserIsSubscribed::class, ]); // Control execution order $middleware->priority([ \Illuminate\Session\Middleware\StartSession::class, \Illuminate\Auth\Middleware\Authenticate::class, \App\Http\Middleware\EnsureUserHasRole::class, ]); }) ->create(); ``` `append()`와 `prepend()` 메서드는 미들웨어를 그룹의 끝 또는 앞에 삽입할지 제어합니다. 그룹 지정 없이 사용하는 글로벌 미들웨어(`$middleware->append(...)`)는 웹 라우트든 API 라우트든 모든 요청에서 실행됩니다. > **실행 순서에 주의** > > 미들웨어 실행 순서는 매우 중요합니다. 세션이 시작되어야 인증을 확인할 수 있고, 인증이 완료되어야 인가를 수행할 수 있습니다. 속도 제한은 리소스를 많이 소모하는 비즈니스 로직 이전에 적용되어야 합니다. `priority()` 메서드는 라우트에서 미들웨어가 어떤 순서로 정의되었는지와 무관하게 전역적으로 이 순서를 강제합니다. ## 종료 가능한 미들웨어(Terminable Middleware) 모든 작업이 클라이언트에 응답을 보내기 전에 완료될 필요는 없습니다. 종료 가능한 미들웨어는 `terminate()` 메서드를 추가로 구현하며, 이 메서드는 응답이 완전히 전송된 후에 실행됩니다. 클라이언트는 더 빠르게 응답을 받고, 애플리케이션은 백그라운드에서 후속 처리를 수행합니다. ```php // app/Http/Middleware/CollectAnalytics.php namespace App\Http\Middleware; use Closure; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Symfony\Component\HttpFoundation\Response; class CollectAnalytics { public function handle(Request $request, Closure $next): Response { return $next($request); // Pass through without delay } public function terminate(Request $request, Response $response): void { // Runs after response is sent to client DB::table('analytics')->insert([ 'path' => $request->path(), 'method' => $request->method(), 'status_code' => $response->getStatusCode(), 'user_id' => $request->user()?->id, 'ip' => $request->ip(), 'created_at' => now(), ]); } } ``` 종료 가능한 미들웨어의 대표적인 활용 사례로는 분석 데이터 수집, 감사 로그 기록, 캐시 워밍, 알림 발송 등이 있습니다. 중요한 점은 Laravel이 `terminate()` 메서드 실행 시 미들웨어의 새 인스턴스를 생성한다는 것입니다. 미들웨어가 컨테이너에 싱글톤으로 등록되지 않은 경우, `handle()`에서 설정한 상태를 `terminate()`에서 사용할 수 없습니다. ## 실전 미들웨어 패턴 프로덕션 환경에서는 표준 사례를 넘어서는 반복적인 미들웨어 패턴이 자주 사용됩니다. 특히 유용한 두 가지 패턴은 유지보수 모드 우회와 보안 헤더 설정입니다. ### 유지보수 모드 우회 유지보수 모드에서도 개발자와 관리자는 변경 사항을 확인하기 위해 애플리케이션에 접근할 수 있어야 합니다. 다음 미들웨어는 특정 IP 주소에서의 접근을 허용하고, 나머지 방문자에게는 유지보수 페이지를 표시합니다. ```php // app/Http/Middleware/MaintenanceBypass.php class MaintenanceBypass { private array $allowedIps = ['192.168.1.0/24', '10.0.0.1']; public function handle(Request $request, Closure $next): Response { if (app()->isDownForMaintenance()) { foreach ($this->allowedIps as $ip) { if ($request->ip() === $ip) { return $next($request); } } } return $next($request); } } ``` ### 보안 헤더 HTTP 보안 헤더는 프로덕션 애플리케이션의 필수 구성 요소입니다. 웹 서버 설정에서 지정하는 대신 미들웨어에서 직접 정의하면 버전 관리가 용이하고, 사용하는 웹 서버에 관계없이 헤더가 일관되게 적용됩니다. ```php // app/Http/Middleware/SecurityHeaders.php class SecurityHeaders { public function handle(Request $request, Closure $next): Response { $response = $next($request); $response->headers->set('X-Content-Type-Options', 'nosniff'); $response->headers->set('X-Frame-Options', 'SAMEORIGIN'); $response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin'); $response->headers->set( 'Strict-Transport-Security', 'max-age=31536000; includeSubDomains' ); return $response; } } ``` 두 미들웨어 모두 모든 요청에 적용되어야 하므로 글로벌 미들웨어로 등록하기에 적합합니다. 보안 헤더 미들웨어는 컨트롤러 처리 이후 응답을 수정하는 After 미들웨어의 전형적인 예시입니다. ## 결론 Laravel 미들웨어는 단순한 필터 계층을 넘어서 요청 처리의 근간을 이루며, 보안 및 인프라 관심사를 비즈니스 로직으로부터 깔끔하게 분리하는 아키텍처를 가능하게 합니다. 핵심 내용을 정리하면 다음과 같습니다. - **파이프라인 아키텍처**: 모든 요청은 미들웨어 체인을 순차적으로 통과합니다. `$next($request)` 기준으로 코드의 위치가 컨트롤러 처리 전후의 실행 시점을 결정합니다. - **인증 미들웨어**: `auth` 미들웨어는 인증되지 않은 접근으로부터 라우트를 보호합니다. 다중 가드 설정으로 웹, API, 관리자 영역에 각각 다른 인증 메커니즘을 적용할 수 있습니다. - **속도 제한**: `throttle` 미들웨어와 명명된 Rate Limiter는 단순한 제한부터 역할 기반 계층형 시스템까지 유연한 설정을 제공합니다. - **커스텀 미들웨어**: `php artisan make:middleware` 명령으로 새 미들웨어 클래스를 즉시 생성할 수 있습니다. 매개변수, 스프레드 연산자, 별칭을 활용하면 재사용 가능하고 설정 가능한 미들웨어를 구축할 수 있습니다. - **bootstrap/app.php 등록**: Laravel 11 이후 모든 미들웨어 설정은 단일 파일에서 중앙 관리됩니다. 글로벌 미들웨어, 그룹, 별칭, 우선순위가 모두 포함됩니다. - **종료 가능한 미들웨어**: `terminate()` 메서드를 통해 응답 전송 후 비차단(non-blocking) 후처리가 가능합니다. 분석 데이터 수집과 감사 로그에 적합합니다. - **프로덕션 패턴**: 유지보수 모드 우회와 보안 헤더는 프로덕션 Laravel 애플리케이션에 반드시 포함되어야 하는 미들웨어의 대표적인 예시입니다. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/ko/blog/laravel/laravel-middleware-deep-dive