컨트롤러
번역일: 2026년 7월 2일
컨트롤러
소개
라우트 파일에 모든 요청 처리 로직을 클로저로 정의하는 대신, 컨트롤러 클래스를 사용해 관련 로직을 하나의 클래스로 묶을 수 있습니다. 예를 들어 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
컨트롤러가 반드시 기본 클래스를 상속해야 하는 것은 아닙니다. 다만, 여러 컨트롤러에서 공통으로 사용할 기능이 있다면 기본 컨트롤러 클래스를 만들고 상속하는 방식이 편리할 수 있습니다.
단일 액션 컨트롤러
특정 액션이 복잡하여 컨트롤러 하나를 통째로 그 역할에만 전담시키고 싶을 때는 __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');또는 컨트롤러 클래스 내부에서 미들웨어를 지정하는 방법도 있습니다. 이 경우 컨트롤러가 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);
},
];
}WARNING
Illuminate\Routing\Controllers\HasMiddleware를 구현하는 컨트롤러는 Illuminate\Routing\Controller를 상속하면 안 됩니다.
리소스 컨트롤러
Eloquent 모델 각각을 하나의 "리소스"로 본다면, 보통 리소스마다 생성·조회·수정·삭제(CRUD) 작업이 반복됩니다. Laravel의 리소스 라우팅은 이러한 CRUD 라우트를 단 한 줄로 등록할 수 있게 해줍니다.
--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,
]);리소스 컨트롤러가 처리하는 액션
| 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일부 리소스 라우트만 사용하기
기본 CRUD 액션 중 일부만 필요하다면 only 또는 except 메서드로 등록할 라우트를 제한할 수 있습니다.
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와 자식 ID를 모두 포함할 필요가 없을 때도 있습니다. 자동 증가 기본 키처럼 고유 식별자를 사용하는 경우, "얕은 중첩"을 활용할 수 있습니다.
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 동사를 현지화하려면 App\Providers\AppServiceProvider의 boot 메서드 초반에 Route::resourceVerbs를 호출하세요.
/**
* 애플리케이션 서비스를 부트스트랩합니다.
*/
public function boot(): void
{
Route::resourceVerbs([
'create' => 'crear',
'edit' => 'editar',
]);
}Laravel의 복수형 처리기는 여러 언어를 지원합니다. 동사와 복수형 언어 설정을 마치면 Route::resource('publicacion', PublicacionController::class)와 같은 라우트 등록 시 아래와 같은 URI가 생성됩니다.
/publicacion/crear
/publicacion/{publicaciones}/editar리소스 컨트롤러에 라우트 추가하기
리소스 라우트 기본 세트 외에 추가 라우트가 필요하다면, Route::resource보다 먼저 해당 라우트를 정의해야 합니다. 그렇지 않으면 리소스 메서드가 등록한 라우트가 추가 라우트를 의도치 않게 덮어쓸 수 있습니다.
use App\Http\Controller\PhotoController;
Route::get('/photos/popular', [PhotoController::class, 'popular']);
Route::resource('photos', PhotoController::class);NOTE
컨트롤러는 단일 책임에 집중하도록 유지하세요. 리소스 액션 외의 메서드가 반복적으로 필요하다면, 컨트롤러를 두 개의 작은 컨트롤러로 분리하는 것을 고려하세요.
싱글톤 리소스 컨트롤러
애플리케이션에 따라서는 인스턴스가 단 하나뿐인 리소스가 있을 수 있습니다. 예를 들어 사용자의 "프로필"은 수정할 수 있지만 하나만 존재하며, 이미지에도 "썸네일"이 하나만 있을 수 있습니다. 이런 리소스를 "싱글톤 리소스"라고 하며, Route::singleton으로 등록합니다.
use App\Http\Controllers\ProfileController;
use Illuminate\Support\Facades\Route;
Route::singleton('profile', ProfileController::class);싱글톤 리소스는 인스턴스가 하나뿐이므로 생성 라우트는 등록되지 않으며, 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 메서드를 사용하세요.
Route::singleton('photos.thumbnail', ThumbnailController::class)->creatable();이 경우 다음 라우트가 등록됩니다. 생성 가능한 싱글톤 리소스에는 DELETE 라우트도 함께 등록됩니다.
| 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에서 사용되는 싱글톤 리소스를 등록할 수 있습니다. HTML 페이지를 반환하는 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']);컨트롤러 메서드는 다음과 같이 작성합니다.
<?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');
}
}