> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# 에러 핸들링

> Laravel의 예외 처리 구조를 배웁니다. 예외의 리포트·렌더링·커스텀 에러 페이지·HTTP 에러 응답의 생성까지 포괄적으로 설명합니다.

## 개요

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

### 예외 처리 플로우

예외가 발생해서 클라이언트에 응답이 반환되기까지의 흐름을 나타냅니다.

```mermaid theme={null}
flowchart TD
    A["예외 발생"] --> B["ExceptionHandler::report"]
    B --> C{"로그 대상?"}
    C -->|"Yes"| D["로그 기록"]
    C -->|"No"| E["스킵"]
    D --> F["ExceptionHandler::render"]
    E --> F
    F --> G{"요청 유형"}
    G -->|"Web"| H["HTML 에러 페이지"]
    G -->|"API/JSON"| I["JSON 에러 응답"]
    H --> J["클라이언트에 반환"]
    I --> J
```

```php theme={null}
// 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.php`의 `debug` 옵션이 에러 정보의 표시 양을 제어합니다.
기본적으로는 `.env`의 `APP_DEBUG` 환경 변수의 값이 사용됩니다.

```ini theme={null}
# 로컬 개발
APP_DEBUG=true

# 운영 환경
APP_DEBUG=false
```

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

## 예외의 리포트

예외의 리포트는 예외를 로그에 기록하거나 [Sentry](https://github.com/getsentry/sentry-laravel)나 [Flare](https://flareapp.io) 등의 외부 서비스로 전송하는 처리입니다.
기본적으로는 `config/logging.php`의 설정에 기반해 로그에 기록됩니다.

### 커스텀 리포트 콜백

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

```php theme={null}
use App\Exceptions\InvalidOrderException;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->report(function (InvalidOrderException $e) {
        // 외부 서비스에의 통지 등
    });
})
```

커스텀 콜백을 등록해도 기본 로그 기록은 계속됩니다.
기본에의 전파를 멈추고 싶은 경우, `stop()`을 호출하거나 `false`를 반환합니다.

```php theme={null}
->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->report(function (InvalidOrderException $e) {
        // ...
    })->stop();
})
```

### `report()` 헬퍼

에러 페이지를 표시하지 않고 예외만 보고하고 싶은 경우 `report()` 헬퍼를 사용합니다.

```php theme={null}
public function isValid(string $value): bool
{
    try {
        // 밸리데이션 처리...
    } catch (Throwable $e) {
        report($e);

        return false;
    }
}
```

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

### 중복 리포트의 방지

같은 예외 인스턴스가 여러 번 `report()`에 전달되면, 로그에 중복 엔트리가 작성되는 경우가 있습니다.
`dontReportDuplicates()`를 설정하면 같은 인스턴스는 최초 1번만 기록됩니다.

```php theme={null}
->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->dontReportDuplicates();
})
```

```php theme={null}
$original = new RuntimeException('Whoops!');

report($original); // 기록됨

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

### 글로벌 로그 컨텍스트

모든 예외 로그에 공통 정보를 부여하고 싶은 경우 `context` 메서드를 사용합니다.
사용 가능하다면 현재 사용자 ID는 자동으로 부여됩니다.

```php theme={null}
->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->context(fn () => [
        'app_version' => config('app.version'),
    ]);
})
```

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

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

```php theme={null}
<?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` 메서드를 사용합니다.

```php theme={null}
use PDOException;
use Psr\Log\LogLevel;

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

### 예외 리포트의 스로틀링

대량의 예외가 발생하는 경우, `throttle` 메서드로 리포트 수를 제어할 수 있습니다.

```php theme={null}
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`을 사용합니다.

```php theme={null}
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` 메서드에 클로저를 전달해 예외를 응답으로 변환합니다.

```php theme={null}
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` 등)의 렌더링도 덮어쓸 수 있습니다.
클로저가 값을 반환하지 않는 경우, 기본 렌더링이 사용됩니다.

```php theme={null}
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`을 사용합니다.

```php theme={null}
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` 메서드를 사용하면 생성된 응답을 추가로 가공할 수 있습니다.

```php theme={null}
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`에 설정을 쓰지 않아도 자동으로 호출됩니다.

### 예외 클래스의 작성

<Steps>
  <Step title="예외 클래스 작성">
    ```shell theme={null}
    php artisan make:exception InvalidOrderException
    ```
  </Step>

  <Step title="report() 와 render() 구현">
    ```php theme={null}
    <?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);
        }
    }
    ```
  </Step>
</Steps>

<Info>
  `report()` 메서드에는 타입 힌트로 의존성 주입을 사용할 수 있습니다. Laravel의 서비스 컨테이너가 자동으로 해결합니다.
</Info>

### `ShouldntReport` 인터페이스

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

```php theme={null}
<?php

namespace App\Exceptions;

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

class PodcastProcessingException extends Exception implements ShouldntReport
{
    //
}
```

## 예외의 스로우

### `abort()` 헬퍼

애플리케이션의 어디에서든 HTTP 에러 응답을 발생시킬 수 있습니다.

```mermaid theme={null}
flowchart LR
    A["abort(404)"] --> B["NotFoundHttpException"]
    C["abort(403)"] --> D["AccessDeniedHttpException"]
    E["abort(500)"] --> F["HttpException<br>500"]
    G["abort(422)"] --> H["UnprocessableEntityHttpException"]
    B --> I["resources/views/errors/404.blade.php"]
    D --> J["resources/views/errors/403.blade.php"]
    F --> K["resources/views/errors/500.blade.php"]
    H --> L["resources/views/errors/422.blade.php"]
```

```php theme={null}
// 404 Not Found
abort(404);

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

### `abort_if()` / `abort_unless()`

조건부로 예외를 스로우하는 헬퍼입니다.

```php theme={null}
// $condition 이 true 일 때 abort
abort_if(! $user->isAdmin(), 403);

// $condition 이 false 일 때 abort
abort_unless($user->hasPermission('edit'), 403, 'Permission denied.');
```

<Tip>
  컨트롤러나 미들웨어에서 권한 체크를 할 때 편리합니다. 게이트나 폴리시와 조합해서 사용되는 경우도 많습니다.
</Tip>

## 예외의 글로벌한 제어

### 특정 예외를 무시

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

```php theme={null}
use App\Exceptions\InvalidOrderException;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->dontReport([
        InvalidOrderException::class,
    ]);
})
```

조건부로 무시하고 싶은 경우 `dontReportWhen`에 클로저를 전달합니다.

```php theme={null}
use Throwable;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->dontReportWhen(function (Throwable $e) {
        return $e instanceof PodcastProcessingException &&
               $e->reason() === 'Subscription expired';
    });
})
```

<Info>
  Laravel은 기본적으로 404 에러나 CSRF 토큰 부정(419), 오리진 불일치(403) 등 일부 예외를 자동으로 무시하고 있습니다.
</Info>

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

기본으로 무시되고 있는 예외를 리포트 대상으로 되돌리려면 `stopIgnoring`을 사용합니다.

```php theme={null}
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` 변수를 사용해 에러 정보에 접근할 수 있습니다.

```blade theme={null}
{{-- 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`로 취득합니다.

```shell theme={null}
php artisan vendor:publish --tag=laravel-errors
```

### 폴백 에러 페이지

특정 스테이터스 코드에 대응하는 뷰가 없는 경우의 폴백으로서, `4xx.blade.php`와 `5xx.blade.php`를 작성할 수 있습니다.

```
resources/
└── views/
    └── errors/
        ├── 4xx.blade.php  # 400번대의 폴백
        └── 5xx.blade.php  # 500번대의 폴백
```

<Warning>
  `404`, `500`, `503`에 대해서는 Laravel이 기본 에러 페이지를 준비하고 있습니다. 이것들을 커스터마이즈하려면 폴백이 아닌 개별 파일(`404.blade.php` 등)을 작성해 주십시오.
</Warning>

## 실전 예시: API 예외 핸들러

API를 제공하는 애플리케이션에서는 예외를 항상 JSON으로 반환할 필요가 있습니다.
아래는 `bootstrap/app.php`에서 API 에러를 일원 관리하는 구현 예시입니다.

```php theme={null}
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 theme={null}
<?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 theme={null}
<?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);
    }
}
```

## 정리

<AccordionGroup>
  <Accordion title="예외 리포트의 정리">
    | 방법                       | 용도                        |
    | ------------------------ | ------------------------- |
    | `$exceptions->report()`  | 예외의 종류별로 커스텀 리포트 로직을 등록   |
    | `$exceptions->context()` | 모든 예외 로그에 공통 정보를 부여       |
    | `context()` 메서드          | 예외 클래스 자신에 고유의 컨텍스트를 갖게 함 |
    | `report()` 헬퍼            | 응답을 중단하지 않고 예외만 보고        |
    | `dontReportDuplicates()` | 같은 인스턴스의 중복 리포트를 방지       |
    | `ShouldntReport` 인터페이스   | 일체 리포트하지 않는 예외 클래스를 작성    |
  </Accordion>

  <Accordion title="예외 렌더링의 정리">
    | 방법                       | 용도                        |
    | ------------------------ | ------------------------- |
    | `$exceptions->render()`  | 예외의 종류별로 커스텀 응답을 반환       |
    | `render()` 메서드           | 예외 클래스 자신에 렌더링 로직을 갖게 함   |
    | `shouldRenderJsonWhen()` | JSON/HTML 의 판정 로직을 커스터마이즈 |
    | `respond()`              | 생성된 응답을 추가로 가공            |
  </Accordion>

  <Accordion title="HTTP 에러 페이지의 정리">
    * `resources/views/errors/404.blade.php` 등의 파일을 만들기만 하면 자동으로 사용됨
    * `$exception` 변수로 에러의 상세에 접근 가능
    * `php artisan vendor:publish --tag=laravel-errors`로 기본 템플릿을 취득 가능
    * `4xx.blade.php` / `5xx.blade.php`로 폴백 페이지를 정의 가능
  </Accordion>

  <Accordion title="운영 환경에서의 모범 사례">
    * `APP_DEBUG=false`를 반드시 설정하고, 스택 트레이스를 사용자에게 보여주지 않음
    * Sentry나 Flare 등의 외부 에러 추적 서비스와 연계해 에러를 일원 관리
    * `throttle()`을 사용해 대량의 예외가 발생했을 때의 로그 넘침을 방지
    * API 엔드포인트에서는 일관된 JSON 에러 응답 형식을 유지
  </Accordion>
</AccordionGroup>


## Related topics

- [Laravel 11 이후의 애플리케이션 구조](/ko/advanced/app-structure.md)
- [Laravel Pulse](/ko/pulse.md)
- [Laravel Pennant](/ko/pennant.md)
- [HTTP 테스트](/ko/http-tests.md)
- [구 구조에서 새 구조로의 이관 가이드](/ko/advanced/app-structure-migration.md)
