컨트롤러

번역일: 2026년 7월 2일

컨트롤러

소개

모든 요청 처리 로직을 라우트 파일의 클로저로 작성하는 대신, 컨트롤러 클래스를 사용해 관련 로직을 한 곳에 모을 수 있습니다. 예를 들어 UserController는 사용자 조회, 생성, 수정, 삭제에 관한 모든 요청을 담당합니다. 컨트롤러는 기본적으로 app/Http/Controllers 디렉터리에 저장됩니다.

컨트롤러 작성

기본 컨트롤러

Artisan 명령어로 컨트롤러를 빠르게 생성할 수 있습니다:

php artisan make:controller UserController

생성된 컨트롤러는 app/Http/Controllers 디렉터리에 저장됩니다. 아래는 기본 컨트롤러의 예시입니다. 컨트롤러에는 HTTP 요청에 응답하는 public 메서드를 원하는 만큼 정의할 수 있습니다:

<?php namespace App\Http\Controllers; use App\Models\User; use Illuminate\View\View; class UserController extends Controller { /** * 주어진 사용자의 프로필을 표시합니다. */ public function show(string $id): View { return view('user.profile', [ 'user' => User::findOrFail($id) ]); } }

컨트롤러 클래스와 메서드를 작성했다면, 다음과 같이 라우트를 등록합니다:

use App\Http\Controllers\UserController; Route::get('/user/{id}', [UserController::class, 'show']);

요청이 해당 URI와 일치하면 UserControllershow 메서드가 호출되고, 라우트 파라미터가 메서드 인수로 전달됩니다.

NOTE

컨트롤러가 반드시 베이스 클래스를 상속할 필요는 없습니다. 다만, 여러 컨트롤러에서 공통으로 사용할 메서드가 있다면 베이스 컨트롤러 클래스를 만들어 상속하는 방식이 편리합니다.

단일 액션 컨트롤러

특정 액션의 로직이 복잡해서 컨트롤러 하나를 그 액션에만 전담시키고 싶다면, 컨트롤러에 __invoke 메서드 하나만 정의하면 됩니다:

<?php namespace App\Http\Controllers; class ProvisionServer extends Controller { /** * 새 웹 서버를 프로비저닝합니다. */ public function __invoke() { // ... } }

단일 액션 컨트롤러의 라우트를 등록할 때는 메서드 이름을 지정하지 않고 컨트롤러 클래스명만 전달합니다:

use App\Http\Controllers\ProvisionServer; Route::post('/server', ProvisionServer::class);

--invokable 옵션으로 단일 액션 컨트롤러를 바로 생성할 수도 있습니다:

php artisan make:controller ProvisionServer --invokable

NOTE

컨트롤러 스텁은 스텁 퍼블리싱을 통해 커스터마이징할 수 있습니다.

컨트롤러 미들웨어

미들웨어는 라우트 파일에서 컨트롤러 라우트에 직접 지정할 수 있습니다:

Route::get('/profile', [UserController::class, 'show'])->middleware('auth');

또는 컨트롤러 클래스 내부에서 미들웨어를 지정할 수도 있습니다. 이 방법을 사용하려면 컨트롤러가 HasMiddleware 인터페이스를 구현해야 하며, 정적 middleware 메서드에서 적용할 미들웨어 배열을 반환합니다:

<?php namespace App\Http\Controllers; use Illuminate\Routing\Controllers\HasMiddleware; use Illuminate\Routing\Controllers\Middleware; class UserController implements HasMiddleware { /** * 컨트롤러에 적용할 미들웨어를 반환합니다. */ public static function middleware(): array { return [ 'auth', new Middleware('log', only: ['index']), new Middleware('subscribed', except: ['store']), ]; } // ... }

별도의 미들웨어 클래스를 만들지 않고 클로저로 인라인 미들웨어를 정의하는 방법도 있습니다:

use Closure; use Illuminate\Http\Request; /** * 컨트롤러에 적용할 미들웨어를 반환합니다. */ public static function middleware(): array { return [ function (Request $request, Closure $next) { return $next($request); }, ]; }

리소스 컨트롤러

애플리케이션의 Eloquent 모델 각각을 "리소스"로 생각하면, 대부분의 리소스에 대해 생성·조회·수정·삭제(CRUD)라는 동일한 작업이 반복됩니다. 예를 들어 Photo 모델과 Movie 모델이 있다면, 사용자는 두 모델 모두에 대해 같은 유형의 작업을 수행할 가능성이 높습니다.

Laravel의 리소스 라우팅은 이런 반복 패턴을 단 한 줄의 코드로 처리합니다. --resource 옵션으로 컨트롤러를 생성하면 CRUD 액션 메서드가 자동으로 포함됩니다:

php artisan make:controller PhotoController --resource

이 명령어는 app/Http/Controllers/PhotoController.php를 생성합니다. 이제 리소스 라우트를 등록합니다:

use App\Http\Controllers\PhotoController; Route::resource('photos', PhotoController::class);

이 한 줄로 리소스에 대한 여러 라우트가 한꺼번에 등록됩니다. route:list Artisan 명령어로 전체 라우트 목록을 확인할 수 있습니다.

여러 리소스 컨트롤러를 한 번에 등록하려면 resources 메서드에 배열을 전달합니다:

Route::resources([ 'photos' => PhotoController::class, 'posts' => PostController::class, ]);

소프트 삭제를 사용하는 여러 리소스를 한 번에 등록할 때는 softDeletableResources 메서드를 사용합니다:

Route::softDeletableResources([ 'photos' => PhotoController::class, 'posts' => PostController::class, ]);

리소스 컨트롤러가 처리하는 액션

HTTP 메서드URI액션라우트 이름
GET/photosindexphotos.index
GET/photos/createcreatephotos.create
POST/photosstorephotos.store
GET/photos/{photo}showphotos.show
GET/photos/{photo}/editeditphotos.edit
PUT/PATCH/photos/{photo}updatephotos.update
DELETE/photos/{photo}destroyphotos.destroy

모델을 찾지 못했을 때 동작 커스터마이징

암묵적으로 바인딩된 리소스 모델을 찾지 못하면 기본적으로 404 응답이 반환됩니다. missing 메서드를 사용하면 이 동작을 커스터마이징할 수 있습니다. 전달한 클로저는 모델을 찾지 못할 때 호출됩니다:

use App\Http\Controllers\PhotoController; use Illuminate\Http\Request; use Illuminate\Support\Facades\Redirect; Route::resource('photos', PhotoController::class) ->missing(function (Request $request) { return Redirect::route('photos.index'); });

소프트 삭제된 모델

암묵적 모델 바인딩은 기본적으로 소프트 삭제된 모델을 조회하지 않고 404를 반환합니다. withTrashed 메서드를 사용하면 소프트 삭제된 모델도 허용할 수 있습니다:

use App\Http\Controllers\PhotoController; Route::resource('photos', PhotoController::class)->withTrashed();

인수 없이 withTrashed를 호출하면 show, edit, update 라우트에서 소프트 삭제된 모델을 허용합니다. 특정 라우트만 지정하려면 배열로 전달합니다:

Route::resource('photos', PhotoController::class)->withTrashed(['show']);

리소스 모델 지정

라우트 모델 바인딩을 사용하고 컨트롤러 메서드에서 모델 인스턴스를 타입힌트로 받고 싶다면, 컨트롤러 생성 시 --model 옵션을 사용합니다:

php artisan make:controller PhotoController --model=Photo --resource

폼 요청 클래스 생성

리소스 컨트롤러 생성 시 --requests 옵션을 추가하면, storeupdate 메서드에 사용할 폼 요청 클래스도 함께 생성됩니다:

php artisan make:controller PhotoController --model=Photo --resource --requests

부분 리소스 라우트

리소스 라우트를 등록할 때 기본 액션 전체가 아닌 일부만 사용하도록 제한할 수 있습니다:

use App\Http\Controllers\PhotoController; Route::resource('photos', PhotoController::class)->only([ 'index', 'show' ]); Route::resource('photos', PhotoController::class)->except([ 'create', 'store', 'update', 'destroy' ]);

API 리소스 라우트

API용 리소스 라우트를 선언할 때는 HTML 폼을 렌더링하는 createedit 라우트가 필요 없는 경우가 많습니다. apiResource 메서드를 사용하면 이 두 라우트를 자동으로 제외합니다:

use App\Http\Controllers\PhotoController; Route::apiResource('photos', PhotoController::class);

apiResources 메서드로 여러 API 리소스 컨트롤러를 한 번에 등록할 수도 있습니다:

use App\Http\Controllers\PhotoController; use App\Http\Controllers\PostController; Route::apiResources([ 'photos' => PhotoController::class, 'posts' => PostController::class, ]);

createedit 메서드가 없는 API 리소스 컨트롤러를 바로 생성하려면 make:controller 명령어에 --api 옵션을 사용합니다:

php artisan make:controller PhotoController --api

중첩 리소스

리소스가 다른 리소스에 속하는 경우, 중첩 라우트가 필요할 수 있습니다. 예를 들어 사진(photo)에 여러 댓글(comment)이 달릴 수 있다면, 라우트 선언에 "점(dot) 표기법"을 사용합니다:

use App\Http\Controllers\PhotoCommentController; Route::resource('photos.comments', PhotoCommentController::class);

이렇게 하면 다음과 같은 URI로 접근할 수 있는 중첩 리소스 라우트가 등록됩니다:

/photos/{photo}/comments/{comment}

중첩 리소스 스코핑

Laravel의 암묵적 모델 바인딩 기능은 자식 모델이 부모 모델에 실제로 속하는지 자동으로 확인(스코핑)해 줍니다. 중첩 리소스를 정의할 때 scoped 메서드를 사용하면 자동 스코핑을 활성화하고, 자식 리소스를 어떤 필드로 조회할지 지정할 수 있습니다. 자세한 내용은 리소스 라우트 스코핑 섹션을 참고하세요.

얕은 중첩(Shallow Nesting)

자식 모델의 ID가 이미 고유 식별자(예: 자동 증가 기본 키)라면, URI에 부모 ID와 자식 ID를 모두 포함할 필요가 없습니다. 이런 경우 "얕은 중첩"을 사용할 수 있습니다:

use App\Http\Controllers\CommentController; Route::resource('photos.comments', CommentController::class)->shallow();

이 라우트 정의는 다음 라우트들을 생성합니다:

HTTP 메서드URI액션라우트 이름
GET/photos/{photo}/commentsindexphotos.comments.index
GET/photos/{photo}/comments/createcreatephotos.comments.create
POST/photos/{photo}/commentsstorephotos.comments.store
GET/comments/{comment}showcomments.show
GET/comments/{comment}/editeditcomments.edit
PUT/PATCH/comments/{comment}updatecomments.update
DELETE/comments/{comment}destroycomments.destroy

리소스 라우트 이름 지정

기본적으로 리소스 컨트롤러의 모든 액션에는 라우트 이름이 자동으로 부여됩니다. names 배열을 전달해 원하는 이름으로 재정의할 수 있습니다:

use App\Http\Controllers\PhotoController; Route::resource('photos', PhotoController::class)->names([ 'create' => 'photos.build' ]);

리소스 라우트 파라미터 이름 지정

Route::resource는 기본적으로 리소스 이름의 단수형을 라우트 파라미터 이름으로 사용합니다. parameters 메서드에 연관 배열을 전달해 리소스별로 파라미터 이름을 변경할 수 있습니다:

use App\Http\Controllers\AdminUserController; Route::resource('users', AdminUserController::class)->parameters([ 'users' => 'admin_user' ]);

위 예시에서 show 라우트의 URI는 다음과 같이 생성됩니다:

/users/{admin_user}

리소스 라우트 스코핑

Laravel의 스코프 암묵적 모델 바인딩은 자식 모델이 부모 모델에 속하는지 자동으로 확인합니다. scoped 메서드를 사용하면 자동 스코핑을 활성화하고 자식 리소스를 어떤 필드로 조회할지 지정할 수 있습니다:

use App\Http\Controllers\PhotoCommentController; Route::resource('photos.comments', PhotoCommentController::class)->scoped([ 'comment' => 'slug', ]);

이 라우트는 다음과 같은 URI로 접근할 수 있는 스코프된 중첩 리소스를 등록합니다:

/photos/{photo}/comments/{comment:slug}

커스텀 키를 사용하는 암묵적 바인딩이 중첩 라우트 파라미터로 사용되면, Laravel은 부모 모델에서 관계 이름을 자동으로 추론하여 자식 모델을 조회합니다. 이 경우 Photo 모델에 comments라는 관계(라우트 파라미터 이름의 복수형)가 있다고 가정하고 Comment 모델을 조회합니다.

리소스 URI 현지화

기본적으로 Route::resource는 영어 동사와 복수형 규칙을 사용해 리소스 URI를 생성합니다. createedit 동사를 다른 언어로 변경하려면 Route::resourceVerbs 메서드를 사용합니다. App\Providers\AppServiceProviderboot 메서드 초반에 설정하는 것이 좋습니다:

/** * 애플리케이션 서비스를 부트스트랩합니다. */ public function boot(): void { Route::resourceVerbs([ 'create' => 'crear', 'edit' => 'editar', ]); }

Laravel의 복수화 기능은 여러 언어를 지원합니다. 동사와 복수화 언어를 커스터마이징하면, 예를 들어 Route::resource('publicacion', PublicacionController::class)는 다음과 같은 URI를 생성합니다:

/publicacion/crear

/publicacion/{publicaciones}/editar

리소스 컨트롤러 보완

기본 리소스 라우트 외에 추가 라우트가 필요하다면, 반드시 Route::resource 호출 이전에 정의해야 합니다. 그렇지 않으면 resource 메서드가 등록한 라우트가 추가 라우트보다 우선할 수 있습니다:

use App\Http\Controller\PhotoController; Route::get('/photos/popular', [PhotoController::class, 'popular']); Route::resource('photos', PhotoController::class);

NOTE

컨트롤러는 하나의 책임에 집중하도록 유지하세요. 기본 리소스 액션 외에 메서드가 자주 추가된다면, 컨트롤러를 더 작은 두 개로 분리하는 것을 고려하세요.

싱글톤 리소스 컨트롤러

애플리케이션에는 인스턴스가 단 하나뿐인 리소스가 있을 수 있습니다. 예를 들어 사용자의 "프로필"은 수정할 수 있지만 한 사용자가 여러 개의 프로필을 가질 수 없습니다. 마찬가지로 이미지에는 "썸네일"이 하나뿐일 수 있습니다. 이런 리소스를 "싱글톤 리소스"라 하며, singleton 메서드로 등록합니다:

use App\Http\Controllers\ProfileController; use Illuminate\Support\Facades\Route; Route::singleton('profile', ProfileController::class);

싱글톤 리소스는 인스턴스가 하나뿐이므로 "생성" 라우트가 등록되지 않으며, 식별자를 필요로 하지 않습니다:

HTTP 메서드URI액션라우트 이름
GET/profileshowprofile.show
GET/profile/editeditprofile.edit
PUT/PATCH/profileupdateprofile.update

싱글톤 리소스는 일반 리소스 안에 중첩할 수도 있습니다:

Route::singleton('photos.thumbnail', ThumbnailController::class);

이 예시에서 photos 리소스는 표준 리소스 라우트 전체를 받으며, thumbnail 리소스는 다음과 같은 싱글톤 라우트를 갖습니다:

HTTP 메서드URI액션라우트 이름
GET/photos/{photo}/thumbnailshowphotos.thumbnail.show
GET/photos/{photo}/thumbnail/editeditphotos.thumbnail.edit
PUT/PATCH/photos/{photo}/thumbnailupdatephotos.thumbnail.update

생성 가능한 싱글톤 리소스

싱글톤 리소스에도 생성 및 저장 라우트가 필요하다면 creatable 메서드를 사용합니다:

Route::singleton('photos.thumbnail', ThumbnailController::class)->creatable();

이 경우 DELETE 라우트도 함께 등록됩니다:

HTTP 메서드URI액션라우트 이름
GET/photos/{photo}/thumbnail/createcreatephotos.thumbnail.create
POST/photos/{photo}/thumbnailstorephotos.thumbnail.store
GET/photos/{photo}/thumbnailshowphotos.thumbnail.show
GET/photos/{photo}/thumbnail/editeditphotos.thumbnail.edit
PUT/PATCH/photos/{photo}/thumbnailupdatephotos.thumbnail.update
DELETE/photos/{photo}/thumbnaildestroyphotos.thumbnail.destroy

생성·저장 라우트 없이 DELETE 라우트만 등록하고 싶다면 destroyable 메서드를 사용합니다:

Route::singleton(...)->destroyable();

API 싱글톤 리소스

apiSingleton 메서드를 사용하면 createedit 라우트 없이 API용 싱글톤 리소스를 등록합니다:

Route::apiSingleton('profile', ProfileController::class);

API 싱글톤 리소스에도 creatable을 추가하면 storedestroy 라우트가 함께 등록됩니다:

Route::apiSingleton('photos.thumbnail', ProfileController::class)->creatable();

미들웨어와 리소스 컨트롤러

middleware, middlewareFor, withoutMiddlewareFor 메서드를 사용하면 리소스 라우트의 전체 또는 특정 메서드에만 미들웨어를 세밀하게 적용할 수 있습니다.

전체 메서드에 미들웨어 적용

middleware 메서드로 리소스(또는 싱글톤 리소스)의 모든 라우트에 미들웨어를 적용합니다:

Route::resource('users', UserController::class) ->middleware(['auth', 'verified']); Route::singleton('profile', ProfileController::class) ->middleware('auth');

특정 메서드에만 미들웨어 적용

middlewareFor 메서드를 사용하면 특정 메서드에만 미들웨어를 적용할 수 있습니다:

Route::resource('users', UserController::class) ->middlewareFor('show', 'auth'); Route::apiResource('users', UserController::class) ->middlewareFor(['show', 'update'], 'auth'); Route::resource('users', UserController::class) ->middlewareFor('show', 'auth') ->middlewareFor('update', 'auth'); Route::apiResource('users', UserController::class) ->middlewareFor(['show', 'update'], ['auth', 'verified']);

middlewareFor는 싱글톤 및 API 싱글톤 리소스 컨트롤러에도 사용할 수 있습니다:

Route::singleton('profile', ProfileController::class) ->middlewareFor('show', 'auth'); Route::apiSingleton('profile', ProfileController::class) ->middlewareFor(['show', 'update'], 'auth');

특정 메서드에서 미들웨어 제외

withoutMiddlewareFor 메서드를 사용하면 특정 메서드에서 미들웨어를 제외할 수 있습니다:

Route::middleware(['auth', 'verified', 'subscribed'])->group(function () { Route::resource('users', UserController::class) ->withoutMiddlewareFor('index', ['auth', 'verified']) ->withoutMiddlewareFor(['create', 'store'], 'verified') ->withoutMiddlewareFor('destroy', 'subscribed'); });

의존성 주입과 컨트롤러

생성자 주입

Laravel의 서비스 컨테이너는 모든 컨트롤러를 해석(resolve)합니다. 덕분에 컨트롤러 생성자에 필요한 의존성을 타입힌트로 선언하면, 컨테이너가 자동으로 해당 인스턴스를 주입해 줍니다:

<?php namespace App\Http\Controllers; use App\Repositories\UserRepository; class UserController extends Controller { /** * 새 컨트롤러 인스턴스를 생성합니다. */ public function __construct( protected UserRepository $users, ) {} }

메서드 주입

생성자 주입 외에도 컨트롤러 메서드의 인수에 타입힌트를 사용해 의존성을 주입받을 수 있습니다. 가장 일반적인 예는 Illuminate\Http\Request 인스턴스를 메서드에 주입하는 방식입니다:

<?php namespace App\Http\Controllers; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; class UserController extends Controller { /** * 새 사용자를 저장합니다. */ public function store(Request $request): RedirectResponse { $name = $request->name; // 사용자를 저장합니다... return redirect('/users'); } }

컨트롤러 메서드에 라우트 파라미터도 함께 받아야 한다면, 다른 의존성 뒤에 라우트 인수를 선언합니다. 예를 들어 라우트가 다음과 같이 정의되어 있다면:

use App\Http\Controllers\UserController; Route::put('/user/{id}', [UserController::class, 'update']);

다음과 같이 Illuminate\Http\Request를 타입힌트로 받으면서 id 파라미터도 함께 사용할 수 있습니다:

<?php namespace App\Http\Controllers; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; class UserController extends Controller { /** * 주어진 사용자를 수정합니다. */ public function update(Request $request, string $id): RedirectResponse { // 사용자를 수정합니다... return redirect('/users'); } }

이 문서는 Laravel 공식 문서(MIT)를 한국 개발자를 위해 번역·재구성한 것입니다.

번역일: 2026년 7월 2일