Laravel Middleware 완벽 가이드: 인증, 속도 제한, 커스텀 미들웨어 구축

Laravel 미들웨어의 파이프라인 구조부터 인증, 속도 제한(Rate Limiting), 커스텀 미들웨어 작성, 프로덕션 패턴까지 실무 코드 예제와 함께 체계적으로 정리합니다.

Laravel 미들웨어 아키텍처 - 요청 파이프라인, 인증, 속도 제한 구조 다이어그램

미들웨어는 모든 견고한 Laravel 애플리케이션의 핵심 기반입니다. HTTP 요청과 애플리케이션 로직 사이에 위치하는 필터 계층으로서, 접근 권한을 검증하고 요청 빈도를 제한하며 데이터를 변환하는 역할을 수행합니다. 컨트롤러가 단 한 줄의 코드도 실행하기 전에 이 모든 처리가 완료됩니다. 인증부터 속도 제한, 맞춤형 검증 로직까지 Laravel을 프로덕션 환경에서 운영하려면 미들웨어 파이프라인을 깊이 이해해야 합니다. 이 글에서는 미들웨어 파이프라인의 작동 원리, Laravel이 제공하는 내장 미들웨어, 그리고 특정 요구사항에 맞는 커스텀 미들웨어 작성 방법을 실제 코드 예제와 함께 살펴봅니다.

미들웨어가 필수적인 이유

미들웨어는 인증, 로깅, 입력 검증과 같은 횡단 관심사(cross-cutting concerns)를 비즈니스 로직으로부터 분리합니다. 모든 컨트롤러에서 보안 검사를 반복하는 대신, 미들웨어에서 한 번 정의하면 관련 라우트에 자동으로 적용됩니다. 결과적으로 컨트롤러는 간결해지고, 보안 메커니즘은 일관되며, 코드베이스 유지보수가 훨씬 수월해집니다.

미들웨어 파이프라인의 작동 원리

Laravel의 미들웨어 파이프라인은 동심원 형태의 계층 구조로 이해할 수 있습니다. 모든 HTTP 요청은 바깥 계층에서 안쪽으로 각 미들웨어를 순차적으로 통과하며 컨트롤러에 도달합니다. 응답은 같은 계층을 역순으로 거슬러 올라갑니다. 이 원리는 요청 처리 전후 모두에서 코드를 실행하는 미들웨어를 살펴보면 명확하게 드러납니다.

다음 예제는 요청의 처리 시간을 측정합니다. $next($request) 호출 전의 코드는 요청이 컨트롤러에 도달하기 전에 실행되고, 호출 후의 코드는 응답이 생성된 이후에 실행됩니다. 이것이 바로 파이프라인의 양파 껍질(onion) 아키텍처입니다.

app/Http/Middleware/LogRequestTime.phpphp
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 응답을 받게 됩니다.

라우트는 개별적으로 또는 그룹 단위로 보호할 수 있습니다. 그룹 방식이 권장되는데, 라우트 정의를 깔끔하게 유지하고 실수로 라우트가 보호되지 않는 상황을 방지하기 때문입니다.

routes/web.phpphp
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 토큰, 또는 커스텀 메커니즘이 될 수 있습니다. 콜론 뒤에 가드 이름을 지정하면 특정 라우트 그룹에 적용할 인증 방식을 제어할 수 있습니다.

routes/api.phpphp
// 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) 상태 코드로 응답합니다.

가장 간단한 설정 방식은 라우트 정의에서 직접 지정하는 것입니다. 첫 번째 매개변수는 최대 요청 수, 두 번째는 분 단위의 시간 창입니다.

routes/api.phpphp
// 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는 서비스 프로바이더에서 정의하며 동적이고 문맥 기반의 설정을 가능하게 합니다. 다음 예제는 계층형 시스템을 보여줍니다. 엔터프라이즈 고객은 일반 사용자보다 더 높은 한도를 제공받고, 익명 접근은 가장 엄격하게 제한됩니다.

app/Providers/AppServiceProvider.phpphp
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는 숫자 값을 직접 지정하는 대신 라우트 정의에서 이름으로 참조합니다.

routes/api.phpphp
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 주소, 또는 임의의 조합이 가능합니다.

Laravel 면접 준비가 되셨나요?

인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.

커스텀 미들웨어 작성

내장 미들웨어로 충분하지 않을 때에는 단일 Artisan 명령으로 새로운 미들웨어 클래스를 생성할 수 있습니다.

bash
php artisan make:middleware EnsureUserHasRole

이 명령은 app/Http/Middleware/ 디렉터리에 handle 메서드가 준비된 클래스를 생성합니다. 이후 원하는 로직을 구현하면 됩니다.

역할 기반 접근 제어

커스텀 미들웨어의 대표적인 활용 사례는 역할 기반 인가(authorization)입니다. 다음 미들웨어는 인증된 사용자가 요구되는 역할 중 하나를 보유하고 있는지 확인합니다. 스프레드 연산자(...$roles)를 통해 가변 개수의 역할을 매개변수로 받을 수 있습니다.

app/Http/Middleware/EnsureUserHasRole.phpphp
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)으로 등록하면 라우트 정의에서 짧고 직관적인 이름으로 사용할 수 있습니다. 매개변수는 콜론 뒤에 전달하며, 여러 개는 쉼표로 구분합니다.

bootstrap/app.phpphp
->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을 사용하는지 확인하고, 모든 문자열 입력값에서 불필요한 공백을 제거합니다.

app/Http/Middleware/ApiRequestSanitizer.phpphp
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에서 글로벌 미들웨어, 그룹 미들웨어, 별칭, 실행 우선순위를 모두 중앙에서 정의합니다.

bootstrap/app.phpphp
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() 메서드를 추가로 구현하며, 이 메서드는 응답이 완전히 전송된 후에 실행됩니다. 클라이언트는 더 빠르게 응답을 받고, 애플리케이션은 백그라운드에서 후속 처리를 수행합니다.

app/Http/Middleware/CollectAnalytics.phpphp
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 주소에서의 접근을 허용하고, 나머지 방문자에게는 유지보수 페이지를 표시합니다.

app/Http/Middleware/MaintenanceBypass.phpphp
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 보안 헤더는 프로덕션 애플리케이션의 필수 구성 요소입니다. 웹 서버 설정에서 지정하는 대신 미들웨어에서 직접 정의하면 버전 관리가 용이하고, 사용하는 웹 서버에 관계없이 헤더가 일관되게 적용됩니다.

app/Http/Middleware/SecurityHeaders.phpphp
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 면접 준비가 되셨나요?

인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.

결론

Laravel 미들웨어는 단순한 필터 계층을 넘어서 요청 처리의 근간을 이루며, 보안 및 인프라 관심사를 비즈니스 로직으로부터 깔끔하게 분리하는 아키텍처를 가능하게 합니다. 핵심 내용을 정리하면 다음과 같습니다.

  • 파이프라인 아키텍처: 모든 요청은 미들웨어 체인을 순차적으로 통과합니다. $next($request) 기준으로 코드의 위치가 컨트롤러 처리 전후의 실행 시점을 결정합니다.
  • 인증 미들웨어: auth 미들웨어는 인증되지 않은 접근으로부터 라우트를 보호합니다. 다중 가드 설정으로 웹, API, 관리자 영역에 각각 다른 인증 메커니즘을 적용할 수 있습니다.
  • 속도 제한: throttle 미들웨어와 명명된 Rate Limiter는 단순한 제한부터 역할 기반 계층형 시스템까지 유연한 설정을 제공합니다.
  • 커스텀 미들웨어: php artisan make:middleware 명령으로 새 미들웨어 클래스를 즉시 생성할 수 있습니다. 매개변수, 스프레드 연산자, 별칭을 활용하면 재사용 가능하고 설정 가능한 미들웨어를 구축할 수 있습니다.
  • bootstrap/app.php 등록: Laravel 11 이후 모든 미들웨어 설정은 단일 파일에서 중앙 관리됩니다. 글로벌 미들웨어, 그룹, 별칭, 우선순위가 모두 포함됩니다.
  • 종료 가능한 미들웨어: terminate() 메서드를 통해 응답 전송 후 비차단(non-blocking) 후처리가 가능합니다. 분석 데이터 수집과 감사 로그에 적합합니다.
  • 프로덕션 패턴: 유지보수 모드 우회와 보안 헤더는 프로덕션 Laravel 애플리케이션에 반드시 포함되어야 하는 미들웨어의 대표적인 예시입니다.

연습을 시작하세요!

면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.

태그

#laravel
#middleware
#authentication
#rate-limiting
#php

공유

관련 기사