메인 콘텐츠로 건너뛰기

개요

Laravel 프로젝트를 작성하면 에러와 예외의 처리는 미리 설정된 상태로 준비되어 있습니다. 커스터마이즈는 bootstrap/app.phpwithExceptions 메서드로 수행합니다.

예외 처리 플로우

예외가 발생해서 클라이언트에 응답이 반환되기까지의 흐름을 나타냅니다.
// bootstrap/app.php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;

return Application::configure(basePath: dirname(__DIR__))
    ->withExceptions(function (Exceptions $exceptions): void {
        // 예외의 보고·렌더링을 여기서 설정
    })->create();
withExceptions 클로저에 전달되는 $exceptions 객체는 Illuminate\Foundation\Configuration\Exceptions의 인스턴스이며, 애플리케이션 전체의 예외 핸들링을 관리합니다.

디버그 설정

config/app.phpdebug 옵션이 에러 정보의 표시 양을 제어합니다. 기본적으로는 .envAPP_DEBUG 환경 변수의 값이 사용됩니다.
# 로컬 개발
APP_DEBUG=true

# 운영 환경
APP_DEBUG=false
운영 환경에서는 APP_DEBUG를 반드시 false로 하십시오. true로 두면 기밀 정보가 최종 사용자에게 노출될 위험이 있습니다.

예외의 리포트

예외의 리포트는 예외를 로그에 기록하거나 SentryFlare 등의 외부 서비스로 전송하는 처리입니다. 기본적으로는 config/logging.php의 설정에 기반해 로그에 기록됩니다.

커스텀 리포트 콜백

예외의 종류에 따라 다른 리포트 처리를 하고 싶은 경우 report 메서드에 클로저를 전달합니다. Laravel은 클로저의 타입 힌트로부터 예외의 종류를 판단합니다.
use App\Exceptions\InvalidOrderException;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->report(function (InvalidOrderException $e) {
        // 외부 서비스에의 통지 등
    });
})
커스텀 콜백을 등록해도 기본 로그 기록은 계속됩니다. 기본에의 전파를 멈추고 싶은 경우, stop()을 호출하거나 false를 반환합니다.
->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->report(function (InvalidOrderException $e) {
        // ...
    })->stop();
})

report() 헬퍼

에러 페이지를 표시하지 않고 예외만 보고하고 싶은 경우 report() 헬퍼를 사용합니다.
public function isValid(string $value): bool
{
    try {
        // 밸리데이션 처리...
    } catch (Throwable $e) {
        report($e);

        return false;
    }
}
report() 헬퍼는 사용자에의 응답을 중단하지 않고 에러를 기록할 수 있습니다. 백그라운드 잡이나 중요하지 않은 처리의 예외 처리에 편리합니다.

중복 리포트의 방지

같은 예외 인스턴스가 여러 번 report()에 전달되면, 로그에 중복 엔트리가 작성되는 경우가 있습니다. dontReportDuplicates()를 설정하면 같은 인스턴스는 최초 1번만 기록됩니다.
->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->dontReportDuplicates();
})
$original = new RuntimeException('Whoops!');

report($original); // 기록됨

try {
    throw $original;
} catch (Throwable $caught) {
    report($caught); // 무시됨(같은 인스턴스)
}

글로벌 로그 컨텍스트

모든 예외 로그에 공통 정보를 부여하고 싶은 경우 context 메서드를 사용합니다. 사용 가능하다면 현재 사용자 ID는 자동으로 부여됩니다.
->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->context(fn () => [
        'app_version' => config('app.version'),
    ]);
})

예외 클래스에의 context() 메서드 추가

예외 클래스 자신에 context() 메서드를 정의하면, 그 예외에 고유의 컨텍스트 정보를 로그에 포함시킬 수 있습니다.
<?php

namespace App\Exceptions;

use Exception;

class InvalidOrderException extends Exception
{
    public function __construct(
        private readonly int $orderId,
        string $message = '',
    ) {
        parent::__construct($message);
    }

    /**
     * 예외의 컨텍스트 정보를 반환
     *
     * @return array<string, mixed>
     */
    public function context(): array
    {
        return ['order_id' => $this->orderId];
    }
}

로그 레벨의 변경

특정 예외를 특정 로그 레벨로 기록하고 싶은 경우 level 메서드를 사용합니다.
use PDOException;
use Psr\Log\LogLevel;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->level(PDOException::class, LogLevel::CRITICAL);
})

예외 리포트의 스로틀링

대량의 예외가 발생하는 경우, throttle 메서드로 리포트 수를 제어할 수 있습니다.
use Illuminate\Support\Lottery;
use Throwable;

->withExceptions(function (Exceptions $exceptions): void {
    // 1000회에 1회만 랜덤으로 리포트
    $exceptions->throttle(function (Throwable $e) {
        return Lottery::odds(1, 1000);
    });
})
1분당 건수로 제한하고 싶은 경우 Limit을 사용합니다.
use Illuminate\Broadcasting\BroadcastException;
use Illuminate\Cache\RateLimiting\Limit;
use Throwable;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->throttle(function (Throwable $e) {
        if ($e instanceof BroadcastException) {
            return Limit::perMinute(300);
        }
    });
})

예외의 렌더링

렌더링은 예외를 HTTP 응답으로 변환하는 처리입니다. 기본적으로는 Laravel이 자동으로 적절한 응답을 생성하지만, 커스터마이즈도 가능합니다.

커스텀 렌더링 콜백

render 메서드에 클로저를 전달해 예외를 응답으로 변환합니다.
use App\Exceptions\InvalidOrderException;
use Illuminate\Http\Request;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->render(function (InvalidOrderException $e, Request $request) {
        return response()->view('errors.invalid-order', status: 500);
    });
})
내장 예외(NotFoundHttpException 등)의 렌더링도 덮어쓸 수 있습니다. 클로저가 값을 반환하지 않는 경우, 기본 렌더링이 사용됩니다.
use Illuminate\Http\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->render(function (NotFoundHttpException $e, Request $request) {
        if ($request->is('api/*')) {
            return response()->json([
                'message' => 'Record not found.',
            ], 404);
        }
        // null 을 반환하면 기본 404 페이지가 표시됨
    });
})

JSON / HTML 의 자동 판정

Laravel은 요청의 Accept 헤더에 기반해 HTML과 JSON 어느 쪽으로 반환할지를 자동 판정합니다. 이 판정 로직을 커스터마이즈하고 싶은 경우 shouldRenderJsonWhen을 사용합니다.
use Illuminate\Http\Request;
use Throwable;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $e) {
        if ($request->is('admin/*')) {
            return true; // 관리 화면은 항상 JSON 으로 반환
        }

        return $request->expectsJson();
    });
})

응답 전체의 커스터마이즈

respond 메서드를 사용하면 생성된 응답을 추가로 가공할 수 있습니다.
use Symfony\Component\HttpFoundation\Response;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->respond(function (Response $response) {
        if ($response->getStatusCode() === 419) {
            return back()->with([
                'message' => '페이지의 유효 기간이 만료되었습니다. 다시 시도해 주십시오.',
            ]);
        }

        return $response;
    });
})

커스텀 예외 클래스

app/Exceptions/ 디렉터리에 독자적인 예외 클래스를 작성할 수 있습니다. report() 메서드와 render() 메서드를 정의하면 bootstrap/app.php에 설정을 쓰지 않아도 자동으로 호출됩니다.

예외 클래스의 작성

1

예외 클래스 작성

php artisan make:exception InvalidOrderException
2

report() 와 render() 구현

<?php

namespace App\Exceptions;

use Exception;
use Illuminate\Http\Request;
use Illuminate\Http\Response;

class InvalidOrderException extends Exception
{
    public function __construct(
        private readonly int $orderId,
        string $message = 'Invalid order.',
    ) {
        parent::__construct($message);
    }

    /**
     * 예외를 리포트
     */
    public function report(): void
    {
        // 외부 서비스에의 통지 등
    }

    /**
     * 예외를 HTTP 응답으로 변환
     */
    public function render(Request $request): Response
    {
        return response()->view('errors.invalid-order', [
            'orderId' => $this->orderId,
        ], 422);
    }
}
report() 메서드에는 타입 힌트로 의존성 주입을 사용할 수 있습니다. Laravel의 서비스 컨테이너가 자동으로 해결합니다.

ShouldntReport 인터페이스

리포트가 불필요한 예외에는 ShouldntReport 인터페이스를 구현합니다. 이 인터페이스를 구현한 예외는 일체 리포트되지 않습니다.
<?php

namespace App\Exceptions;

use Exception;
use Illuminate\Contracts\Debug\ShouldntReport;

class PodcastProcessingException extends Exception implements ShouldntReport
{
    //
}

예외의 스로우

abort() 헬퍼

애플리케이션의 어디에서든 HTTP 에러 응답을 발생시킬 수 있습니다.
// 404 Not Found
abort(404);

// 메시지 포함
abort(403, '이 조작을 할 권한이 없습니다.');

abort_if() / abort_unless()

조건부로 예외를 스로우하는 헬퍼입니다.
// $condition 이 true 일 때 abort
abort_if(! $user->isAdmin(), 403);

// $condition 이 false 일 때 abort
abort_unless($user->hasPermission('edit'), 403, 'Permission denied.');
컨트롤러나 미들웨어에서 권한 체크를 할 때 편리합니다. 게이트나 폴리시와 조합해서 사용되는 경우도 많습니다.

예외의 글로벌한 제어

특정 예외를 무시

보고하지 않을 예외를 dontReport로 지정합니다. 렌더링의 커스텀 로직은 계속 기능합니다.
use App\Exceptions\InvalidOrderException;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->dontReport([
        InvalidOrderException::class,
    ]);
})
조건부로 무시하고 싶은 경우 dontReportWhen에 클로저를 전달합니다.
use Throwable;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->dontReportWhen(function (Throwable $e) {
        return $e instanceof PodcastProcessingException &&
               $e->reason() === 'Subscription expired';
    });
})
Laravel은 기본적으로 404 에러나 CSRF 토큰 부정(419), 오리진 불일치(403) 등 일부 예외를 자동으로 무시하고 있습니다.

Laravel 이 무시하고 있는 예외를 유효화

기본으로 무시되고 있는 예외를 리포트 대상으로 되돌리려면 stopIgnoring을 사용합니다.
use Symfony\Component\HttpKernel\Exception\HttpException;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->stopIgnoring(HttpException::class);
})

HTTP 에러 페이지

Laravel에서는 HTTP 스테이터스 코드별로 커스텀 에러 뷰를 정의할 수 있습니다.

커스텀 에러 뷰의 작성

resources/views/errors/ 디렉터리에 스테이터스 코드를 파일명으로 한 Blade 템플릿을 작성합니다.
resources/
└── views/
    └── errors/
        ├── 404.blade.php
        ├── 403.blade.php
        └── 500.blade.php
뷰 내에서는 $exception 변수를 사용해 에러 정보에 접근할 수 있습니다.
{{-- resources/views/errors/404.blade.php --}}
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <title>페이지를 찾을 수 없습니다</title>
</head>
<body>
    <h1>404 - 페이지를 찾을 수 없습니다</h1>
    <p>{{ $exception->getMessage() }}</p>
    <a href="{{ url('/') }}">톱 페이지로 돌아가기</a>
</body>
</html>

기본 에러 템플릿을 공개

Laravel 표준의 에러 페이지를 커스터마이즈의 출발점으로서 사용하고 싶은 경우 vendor:publish로 취득합니다.
php artisan vendor:publish --tag=laravel-errors

폴백 에러 페이지

특정 스테이터스 코드에 대응하는 뷰가 없는 경우의 폴백으로서, 4xx.blade.php5xx.blade.php를 작성할 수 있습니다.
resources/
└── views/
    └── errors/
        ├── 4xx.blade.php  # 400번대의 폴백
        └── 5xx.blade.php  # 500번대의 폴백
404, 500, 503에 대해서는 Laravel이 기본 에러 페이지를 준비하고 있습니다. 이것들을 커스터마이즈하려면 폴백이 아닌 개별 파일(404.blade.php 등)을 작성해 주십시오.

실전 예시: API 예외 핸들러

API를 제공하는 애플리케이션에서는 예외를 항상 JSON으로 반환할 필요가 있습니다. 아래는 bootstrap/app.php에서 API 에러를 일원 관리하는 구현 예시입니다.
use Illuminate\Auth\AuthenticationException;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

->withExceptions(function (Exceptions $exceptions): void {
    // API 요청에는 항상 JSON 으로 반환
    $exceptions->render(function (NotFoundHttpException $e, Request $request) {
        if ($request->is('api/*')) {
            return response()->json([
                'message' => '리소스를 찾을 수 없습니다.',
            ], 404);
        }
    });

    $exceptions->render(function (AuthenticationException $e, Request $request) {
        if ($request->is('api/*')) {
            return response()->json([
                'message' => '인증이 필요합니다.',
            ], 401);
        }
    });

    $exceptions->render(function (ValidationException $e, Request $request) {
        if ($request->is('api/*')) {
            return response()->json([
                'message' => '밸리데이션 에러.',
                'errors'  => $e->errors(),
            ], 422);
        }
    });
})

커스텀 API 예외 클래스의 구현

API 전용의 기저 예외 클래스를 만들면, 각 엔드포인트에서 통일된 에러 응답을 반환할 수 있습니다.
<?php

namespace App\Exceptions;

use Exception;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class ApiException extends Exception
{
    public function __construct(
        string $message = 'An error occurred.',
        private readonly int $statusCode = 500,
        private readonly array $errors = [],
    ) {
        parent::__construct($message);
    }

    public function render(Request $request): JsonResponse
    {
        $data = ['message' => $this->getMessage()];

        if (! empty($this->errors)) {
            $data['errors'] = $this->errors;
        }

        return response()->json($data, $this->statusCode);
    }
}
컨트롤러에서의 사용 예시입니다.
<?php

namespace App\Http\Controllers\Api;

use App\Exceptions\ApiException;
use App\Models\Order;

class OrderController extends Controller
{
    public function show(int $id): JsonResponse
    {
        $order = Order::find($id);

        if (! $order) {
            throw new ApiException('주문을 찾을 수 없습니다.', 404);
        }

        if ($order->isCancelled()) {
            throw new ApiException('이 주문은 캔슬 완료입니다.', 422);
        }

        return response()->json($order);
    }
}

정리

방법용도
$exceptions->report()예외의 종류별로 커스텀 리포트 로직을 등록
$exceptions->context()모든 예외 로그에 공통 정보를 부여
context() 메서드예외 클래스 자신에 고유의 컨텍스트를 갖게 함
report() 헬퍼응답을 중단하지 않고 예외만 보고
dontReportDuplicates()같은 인스턴스의 중복 리포트를 방지
ShouldntReport 인터페이스일체 리포트하지 않는 예외 클래스를 작성
방법용도
$exceptions->render()예외의 종류별로 커스텀 응답을 반환
render() 메서드예외 클래스 자신에 렌더링 로직을 갖게 함
shouldRenderJsonWhen()JSON/HTML 의 판정 로직을 커스터마이즈
respond()생성된 응답을 추가로 가공
  • resources/views/errors/404.blade.php 등의 파일을 만들기만 하면 자동으로 사용됨
  • $exception 변수로 에러의 상세에 접근 가능
  • php artisan vendor:publish --tag=laravel-errors로 기본 템플릿을 취득 가능
  • 4xx.blade.php / 5xx.blade.php로 폴백 페이지를 정의 가능
  • APP_DEBUG=false를 반드시 설정하고, 스택 트레이스를 사용자에게 보여주지 않음
  • Sentry나 Flare 등의 외부 에러 추적 서비스와 연계해 에러를 일원 관리
  • throttle()을 사용해 대량의 예외가 발생했을 때의 로그 넘침을 방지
  • API 엔드포인트에서는 일관된 JSON 에러 응답 형식을 유지
마지막 수정일 2026년 7월 13일