컨트롤러

업데이트됨

번역일: 2026년 7월 15일

이 페이지는 원문이 업데이트되어 번역이 갱신되었습니다.

원문 수정
2026년 7월 15일
번역 갱신
2026년 7월 15일

컨트롤러

소개

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

컨트롤러 작성하기

기본 컨트롤러

새 컨트롤러를 빠르게 생성하려면 make:controller Artisan 명령을 사용합니다. 생성된 파일은 app/Http/Controllers 디렉터리에 저장됩니다.

php artisan make:controller UserController

다음은 기본 컨트롤러의 예시입니다. 컨트롤러에는 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가 라우트와 일치하면 App\Http\Controllers\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 옵션을 사용하면 __invoke 메서드가 포함된 컨트롤러를 바로 생성할 수 있습니다.

php artisan make:controller ProvisionServer --invokable

NOTE

컨트롤러 스텁은 stub 배포를 통해 커스터마이징할 수 있습니다.

컨트롤러 미들웨어

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

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); }, ]; }

미들웨어 어트리뷰트

PHP 어트리뷰트를 사용해 컨트롤러에 미들웨어를 지정할 수도 있습니다.

<?php namespace App\Http\Controllers; use Illuminate\Routing\Attributes\Controllers\Middleware; #[Middleware('auth')] #[Middleware('log', only: ['index'])] #[Middleware('subscribed', except: ['store'])] class UserController { // ... }

개별 메서드에도 미들웨어 어트리뷰트를 적용할 수 있으며, 메서드 레벨의 미들웨어는 클래스 레벨 미들웨어와 병합됩니다.

<?php namespace App\Http\Controllers; use Closure; use Illuminate\Http\Request; use Illuminate\Routing\Attributes\Controllers\Middleware; #[Middleware('auth')] class UserController { #[Middleware('log')] #[Middleware('subscribed')] public function index() { // ... } #[Middleware(static function (Request $request, Closure $next) { // ... return $next($request); })] public function store() { // ... } }

특정 미들웨어를 컨트롤러 또는 메서드에서 제외하려면 WithoutMiddleware 어트리뷰트를 사용합니다. onlyexcept 인수로 클래스 레벨 어트리뷰트를 특정 메서드에만 적용하거나 제외할 수 있습니다.

<?php namespace App\Http\Controllers; use App\Http\Middleware\EnsureTokenIsValid; use Illuminate\Routing\Attributes\Controllers\WithoutMiddleware; #[WithoutMiddleware('subscribed', except: ['index'])] class UserController { #[WithoutMiddleware(EnsureTokenIsValid::class)] public function index() { // ... } public function show() { // ... } }

클래스 레벨의 WithoutMiddleware 어트리뷰트는 자식 컨트롤러에 상속됩니다. 이 어트리뷰트는 라우트 미들웨어만 제거할 수 있으며, 전역 미들웨어에는 적용되지 않습니다.

인가 어트리뷰트

Policy를 통해 컨트롤러 액션을 인가할 때, Authorize 어트리뷰트를 can 미들웨어의 간편한 대안으로 사용할 수 있습니다.

<?php namespace App\Http\Controllers; use App\Models\Comment; use App\Models\Post; use Illuminate\Routing\Attributes\Controllers\Authorize; class CommentController { #[Authorize('create', [Comment::class, 'post'])] public function store(Post $post) { // ... } #[Authorize('delete', 'comment')] public function destroy(Comment $comment) { // ... } }

첫 번째 인수는 인가할 ability이고, 두 번째 인수는 Policy에 전달할 모델 클래스, 라우트 파라미터, 또는 파라미터 배열입니다.

리소스 컨트롤러

Eloquent 모델 하나를 "리소스"로 본다면, 대부분의 리소스에는 생성(create), 조회(read), 수정(update), 삭제(delete), 즉 CRUD 액션이 반복적으로 등장합니다. Laravel의 리소스 라우팅은 이 CRUD 라우트를 단 한 줄로 컨트롤러에 연결해 줍니다.

--resource 옵션으로 리소스 컨트롤러를 빠르게 생성할 수 있습니다.

php artisan make:controller PhotoController --resource

생성된 app/Http/Controllers/PhotoController.php에는 각 리소스 액션에 해당하는 메서드 스텁이 포함됩니다. 이제 아래와 같이 리소스 라우트를 등록합니다.

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

이 한 줄로 다양한 HTTP 동사에 대응하는 여러 라우트가 한 번에 등록됩니다. route:list Artisan 명령으로 등록된 라우트를 확인할 수 있습니다.

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

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

withTrashed 메서드를 사용하는 여러 리소스 컨트롤러를 한 번에 등록하려면 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 HTTP 응답이 반환됩니다. 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();

인수 없이 호출하면 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

부분 리소스 라우트

리소스 라우트를 등록할 때 기본 7개 액션 중 일부만 사용하도록 제한할 수 있습니다.

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);

여러 API 리소스 컨트롤러를 한 번에 등록할 수도 있습니다.

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

createedit 메서드가 없는 API 리소스 컨트롤러를 생성하려면 --api 옵션을 사용합니다.

php artisan make:controller PhotoController --api

중첩 리소스

리소스 안에 다른 리소스가 포함되는 경우, 점(.) 표기법으로 중첩 라우트를 정의할 수 있습니다. 예를 들어 사진(photo)에 여러 댓글(comment)이 달리는 구조라면 다음과 같이 작성합니다.

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

이 라우트는 다음과 같은 URI로 접근할 수 있습니다.

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

중첩 리소스 스코핑

Laravel의 묵시적 모델 바인딩 스코핑 기능을 활용하면, 자식 모델이 반드시 부모 모델에 속하는지 자동으로 확인할 수 있습니다. scoped 메서드를 사용해 자동 스코핑을 활성화하는 방법은 리소스 라우트 스코핑 섹션을 참고하세요.

얕은 중첩

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

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);

싱글턴 리소스에는 생성 라우트가 등록되지 않으며, 인스턴스가 하나뿐이므로 식별자(ID)도 URI에 포함되지 않습니다.

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 싱글턴 리소스

API에서 사용하는 싱글턴 리소스는 apiSingleton 메서드로 등록하면 createedit 라우트가 자동으로 제외됩니다.

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

API 싱글턴 리소스에 creatable을 적용하면 storedestroy 라우트가 추가됩니다.

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

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

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

모든 메서드에 미들웨어 적용

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

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

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']);

싱글턴 및 API 싱글턴 리소스 컨트롤러에도 동일하게 사용할 수 있습니다.

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

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

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']);

컨트롤러 메서드를 아래와 같이 작성하면 Requestid 파라미터를 모두 사용할 수 있습니다.

<?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월 15일