컨트롤러
번역일: 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가 라우트와 일치하면 UserController의 show 메서드가 호출되고, 라우트 파라미터가 메서드 인자로 전달됩니다.
NOTE
컨트롤러가 반드시 기본 클래스를 상속해야 하는 것은 아닙니다. 다만 기본 클래스를 상속하지 않으면 middleware나 authorize 같은 편의 메서드를 사용할 수 없습니다.
단일 액션 컨트롤러
처리 로직이 특히 복잡한 경우, 하나의 컨트롤러 클래스를 단일 액션 전용으로 만드는 것이 유용할 수 있습니다. 이때는 컨트롤러에 __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 --invokableNOTE
컨트롤러 스텁은 스텁 퍼블리싱을 통해 커스터마이즈할 수 있습니다.
컨트롤러 미들웨어
미들웨어는 라우트 파일에서 컨트롤러 라우트에 직접 지정할 수 있습니다.
Route::get('profile', [UserController::class, 'show'])->middleware('auth');또는 컨트롤러의 생성자에서 middleware 메서드를 사용해 미들웨어를 지정할 수도 있습니다. only나 except를 조합하면 특정 액션에만 선택적으로 적용할 수 있습니다.
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 | /photos | index | photos.index |
| GET | /photos/create | create | photos.create |
| POST | /photos | store | photos.store |
| GET | /photos/{photo} | show | photos.show |
| GET | /photos/{photo}/edit | edit | photos.edit |
| PUT/PATCH | /photos/{photo} | update | photos.update |
| DELETE | /photos/{photo} | destroy | photos.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 옵션을 추가하면 store와 update 메서드를 위한 폼 요청 클래스도 함께 생성됩니다.
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 폼 화면을 제공하는 create와 edit 라우트가 불필요합니다. 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,
]);create와 edit 메서드가 포함되지 않은 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}/comments | index | photos.comments.index |
| GET | /photos/{photo}/comments/create | create | photos.comments.create |
| POST | /photos/{photo}/comments | store | photos.comments.store |
| GET | /comments/{comment} | show | comments.show |
| GET | /comments/{comment}/edit | edit | comments.edit |
| PUT/PATCH | /comments/{comment} | update | comments.update |
| DELETE | /comments/{comment} | destroy | comments.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를 생성합니다. create와 edit 동사를 다른 언어로 현지화하려면 Route::resourceVerbs 메서드를 사용합니다. 이 설정은 애플리케이션의 App\Providers\RouteServiceProvider 내 boot 메서드 시작 부분에서 적용하는 것이 좋습니다.
/**
* 라우트 모델 바인딩, 패턴 필터 등을 정의합니다.
*/
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 | /profile | show | profile.show |
| GET | /profile/edit | edit | profile.edit |
| PUT/PATCH | /profile | update | profile.update |
싱글턴 리소스는 표준 리소스 안에 중첩할 수도 있습니다.
Route::singleton('photos.thumbnail', ThumbnailController::class);이 예시에서 photos 리소스는 표준 리소스 라우트를 모두 가지며, thumbnail 리소스는 다음과 같은 싱글턴 라우트를 갖습니다.
| HTTP 메서드 | URI | 액션 | 라우트 이름 |
|---|---|---|---|
| GET | /photos/{photo}/thumbnail | show | photos.thumbnail.show |
| GET | /photos/{photo}/thumbnail/edit | edit | photos.thumbnail.edit |
| PUT/PATCH | /photos/{photo}/thumbnail | update | photos.thumbnail.update |
생성 가능한 싱글턴 리소스
싱글턴 리소스에 생성 및 저장 라우트가 필요하다면 creatable 메서드를 사용합니다. 이 경우 DELETE 라우트도 함께 등록됩니다.
Route::singleton('photos.thumbnail', ThumbnailController::class)->creatable();| HTTP 메서드 | URI | 액션 | 라우트 이름 |
|---|---|---|---|
| GET | /photos/{photo}/thumbnail/create | create | photos.thumbnail.create |
| POST | /photos/{photo}/thumbnail | store | photos.thumbnail.store |
| GET | /photos/{photo}/thumbnail | show | photos.thumbnail.show |
| GET | /photos/{photo}/thumbnail/edit | edit | photos.thumbnail.edit |
| PUT/PATCH | /photos/{photo}/thumbnail | update | photos.thumbnail.update |
| DELETE | /photos/{photo}/thumbnail | destroy | photos.thumbnail.destroy |
생성/저장 라우트 없이 DELETE 라우트만 등록하려면 destroyable 메서드를 사용합니다.
Route::singleton(...)->destroyable();API 싱글턴 리소스
apiSingleton 메서드를 사용하면 API용 싱글턴 리소스를 등록하면서 create와 edit 라우트를 자동으로 제외합니다.
Route::apiSingleton('profile', ProfileController::class);API 싱글턴 리소스도 creatable을 함께 사용하면 store와 destroy 라우트가 추가됩니다.
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');
}
}