컨트롤러

번역일: 2026년 6월 27일

컨트롤러

소개

요청 처리 로직을 라우트 파일의 클로저로만 정의하다 보면 코드가 금세 복잡해집니다. 컨트롤러를 사용하면 관련된 요청 처리 로직을 하나의 클래스로 묶어 체계적으로 관리할 수 있습니다. 예를 들어 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가 라우트와 일치하면 UserControllershow 메서드가 호출되고, 라우트 파라미터가 메서드 인자로 전달됩니다.

NOTE

컨트롤러가 반드시 기본 클래스를 상속해야 하는 것은 아닙니다. 다만 기본 클래스를 상속하지 않으면 middlewareauthorize 같은 편의 메서드를 사용할 수 없습니다.

단일 액션 컨트롤러

처리 로직이 특히 복잡한 경우, 하나의 컨트롤러 클래스를 단일 액션 전용으로 만드는 것이 유용할 수 있습니다. 이때는 컨트롤러에 __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

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

컨트롤러 미들웨어

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

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

또는 컨트롤러의 생성자에서 middleware 메서드를 사용해 미들웨어를 지정할 수도 있습니다. onlyexcept를 조합하면 특정 액션에만 선택적으로 적용할 수 있습니다.

class UserController extends Controller { /** * 새 컨트롤러 인스턴스를 생성합니다. */ public function __construct() { $this->middleware('auth'); $this->middleware('log')->only('index'); $this->middleware('subscribed')->except('store'); } }

클로저를 사용해 인라인 미들웨어를 정의하는 것도 가능합니다. 별도의 미들웨어 클래스를 만들지 않고 해당 컨트롤러에서만 사용할 간단한 로직을 넣을 때 유용합니다.

use Closure; use Illuminate\Http\Request; $this->middleware(function (Request $request, Closure $next) { return $next($request); });

리소스 컨트롤러

애플리케이션의 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);

이 한 줄의 선언이 리소스의 다양한 액션을 처리하는 여러 라우트를 한 번에 등록합니다. route:list Artisan 명령어로 등록된 라우트를 언제든지 확인할 수 있습니다.

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

Route::resources([ '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 메서드를 사용하면 이 동작을 원하는 대로 바꿀 수 있습니다. 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

부분 리소스 라우트

리소스 라우트 등록 시 기본 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 리소스 컨트롤러를 한 번에 등록하려면 apiResources를 사용합니다.

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 메서드를 사용하면 자동 스코핑을 활성화하고 자식 리소스를 어떤 필드로 조회할지 지정할 수 있습니다. 자세한 내용은 리소스 라우트 스코핑 문서를 참고하세요.

얕은 중첩(Shallow Nesting)

자식 리소스의 ID가 이미 고유한 식별자(예: 자동 증가 기본 키)라면 URI에 부모와 자식 ID를 모두 포함시킬 필요가 없습니다. 이런 경우 shallow 메서드로 얕은 중첩을 사용할 수 있습니다.

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\RouteServiceProviderboot 메서드 시작 부분에서 적용하는 것이 좋습니다.

/** * 라우트 모델 바인딩, 패턴 필터 등을 정의합니다. */ 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\Controllers\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);

싱글턴 리소스는 인스턴스가 하나이므로 생성(creation) 라우트는 등록되지 않으며, 식별자도 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 메서드를 사용합니다. 이 경우 DELETE 라우트도 함께 등록됩니다.

Route::singleton('photos.thumbnail', ThumbnailController::class)->creatable();
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 메서드를 사용하면 API용 싱글턴 리소스를 등록하면서 createedit 라우트를 자동으로 제외합니다.

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

API 싱글턴 리소스도 creatable을 함께 사용하면 storedestroy 라우트가 추가됩니다.

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

의존성 주입과 컨트롤러

생성자 주입

Laravel의 서비스 컨테이너는 모든 컨트롤러를 자동으로 해석합니다. 덕분에 컨트롤러 생성자에 필요한 의존성을 타입힌트로 선언하기만 하면 자동으로 주입됩니다.

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

컨트롤러 메서드는 아래처럼 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년 6월 27일