> ## 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 10의 구 앱 구조에서 Laravel 11 이후의 Slim Application Skeleton으로 이관하는 절차를 해설합니다.

## 시작하며

이 가이드에서는, Laravel 10 이전의 구 애플리케이션 구조(`Kernel` 클래스나 여러 서비스 프로바이더를 가진 구조)에서, Laravel 11 이후의 Slim Application Skeleton으로 이관하는 절차를 해설합니다.

<Warning>
  **공식 문서는 이 이관을 권장하고 있지 않습니다.**

  * Laravel 10의 구 구조는 Laravel 11 이후에서도 **그대로 동작**합니다. Laravel 13을 포함한 현재 버전에 이르기까지, 서포트 종료의 예정은 없습니다.
  * 이관은 **완전히 임의**입니다. 강제도 권장도 아닙니다.
  * 구 구조와 새 구조의 차이를 깊이 이해하고 있지 않은 경우에는, 이관을 피하는 것을 강하게 권합니다. 프레임워크 내부에 정통한 개발자용의 작업입니다.
  * 이관 전에 **반드시 백업을 하고**, 테스트가 모두 통과되는지를 확인해 주세요.
</Warning>

## 이관이 필요해지는 장면

이하와 같은 경우에 이관을 검토할 수 있습니다.

* 신규 참가 멤버가 공식 문서와 대조하기 쉽도록, 프로젝트를 최신의 표준 구조에 맞추고 싶음
* Laravel 11 이후에서 신규 작성한 패키지나 스타터 킷과의 정합성을 유지하고 싶음
* 구 구조 유래의 설정 파일이나 클래스를 줄이고, 코드베이스를 심플하게 하고 싶음

## 전제 조건

이 가이드에서는, 이하의 상태를 전제로 합니다.

* **Laravel 버전 업그레이드가 완료되어 있음**(`laravel/framework ^11.0` 이후)
* 기존의 테스트가 모두 통과하고 있음
* Laravel 11 이후의 애플리케이션 구조([Laravel 11 이후의 애플리케이션 구조](/ko/advanced/app-structure)를 참조)를 이해하고 있음

## 이관 예

Laravel 10 + Breeze(Blade 스택)로 작성한 프로젝트를 Breeze 그대로 유지하면서, 애플리케이션 구조만을 새 구조로 이관하는 절차를 보입니다.

<Steps>
  <Step title="bootstrap/app.php를 치환한다">
    구 `bootstrap/app.php`는 `$app` 인스턴스를 생성하여 커널을 등록하는 형식이었습니다. 이것을 `Application::configure()` 체인으로 치환합니다.

    **구(Laravel 10):**

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

    $app = new Illuminate\Foundation\Application(
        $_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
    );

    $app->singleton(
        Illuminate\Contracts\Http\Kernel::class,
        App\Http\Kernel::class
    );

    $app->singleton(
        Illuminate\Contracts\Console\Kernel::class,
        App\Console\Kernel::class
    );

    $app->singleton(
        Illuminate\Contracts\Debug\ExceptionHandler::class,
        App\Exceptions\Handler::class
    );

    return $app;
    ```

    **신(Laravel 11 이후):**

    ```php theme={null}
    <?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) {
            //
        })
        ->withExceptions(function (Exceptions $exceptions) {
            //
        })->create();
    ```

    <Info>
      `withMiddleware()`와 `withExceptions()`의 콜백은, 다음 스텝에서 삭제하는 커널 파일의 설정을 이식하는 장소입니다. 우선은 비운 채로 하고, 후속 스텝에서 추가 기재합니다.
    </Info>
  </Step>

  <Step title="HTTP 커널(app/Http/Kernel.php)을 삭제한다">
    `app/Http/Kernel.php`에는 글로벌 미들웨어, 미들웨어 그룹, 미들웨어 별칭이 정의되어 있었습니다.

    **구(app/Http/Kernel.php):**

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

    namespace App\Http;

    use Illuminate\Foundation\Http\Kernel as HttpKernel;

    class Kernel extends HttpKernel
    {
        protected $middleware = [
            \App\Http\Middleware\TrustProxies::class,
            \Illuminate\Http\Middleware\HandleCors::class,
            \App\Http\Middleware\PreventRequestsDuringMaintenance::class,
            \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
            \App\Http\Middleware\TrimStrings::class,
            \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
        ];

        protected $middlewareGroups = [
            'web' => [
                \App\Http\Middleware\EncryptCookies::class,
                \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
                \Illuminate\Session\Middleware\StartSession::class,
                \Illuminate\View\Middleware\ShareErrorsFromSession::class,
                \App\Http\Middleware\VerifyCsrfToken::class,
                \Illuminate\Routing\Middleware\SubstituteBindings::class,
            ],
            'api' => [
                \Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
                \Illuminate\Routing\Middleware\SubstituteBindings::class,
            ],
        ];

        protected $middlewareAliases = [
            'auth' => \App\Http\Middleware\Authenticate::class,
            // ...
        ];
    }
    ```

    Laravel 11에서는, 위의 미들웨어는 프레임워크 내부에 기본값으로서 짜넣어져 있습니다. 커스터마이즈가 **없는** 경우에는, `app/Http/Kernel.php`를 그대로 삭제할 수 있습니다.

    **커스터마이즈가 있는 경우**(독자 미들웨어를 추가·제외하고 있는 경우)에는, `bootstrap/app.php`의 `withMiddleware()`로 이식한 뒤 삭제해 주세요.

    ```php theme={null}
    ->withMiddleware(function (Middleware $middleware) {
        // グローバルミドルウェアの追加
        $middleware->append(\App\Http\Middleware\MyCustomMiddleware::class);

        // web グループにミドルウェアを追加
        $middleware->web(append: [
            \App\Http\Middleware\HandleInertiaRequests::class,
        ]);

        // ミドルウェアエイリアスの追加
        $middleware->alias([
            'role' => \App\Http\Middleware\CheckRole::class,
        ]);
    })
    ```

    이식이 완료되면 `app/Http/Kernel.php`를 삭제합니다.

    <Info>
      Laravel 11에서는 `TrustProxies`, `EncryptCookies`, `VerifyCsrfToken` 등의 기본 미들웨어 클래스도 `app/Http/Middleware/`에서 삭제할 수 있습니다. 프레임워크 측에 내장되어 있으므로, 커스터마이즈가 불필요하다면 파일 자체가 불필요해집니다.
    </Info>
  </Step>

  <Step title="콘솔 커널(app/Console/Kernel.php)을 삭제한다">
    `app/Console/Kernel.php`는 스케줄의 정의와 커맨드의 자동 로드를 담당했습니다.

    **구(app/Console/Kernel.php):**

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

    namespace App\Console;

    use Illuminate\Console\Scheduling\Schedule;
    use Illuminate\Foundation\Console\Kernel as ConsoleKernel;

    class Kernel extends ConsoleKernel
    {
        protected function schedule(Schedule $schedule): void
        {
            // $schedule->command('inspire')->hourly();
        }

        protected function commands(): void
        {
            $this->load(__DIR__.'/Commands');
            require base_path('routes/console.php');
        }
    }
    ```

    **커맨드의 자동 로드**는 Laravel 11 이후에서는 `app/Console/Commands/` 디렉토리가 자동적으로 스캔되므로, `$this->load()`의 기술은 불필요해졌습니다.

    **스케줄의 정의**는 `routes/console.php` 또는 `bootstrap/app.php`의 `withSchedule()`로 이관합니다.

    ```php theme={null}
    // routes/console.php（Laravel 11 以降）
    use Illuminate\Support\Facades\Schedule;

    Schedule::command('emails:send')->daily();
    ```

    이관 후에 `app/Console/Kernel.php`를 삭제합니다.

    <Info>
      `app/Console/` 디렉토리 내의 커스텀 Artisan 커맨드 파일은 삭제하지 마세요. 커맨드 파일은 그대로 남기고, 커널 클래스의 파일만을 삭제합니다.
    </Info>
  </Step>

  <Step title="예외 핸들러(app/Exceptions/Handler.php)를 삭제한다">
    `app/Exceptions/Handler.php`는 예외 리포트와 렌더링의 설정을 담당했습니다.

    **구(app/Exceptions/Handler.php):**

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

    namespace App\Exceptions;

    use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
    use Throwable;

    class Handler extends ExceptionHandler
    {
        protected $dontFlash = [
            'current_password',
            'password',
            'password_confirmation',
        ];

        public function register(): void
        {
            $this->reportable(function (Throwable $e) {
                //
            });
        }
    }
    ```

    커스터마이즈가 있는 경우에는 `bootstrap/app.php`의 `withExceptions()`로 이식한 뒤 삭제합니다.

    ```php theme={null}
    use Throwable;
    use Illuminate\Http\Request;

    ->withExceptions(function (Exceptions $exceptions) {
        // 例外レポートのカスタマイズ
        $exceptions->report(function (Throwable $e) {
            // ...
        });

        // 例外レンダリングのカスタマイズ
        $exceptions->render(function (\App\Exceptions\InvalidOrderException $e, Request $request) {
            return response()->view('errors.invalid-order', status: 500);
        });
    })
    ```

    `$dontFlash`에 독자의 항목을 추가하고 있던 경우에는 마찬가지로 이식할 수 있습니다.

    ```php theme={null}
    ->withExceptions(function (Exceptions $exceptions) {
        $exceptions->dontFlash([
            'my_sensitive_field',
        ]);
    })
    ```

    이식이 완료되면 `app/Exceptions/Handler.php`를 삭제합니다.
  </Step>

  <Step title="RouteServiceProvider를 삭제하고 라우트 등록을 이관한다">
    `app/Providers/RouteServiceProvider.php`는 라우트 파일의 로드와 레이트 리밋의 설정을 하고 있었습니다.

    **구(app/Providers/RouteServiceProvider.php):**

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

    namespace App\Providers;

    use Illuminate\Cache\RateLimiting\Limit;
    use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
    use Illuminate\Http\Request;
    use Illuminate\Support\Facades\RateLimiter;
    use Illuminate\Support\Facades\Route;

    class RouteServiceProvider extends ServiceProvider
    {
        public const HOME = '/home';

        public function boot(): void
        {
            RateLimiter::for('api', function (Request $request) {
                return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
            });

            $this->routes(function () {
                Route::middleware('api')
                    ->prefix('api')
                    ->group(base_path('routes/api.php'));

                Route::middleware('web')
                    ->group(base_path('routes/web.php'));
            });
        }
    }
    ```

    **라우트 파일의 등록**을 `bootstrap/app.php`의 `withRouting()`으로 이관합니다.

    ```php theme={null}
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php', // API ルートを使う場合
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ```

    **레이트 리밋**은 `AppServiceProvider::boot()`로 이관합니다.

    ```php theme={null}
    // app/Providers/AppServiceProvider.php
    use Illuminate\Cache\RateLimiting\Limit;
    use Illuminate\Http\Request;
    use Illuminate\Support\Facades\RateLimiter;

    public function boot(): void
    {
        RateLimiter::for('api', function (Request $request) {
            return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
        });
    }
    ```

    `HOME` 상수를 사용하고 있는 곳이 있는 경우에는, 직접 URL 문자열로 치환하거나, `AppServiceProvider`에 상수를 이동해 주세요.

    이관 후에 `app/Providers/RouteServiceProvider.php`를 삭제합니다.
  </Step>

  <Step title="서비스 프로바이더를 정리한다">
    Laravel 10에서는 기본으로 5개의 서비스 프로바이더가 준비되어 있었습니다. 이것들을 `AppServiceProvider.php` 하나로 통합합니다.

    **삭제할 프로바이더(내용을 AppServiceProvider로 옮긴 뒤 삭제):**

    | 파일                                           | 이관처                                                               |
    | -------------------------------------------- | ----------------------------------------------------------------- |
    | `app/Providers/AuthServiceProvider.php`      | `AppServiceProvider::boot()`의 `Gate::policy()` 등                  |
    | `app/Providers/BroadcastServiceProvider.php` | Broadcasting을 사용하는 경우에는 `bootstrap/app.php`의 `withBroadcasting()` |
    | `app/Providers/EventServiceProvider.php`     | `AppServiceProvider::boot()`의 `Event::listen()` 등                 |
    | `app/Providers/RouteServiceProvider.php`     | 이전 스텝에서 대응 완료                                                     |

    **구(app/Providers/AuthServiceProvider.php)의 내용을 이관하는 예:**

    ```php theme={null}
    // 旧: app/Providers/AuthServiceProvider.php
    use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;

    class AuthServiceProvider extends ServiceProvider
    {
        protected $policies = [
            Post::class => PostPolicy::class,
        ];

        public function boot(): void
        {
            $this->registerPolicies();
        }
    }
    ```

    ```php theme={null}
    // 新: app/Providers/AppServiceProvider.php
    // Laravelの標準的な命名規則に従っているなら自動検出されるので不要です
    use Illuminate\Support\Facades\Gate;

    class AppServiceProvider extends ServiceProvider
    {
        public function boot(): void
        {
            Gate::policy(Post::class, PostPolicy::class);
        }
    }
    ```

    불필요해진 프로바이더 파일을 삭제하면, `config/app.php`의 `providers` 배열을 삭제합니다.

    ```php theme={null}
    // config/app.php の providers 配列
    // 全て削除して構いません
    'providers' => ServiceProvider::defaultProviders()->merge([
    App\Providers\AppServiceProvider::class,
    // 削除したプロバイダーのエントリも削除
    // App\Providers\AuthServiceProvider::class, // 削除
    // App\Providers\EventServiceProvider::class, // 削除
    // App\Providers\RouteServiceProvider::class, // 削除
    ])->toArray(),
    ```

    또한, `bootstrap/providers.php`를 작성하여 새 구조에 대응합니다.

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

    // bootstrap/providers.php
    return [
        App\Providers\AppServiceProvider::class,
    ];
    ```

    <Info>
      `bootstrap/providers.php`가 존재하는 경우, Laravel은 이쪽을 프로바이더 목록으로서 우선하여 로드합니다.
    </Info>
  </Step>

  <Step title="컨트롤러 베이스 클래스를 갱신한다">
    Laravel 10의 `Controller` 베이스 클래스는 `AuthorizesRequests`와 `ValidatesRequests` 트레이트를 사용하고 있었습니다. Laravel 11의 새 베이스 클래스는 이러한 트레이트를 가지지 않는 심플한 추상 클래스입니다.

    **구(Laravel 10):**

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

    namespace App\Http\Controllers;

    use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
    use Illuminate\Foundation\Validation\ValidatesRequests;
    use Illuminate\Routing\Controller as BaseController;

    class Controller extends BaseController
    {
        use AuthorizesRequests, ValidatesRequests;
    }
    ```

    **신(Laravel 11 이후):**

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

    namespace App\Http\Controllers;

    abstract class Controller
    {
        //
    }
    ```

    트레이트가 제공하고 있던 기능은 다음과 같이 치환합니다.

    | 구의 방법(트레이트 경유)                          | 신의 방법                                                                                |
    | --------------------------------------- | ------------------------------------------------------------------------------------ |
    | `$this->validate($request, $rules)`     | `$request->validate($rules)`                                                         |
    | `$this->authorize('update', $post)`     | `Gate::authorize('update', $post)`                                                   |
    | `$this->authorizeResource(Post::class)` | Laravel11에서는 간단한 대체 수단이 없으므로 `App\Http\Controllers\Controller`를 Laravel10 사양 그대로 둡니다 |

    기존의 컨트롤러가 트레이트의 메서드를 사용하고 있는 경우에는, 각 컨트롤러를 수정하거나, 트레이트를 `Controller` 베이스 클래스에 남기거나를 선택할 수 있습니다. 한꺼번에 모두를 변경하지 않아도 동작은 합니다.

    <Warning>
      Breeze나 Jetstream이 생성한 인증 컨트롤러가 `$this->validate()`나 `$this->authorize()`를 호출하고 있는 경우에는, 베이스 클래스를 변경하기 전에 반드시 동작 확인해 주세요.
    </Warning>
  </Step>

  <Step title="불필요한 config 파일을 삭제한다">
    `config/cors.php`, `config/hashing.php`, `config/view.php` 등 기본에서 변경하지 않은 파일은 삭제할 수 있습니다. 변경하고 있는 파일은 남겨 주세요.
  </Step>

  <Step title="public/index.php를 갱신한다">
    새 구조에 맞춰 변경되어 있으므로 전면적으로 다시 씁니다.

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

    use Illuminate\Foundation\Application;
    use Illuminate\Http\Request;

    define('LARAVEL_START', microtime(true));

    // Determine if the application is in maintenance mode...
    if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
    require $maintenance;
    }

    // Register the Composer autoloader...
    require __DIR__.'/../vendor/autoload.php';

    // Bootstrap Laravel and handle the request...
    /** @var Application $app */
    $app = require_once __DIR__.'/../bootstrap/app.php';

    $app->handleRequest(Request::capture());
    ```
  </Step>

  <Step title="artisan을 갱신한다">
    artisan 파일도 마찬가지로 모두 다시 씁니다.

    ```php theme={null}
    #!/usr/bin/env php
    <?php

    use Illuminate\Foundation\Application;
    use Symfony\Component\Console\Input\ArgvInput;

    define('LARAVEL_START', microtime(true));

    // Register the Composer autoloader...
    require __DIR__.'/vendor/autoload.php';

    // Bootstrap Laravel and handle the command...
    /** @var Application $app */
    $app = require_once __DIR__.'/bootstrap/app.php';

    $status = $app->handleCommand(new ArgvInput);

    exit($status);
    ```
  </Step>

  <Step title="tests/TestCase.php를 갱신한다">
    `CreatesApplication` 트레이트가 불필요해지므로 변경합니다.
    `tests/CreatesApplication.php`는 삭제해도 상관없습니다.

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

    namespace Tests;

    use Illuminate\Foundation\Testing\TestCase as BaseTestCase;

    abstract class TestCase extends BaseTestCase
    {
        //
    }
    ```
  </Step>

  <Step title=".env .env.example phpunit.xml을 갱신한다">
    `CACHE_DRIVER`에서 `CACHE_STORE`로 바뀌거나 항목이 늘어나 있으므로 필요하다면 변경합니다.
    이 부근의 변경은 config 파일이나 본번 환경도 관계되므로 신중히 해 주세요.
    무리하게 추종할 필요는 없습니다.
  </Step>

  <Step title="동작 확인">
    이관이 완료되면, 이하의 순서로 동작 확인을 합니다.

    ```shell theme={null}
    # 設定キャッシュのクリア
    php artisan config:clear
    php artisan route:clear
    php artisan cache:clear

    # ルートが正しく登録されているか確認
    php artisan route:list

    # テストを実行
    php artisan test
    ```

    문제가 발생한 경우에는, 삭제한 파일을 백업에서 복원하고, 에러 메시지를 확인해 주세요.
  </Step>
</Steps>

## 이관 후의 파일 구성

이관 후의 프로젝트는, 이하와 같은 구성이 됩니다(구 구조로부터의 변경점을 보임).

```
app/
├── Console/
│   └── Commands/          ← カスタムコマンドはそのまま残す
│   （Kernel.php は削除）
├── Exceptions/
│   （Handler.php は削除）
├── Http/
│   ├── Controllers/
│   │   └── Controller.php ← シンプルな抽象クラスに変更
│   └── Middleware/        ← カスタムミドルウェアはそのまま残す
│   （Kernel.php は削除）
├── Models/
└── Providers/
    └── AppServiceProvider.php ← 1 ファイルに統合
    （AuthServiceProvider.php は削除）
    （BroadcastServiceProvider.php は削除）
    （EventServiceProvider.php は削除）
    （RouteServiceProvider.php は削除）
bootstrap/
├── app.php                ← Application::configure() 形式に置き換え
└── providers.php          ← 新規作成
routes/
├── web.php
├── api.php                ← 必要な場合のみ
└── console.php            ← スケジュール定義もここに
```

## 정리

| 이관 내용        | 구의 장소                                    | 신의 장소                                       |
| ------------ | ---------------------------------------- | ------------------------------------------- |
| HTTP 미들웨어    | `app/Http/Kernel.php`                    | `bootstrap/app.php`의 `withMiddleware()`     |
| 예외 핸들링       | `app/Exceptions/Handler.php`             | `bootstrap/app.php`의 `withExceptions()`     |
| 스케줄 정의       | `app/Console/Kernel.php`                 | `routes/console.php`                        |
| 라우트 등록       | `app/Providers/RouteServiceProvider.php` | `bootstrap/app.php`의 `withRouting()`        |
| 서비스 프로바이더 목록 | `config/app.php`의 `providers` 배열         | `bootstrap/providers.php`                   |
| 폴리시 등록       | `app/Providers/AuthServiceProvider.php`  | 자동 등록. `AppServiceProvider::boot()`에서 수동 등록 |
| 이벤트 리스너      | `app/Providers/EventServiceProvider.php` | 자동 등록. `AppServiceProvider::boot()`에서 수동 등록 |

## 참고 링크

* [Laravel 11 이후의 애플리케이션 구조](/ko/advanced/app-structure)
* [새 앱 구조 FAQ](/ko/advanced/app-structure-faq)
* [공식 업그레이드 가이드(11.x)](https://github.com/laravel/docs/blob/11.x/upgrade.md)
* [laravel/laravel 리포지토리 10.x → 11.x 차분](https://github.com/laravel/laravel/compare/10.x...11.x)
* [Laravel 10의 Controller.php](https://github.com/laravel/laravel/blob/10.x/app/Http/Controllers/Controller.php)
* [Laravel 11의 Controller.php](https://github.com/laravel/laravel/blob/11.x/app/Http/Controllers/Controller.php)


## Related topics

- [Laravel 11 이후의 새 앱 구조 FAQ](/ko/advanced/app-structure-faq.md)
- [Laravel 10에서 11로의 업그레이드](/ko/blog/upgrade-10-to-11.md)
- [Laravel 11 이후의 애플리케이션 구조](/ko/advanced/app-structure.md)
- [Laravel 12에서 13으로의 업그레이드 가이드](/ko/blog/upgrade-12-to-13.md)
- [파사드](/ko/facades.md)
