Laravel 프로젝트를 작성하면 에러와 예외의 처리는 미리 설정된 상태로 준비되어 있습니다.
커스터마이즈는 bootstrap/app.php의 withExceptions 메서드로 수행합니다.
예외 처리 플로우
예외가 발생해서 클라이언트에 응답이 반환되기까지의 흐름을 나타냅니다.
// 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 환경 변수의 값이 사용됩니다.
# 로컬 개발
APP_DEBUG =true
# 운영 환경
APP_DEBUG =false
운영 환경에서는 APP_DEBUG를 반드시 false로 하십시오. true로 두면 기밀 정보가 최종 사용자에게 노출될 위험이 있습니다.
예외의 리포트
예외의 리포트는 예외를 로그에 기록하거나 Sentry 나 Flare 등의 외부 서비스로 전송하는 처리입니다.
기본적으로는 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에 설정을 쓰지 않아도 자동으로 호출됩니다.
예외 클래스의 작성
예외 클래스 작성
php artisan make:exception InvalidOrderException
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.php와 5xx.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 에러 응답 형식을 유지