Laravel 10 이전과의 비교
Laravel 11에서는 “Slim Application Skeleton”으로서, 애플리케이션 구조가 대폭 쇄신되었습니다. 최대의 변화는, 설정의 분산을 없애고 bootstrap/app.php 한 곳으로 집약한 것입니다.
항목 Laravel 10 이전 Laravel 11 이후 HTTP 커널 app/Http/Kernel.php폐지(프레임워크 내에 통합) 콘솔 커널 app/Console/Kernel.php폐지(routes/console.php로 이관) 예외 핸들러 app/Exceptions/Handler.php폐지(bootstrap/app.php에 집약) 서비스 프로바이더 5파일 AppServiceProvider.php 1파일라우트 파일 web.php / api.php가 기본web.php만 기본, api.php는 옵트인부트스트랩 설정이 여러 파일에 분산 bootstrap/app.php에 집약기본 DB MySQL/PostgreSQL SQLite
이 변경은 신규 프로젝트용 입니다. 기존의 Laravel 10 애플리케이션을 업그레이드해도, 오래된 구조는 그대로 동작합니다.
새로운 디렉토리·파일 구성
스켈레톤의 디렉토리 구성
laravel-app/
├── app/
│ ├── Http/
│ │ └── Controllers/
│ ├── Models/
│ │ └── User.php
│ └── Providers/
│ └── AppServiceProvider.php
├── bootstrap/
│ ├── app.php ← アプリケーション設定の中心
│ ├── cache/
│ └── providers.php ← サービスプロバイダー一覧
├── config/
├── database/
├── public/
│ └── index.php ← エントリーポイント
├── resources/
├── routes/
│ ├── web.php ← Webルート(デフォルト)
│ └── console.php ← Artisanコマンドとスケジュール
├── storage/
└── tests/
bootstrap/app.php — 앱 설정의 중심
<? php
use Illuminate\Foundation\ Application ;
use Illuminate\Foundation\Configuration\ Exceptions ;
use Illuminate\Foundation\Configuration\ Middleware ;
return Application :: configure ( basePath : dirname ( __DIR__ ))
-> withRouting (
web : __DIR__ . '/../routes/web.php' ,
commands : __DIR__ . '/../routes/console.php' ,
health : '/up' ,
)
-> withMiddleware ( function ( Middleware $middleware ) : void {
//
})
-> withExceptions ( function ( Exceptions $exceptions ) : void {
//
}) -> create ();
이 파일 하나로, 라우팅·미들웨어·예외 핸들링을 설정할 수 있습니다. Laravel 10 이전에서는 app/Http/Kernel.php·app/Console/Kernel.php·app/Exceptions/Handler.php의 3파일에 분산되어 있던 설정이, 여기에 집약되어 있습니다.
bootstrap/providers.php — 서비스 프로바이더 목록
<? php
use App\Providers\ AppServiceProvider ;
return [
AppServiceProvider :: class ,
];
이 파일은 서비스 프로바이더의 등록처입니다. Laravel 10에서는 config/app.php의 providers 배열에 기술하고 있었지만, bootstrap/providers.php로 분리되었습니다. Laravel 11의 기본으로는 AppServiceProvider만입니다.
composer require로 패키지를 설치하면, 그 패키지가 bootstrap/providers.php를 자동 갱신하는 경우가 있습니다. config/app.php는 참조되지 않게 된 것은 아니지만, 신규 등록은 bootstrap/providers.php가 권장 장소가 되었습니다.
routes/ 디렉토리의 변경
routes/
├── web.php ← Webルート(デフォルトで読み込まれる)
└── console.php ← Artisanコマンドとスケジュールを定義する
api.php와 channels.php는 기본으로는 존재하지 않습니다. 필요에 따라 Artisan 커맨드로 생성합니다.
# APIルートを追加する(api.php + Sanctumをインストール)
php artisan install:api
# ブロードキャストを追加する(channels.php + Reverb などをインストール)
php artisan install:broadcasting
routes/console.php에서는 스케줄도 정의할 수 있습니다.
<? php
use Illuminate\Foundation\ Inspiring ;
use Illuminate\Support\Facades\ Artisan ;
use Illuminate\Support\Facades\ Schedule ;
Artisan :: command ( 'inspire' , function () {
$this -> comment ( Inspiring :: quote ());
}) -> purpose ( 'Display an inspiring quote' );
Schedule :: command ( 'emails:send' ) -> daily ();
폐지된 파일
HTTP 커널은 프레임워크 내의 Illuminate\Foundation\Http\Kernel에 통합되었습니다. 미들웨어의 커스터마이즈는 bootstrap/app.php의 withMiddleware()에서 합니다. // Laravel 10 以前: app/Http/Kernel.php
protected $middleware = [
\Illuminate\Http\Middleware\ TrustProxies :: class ,
// ...
];
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\ EncryptCookies :: class ,
// ...
],
];
// Laravel 11 以降: bootstrap/app.php
-> withMiddleware ( function ( Middleware $middleware ) {
$middleware -> web ( append : [
EnsureUserIsSubscribed :: class ,
]);
$middleware -> validateCsrfTokens ( except : [ 'stripe/*' ]);
})
app/Console/Kernel.php의 폐지
콘솔 커널의 2개의 역할이 분리되었습니다. Artisan 커맨드는 app/Console/Commands/에 두어 자동 검출되고, 스케줄은 routes/console.php에 기술합니다. // Laravel 10 以前: app/Console/Kernel.php
protected function schedule ( Schedule $schedule ) : void
{
$schedule -> command ( 'emails:send' ) -> daily ();
}
// Laravel 11 以降: routes/console.php
use Illuminate\Support\Facades\ Schedule ;
Schedule :: command ( 'emails:send' ) -> daily ();
app/Exceptions/Handler.php의 폐지
예외 핸들러는 프레임워크 내의 Illuminate\Foundation\Exceptions\Handler에 통합되었습니다. 커스터마이즈는 bootstrap/app.php의 withExceptions()에서 합니다. // Laravel 10 以前: app/Exceptions/Handler.php
public function register () : void
{
$this -> reportable ( function ( InvalidOrderException $e ) {
// ...
});
}
// Laravel 11 以降: bootstrap/app.php
-> withExceptions ( function ( Exceptions $exceptions ) {
$exceptions -> report ( function ( InvalidOrderException $e ) {
// ...
});
})
프레임워크 내부에서의 구현
Application::configure()는 Illuminate\Foundation\Application의 정적 메서드입니다.
// Illuminate\Foundation\Application より
public static function configure ( ? string $basePath = null )
{
$basePath = match ( true ) {
is_string ( $basePath ) => $basePath ,
default => static :: inferBasePath (),
};
return ( new Configuration\ ApplicationBuilder ( new static ( $basePath )))
-> withKernels ()
-> withEvents ()
-> withCommands ()
-> withProviders ();
}
이 메서드는 다음의 처리를 합니다.
basePath에서 애플리케이션의 루트 디렉토리를 결정한다
Application 인스턴스를 생성한다
ApplicationBuilder로 래핑하고, 기본 설정을 적용한다
ApplicationBuilder 인스턴스를 반환한다
중요한 것은, configure() 내에서 이미 withKernels() / withEvents() / withCommands() / withProviders()가 호출되고 있다 는 점입니다. bootstrap/app.php에서 이것들을 다시 호출할 필요는 없습니다.
메서드 체인으로 설정을 하는 흐름
Application :: configure ( basePath : dirname ( __DIR__ )) // ApplicationBuilder を返す
-> withRouting ( ... ) // ルーティングを設定し $this を返す
-> withMiddleware ( ... ) // ミドルウェアを設定し $this を返す
-> withExceptions ( ... ) // 例外ハンドリングを設定し $this を返す
-> create (); // Application インスタンスを返す
create()의 호출로 ApplicationBuilder에서 Application 인스턴스가 꺼내지고, bootstrap/app.php가 return하는 것은 이 Application 인스턴스입니다.
요청에서 앱 기동까지의 흐름
public/index.php가 진입점이 되어, bootstrap/app.php를 로드하여 Application을 취득합니다. 그 후, HTTP Kernel이 요청을 미들웨어의 파이프라인에 통과시키고, 라우터가 컨트롤러에 디스패치합니다.
ApplicationBuilder의 주요 메서드 심층 분석
withRouting() — 라우팅 등록의 내부 처리
public function withRouting (
? Closure $using = null ,
array | string | null $web = null ,
array | string | null $api = null ,
? string $commands = null ,
? string $channels = null ,
? string $pages = null ,
? string $health = null ,
string $apiPrefix = 'api' ,
? callable $then = null
)
내부에서는 AppRouteServiceProvider::loadRoutesUsing()에 콜백을 등록하고, 애플리케이션의 booting 시에 AppRouteServiceProvider를 등록합니다.
// withRouting() の内部処理(簡略版)
protected function buildRoutingCallback (...)
{
return function () use ( $web , $api , $pages , $health , $apiPrefix , $then ) {
if ( is_string ( $api ) || is_array ( $api )) {
Route :: middleware ( 'api' ) -> prefix ( $apiPrefix ) -> group ( $api );
}
if ( is_string ( $health )) {
Route :: get ( $health , function () {
Event :: dispatch ( new DiagnosingHealth );
return response ( View :: file ( ... ), status : 200 );
});
}
if ( is_string ( $web ) || is_array ( $web )) {
Route :: middleware ( 'web' ) -> group ( $web );
}
if ( is_callable ( $then )) {
$then ( $this -> app );
}
};
}
포인트:
api 라우트는 api 미들웨어 그룹과 /api 프리픽스가 자동적으로 적용된다
health에 문자열을 전달하면 헬스 체크 엔드포인트(기본 /up)가 자동 등록된다
health의 경로는 유지보수 모드 중에도 제외된다(PreventRequestsDuringMaintenance::except()로 설정)
web보다 먼저 api가 등록되는 것에 주의. 같은 경로에 대해 Web과 API의 라우트를 정의하면, API 라우트가 우선된다
pages에 문자열을 전달하면 Laravel Folio 의 라우팅이 활성화된다
withMiddleware() — 미들웨어 커스터마이즈
public function withMiddleware ( ? callable $callback = null )
{
$this -> app -> afterResolving ( HttpKernel :: class , function ( $kernel ) use ( $callback ) {
$middleware = ( new Middleware )
-> redirectGuestsTo ( fn () => route ( 'login' ));
if ( ! is_null ( $callback )) {
$callback ( $middleware );
}
$kernel -> setGlobalMiddleware ( $middleware -> getGlobalMiddleware ());
$kernel -> setMiddlewareGroups ( $middleware -> getMiddlewareGroups ());
$kernel -> setMiddlewareAliases ( $middleware -> getMiddlewareAliases ());
// ...
});
return $this ;
}
withMiddleware()는 HttpKernel이 해결된 후 에 콜백을 실행합니다. 이것은 afterResolving() 훅을 사용하고 있기 때문입니다. 콜백에 전달되는 Middleware 오브젝트에는 풍부한 커스터마이즈 메서드가 있습니다.
-> withMiddleware ( function ( Middleware $middleware ) {
// グローバルミドルウェアを追加
$middleware -> append ( MyGlobalMiddleware :: class );
// web グループにミドルウェアを追加
$middleware -> web ( append : [ EnsureUserIsSubscribed :: class ]);
// api グループのミドルウェアを置き換え
$middleware -> api ( replace : [
OldMiddleware :: class => NewMiddleware :: class ,
]);
// CSRF 除外パスを設定
$middleware -> validateCsrfTokens ( except : [ 'stripe/*' , 'webhook/*' ]);
// 未認証ユーザーのリダイレクト先を変更
$middleware -> redirectGuestsTo ( '/custom-login' );
// ミドルウェアの優先順位を設定
$middleware -> priority ([
\Illuminate\Session\Middleware\ StartSession :: class ,
\Illuminate\View\Middleware\ ShareErrorsFromSession :: class ,
]);
})
withExceptions() — 예외 핸들링의 설정
public function withExceptions ( ? callable $using = null )
{
$this -> app -> singleton (
\Illuminate\Contracts\Debug\ ExceptionHandler :: class ,
\Illuminate\Foundation\Exceptions\ Handler :: class
);
if ( $using !== null ) {
$this -> app -> afterResolving (
\Illuminate\Foundation\Exceptions\ Handler :: class ,
fn ( $handler ) => $using ( new Exceptions ( $handler )),
);
}
return $this ;
}
withExceptions()는 프레임워크의 Handler 클래스를 싱글톤으로서 등록한 뒤, 콜백을 afterResolving()으로 설정합니다. 콜백에는 Exceptions 래퍼 오브젝트가 전달됩니다.
-> withExceptions ( function ( Exceptions $exceptions ) {
// 特定の例外をレポートしない
$exceptions -> dontReport ( MissedFlightException :: class );
// 特定の例外をカスタムレポート
$exceptions -> report ( function ( InvalidOrderException $e ) {
// Slack に通知など
});
// 特定の例外のHTTPレスポンスをカスタマイズ
$exceptions -> render ( function ( NotFoundHttpException $e , Request $request ) {
if ( $request -> is ( 'api/*' )) {
return response () -> json ([ 'message' => 'Not Found' ], 404 );
}
});
// スロットリング(同じ例外を連続でレポートしない)
$exceptions -> throttle ( function ( Throwable $e ) {
return Limit :: perMinute ( 20 );
});
})
withProviders() — 서비스 프로바이더 등록
public function withProviders ( array $providers = [], bool $withBootstrapProviders = true )
{
RegisterProviders :: merge (
$providers ,
$withBootstrapProviders
? $this -> app -> getBootstrapProvidersPath ()
: null
);
return $this ;
}
withProviders()는 Application::configure() 내에서 기본 호출되므로, bootstrap/providers.php는 자동적으로 로드됩니다. 추가 프로바이더를 전달하고 싶은 경우에는 bootstrap/app.php에서 명시적으로 호출할 필요가 있습니다.
Application :: configure ( basePath : dirname ( __DIR__ ))
-> withProviders ([
// デフォルトのbootstrap/providers.phpに加えてプロバイダーを追加する
App\Providers\ CustomServiceProvider :: class ,
])
-> withRouting ( ... )
-> create ();
withBootstrapProviders: false를 전달하면 bootstrap/providers.php가 로드되지 않게 됩니다. 특별한 이유가 없는 한 생략해 주세요.
기타의 주요 메서드
메서드 설명 withKernels()HTTP / Console 커널을 싱글톤 등록한다. configure()가 자동 호출 withEvents()이벤트 디스커버리를 활성화한다. configure()가 자동 호출 withCommands(array $commands)Artisan 커맨드 클래스 또는 디렉토리를 추가 등록한다 withSchedule(callable $callback)스케줄을 bootstrap/app.php에서 정의한다 withBroadcasting(string $channels)브로드캐스트 채널 파일을 등록한다 withBindings(array $bindings)컨테이너 바인딩을 등록한다 withSingletons(array $singletons)싱글톤 바인딩을 등록한다 registered(callable $callback)서비스 프로바이더 등록 후에 실행되는 콜백을 추가한다 booting(callable $callback)booting 시에 실행되는 콜백을 추가한다 booted(callable $callback)booted 시에 실행되는 콜백을 추가한다 create()Application 인스턴스를 반환한다
설계 의도: 왜 이렇게 되어 있는가
”코드 퍼스트”의 설정
Laravel 10 이전의 app/Http/Kernel.php에는 배열로 미들웨어를 열거하는 스타일이 사용되고 있었습니다. 이것은 설정 파일에 가까운 쓰는 법으로, PHP의 타입 시스템이나 IDE의 서포트를 얻기 어렵다는 결점이 있었습니다.
Laravel 11에서는 withMiddleware(function (Middleware $middleware) { ... })라는 콜백 스타일로 바뀌었습니다. 이에 의해 타입 자동 완성이 통하고, 조건 분기나 루프 등의 로직을 사용한 동적인 설정이 자연스럽게 쓸 수 있게 됩니다.
”규약보다 설정”에서 “명시적인 설정”으로
api.php를 옵트인으로 한 것은, API 라우트를 사용하지 않는 애플리케이션에서도 api 미들웨어 그룹이 항상 로드되어 있던 것을 해소하기 위함입니다. 사용하지 않는 기능은 기본으로 존재하지 않는다, 는 방침입니다.
afterResolving() 훅의 활용
withMiddleware()나 withExceptions()에서 afterResolving()을 사용하고 있는 것은, 설정의 순서 문제를 피하기 위함입니다. ApplicationBuilder의 메서드는 애플리케이션이 완전히 기동하기 전에 호출되지만, 실제의 처리(커널에의 설정 적용)는 커널이 최초에 해결되었을 때 지연 실행됩니다.
커스터마이즈의 실전 예
API와 Web을 공존시킨다
return Application :: configure ( basePath : dirname ( __DIR__ ))
-> withRouting (
web : __DIR__ . '/../routes/web.php' ,
api : __DIR__ . '/../routes/api.php' ,
commands : __DIR__ . '/../routes/console.php' ,
health : '/up' ,
apiPrefix : 'api/v1' , // デフォルトの /api から変更
)
-> withMiddleware ( function ( Middleware $middleware ) : void {
//
})
-> withExceptions ( function ( Exceptions $exceptions ) : void {
//
}) -> create ();
미들웨어의 커스터마이즈
-> withMiddleware ( function ( Middleware $middleware ) {
// Webルートに認証チェックミドルウェアを追加
$middleware -> web ( append : [
\App\Http\Middleware\ EnsureEmailIsVerified :: class ,
]);
// APIルートで特定のミドルウェアを除外
$middleware -> api ( remove : [
\Illuminate\Session\Middleware\ StartSession :: class ,
]);
// Webhook エンドポイントを CSRF から除外
$middleware -> validateCsrfTokens ( except : [
'webhook/*' ,
'stripe/webhook' ,
]);
// 特定のミドルウェアにエイリアスを設定
$middleware -> alias ([
'subscribed' => \App\Http\Middleware\ EnsureUserIsSubscribed :: class ,
]);
})
스케줄을 bootstrap/app.php에 집약한다
스케줄은 routes/console.php에 쓸 수도 있지만, withSchedule()을 사용하면 bootstrap/app.php로 정리할 수 있습니다.
use Illuminate\Console\Scheduling\ Schedule ;
return Application :: configure ( basePath : dirname ( __DIR__ ))
-> withRouting (
web : __DIR__ . '/../routes/web.php' ,
commands : __DIR__ . '/../routes/console.php' ,
health : '/up' ,
)
-> withSchedule ( function ( Schedule $schedule ) {
$schedule -> command ( 'emails:send' ) -> daily ();
$schedule -> command ( 'reports:generate' ) -> weeklyOn ( 1 , '8:00' );
$schedule -> job ( new PruneOldRecords ) -> daily ();
})
-> withMiddleware ( function ( Middleware $middleware ) : void {
//
})
-> withExceptions ( function ( Exceptions $exceptions ) : void {
//
}) -> create ();
예외 핸들링의 커스터마이즈
-> withExceptions ( function ( Exceptions $exceptions ) {
// APIリクエストには常にJSONで返す
$exceptions -> render ( function ( Throwable $e , Request $request ) {
if ( $request -> is ( 'api/*' ) || $request -> wantsJson ()) {
$status = match ( true ) {
$e instanceof NotFoundHttpException => 404 ,
$e instanceof AuthenticationException => 401 ,
$e instanceof AuthorizationException => 403 ,
$e instanceof ValidationException => 422 ,
default => 500 ,
};
return response () -> json ([
'message' => $e -> getMessage (),
], $status );
}
});
// 本番環境のみSlackに通知
if ( app () -> isProduction ()) {
$exceptions -> report ( function ( Throwable $e ) {
app ( SlackNotifier :: class ) -> notify ( $e );
}) -> stop ();
}
})
컨테이너 바인딩을 bootstrap/app.php에서 관리한다
소규모의 애플리케이션이라면, 심플한 바인딩을 AppServiceProvider가 아닌 bootstrap/app.php에 쓸 수도 있습니다.
return Application :: configure ( basePath : dirname ( __DIR__ ))
-> withRouting ( ... )
-> withSingletons ([
\App\Contracts\ PaymentGateway :: class => \App\Services\ StripeGateway :: class ,
\App\Contracts\ MailService :: class => \App\Services\ SendgridMailService :: class ,
])
-> withMiddleware ( ... )
-> withExceptions ( ... )
-> create ();
Laravel의 기동 처리의 순번
Laravel 애플리케이션의 기동 시, 서비스 프로바이더와 Application의 훅은 이하의 순번으로 실행됩니다.
모든 ServiceProvider의 register() 실행
Application의 registered()
Application의 booting()
모든 ServiceProvider의 boot() 실행
Application의 booted()
AppServiceProvider에 다음 코드를 추가하면 실행 순을 확인할 수 있습니다.
class AppServiceProvider extends ServiceProvider
{
public function register () : void
{
info ( '1. AppServiceProvider register' );
$this -> booting ( function () {
info ( '4. AppServiceProvider booting' );
});
$this -> booted ( function () {
info ( '5. AppServiceProvider booted' );
});
$this -> app -> registered ( function () {
info ( '2. app registered' );
});
$this -> app -> booting ( function () {
info ( '3. app booting' );
});
$this -> app -> booted ( function () {
info ( '6. AppServiceProvider@register app booted' );
});
}
public function boot () : void
{
$this -> app -> booted ( function () {
info ( '7. AppServiceProvider@boot app booted' );
});
}
}
ApplicationBuilder의 registered(), booting(), booted()는 Application에 콜백을 등록할 뿐인 메서드입니다. 일반적으로는 사용할 필요가 없지만, 기동 준비가 완료된 Kernel에 대해 booted()에서 변경을 가하는 등의 특수한 조작이 가능합니다.
// bootstrap/app.php
use Illuminate\Contracts\Http\ Kernel ;
return Application :: configure ( basePath : dirname ( __DIR__ ))
-> withRouting ( ... )
-> withMiddleware ( ... )
-> withExceptions ( ... )
-> booted ( function ( Application $app ) {
$kernel = $app -> make ( Kernel :: class );
//
$app -> instance ( Kernel :: class , $kernel );
})
-> create ();
다음 단계
서비스 컨테이너 ApplicationBuilder가 내부에서 사용하고 있는 서비스 컨테이너의 구조를 이해합니다.
새 앱 구조 FAQ 새로운 애플리케이션 구조에 관한 자주 있는 의문과 답변을 정리하고 있습니다.