본문으로 건너뛰기
버전: 11.x

컨트롤러 (Controllers)

소개

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

컨트롤러 작성하기

기본 컨트롤러

새 컨트롤러를 빠르게 생성하려면 make:controller Artisan 명령어를 사용할 수 있습니다. 기본적으로 애플리케이션의 모든 컨트롤러는 app/Http/Controllers 디렉터리에 저장됩니다.

php artisan make:controller UserController

기본적인 컨트롤러 예시를 살펴보겠습니다. 컨트롤러는 여러 개의 public 메서드를 가질 수 있으며, 각각은 들어오는 HTTP 요청에 응답합니다.

<?php

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\View\View;

class UserController extends Controller
{
/**
* Show the profile for a given user.
*/
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\UserController 클래스의 show 메서드가 호출되고, 라우트 파라미터가 해당 메서드에 전달됩니다.

[!NOTE]
컨트롤러는 반드시 특정 베이스 클래스를 상속받을 필요는 없습니다. 하지만 여러 컨트롤러에서 공통적으로 사용할 메서드를 베이스 컨트롤러 클래스에 작성해두면 관리가 편리할 수 있습니다.

단일 액션 컨트롤러

특정 컨트롤러의 동작이 특히 복잡하다면, 그 동작을 하나의 컨트롤러 클래스에 전담시키는 방식을 쓸 수 있습니다. 이를 위해 컨트롤러에 단 하나의 __invoke 메서드를 정의하면 됩니다.

<?php

namespace App\Http\Controllers;

class ProvisionServer extends Controller
{
/**
* Provision a new web server.
*/
public function __invoke()
{
// ...
}
}

단일 액션 컨트롤러의 라우트를 등록할 때는, 컨트롤러 메서드 이름을 따로 지정하지 않아도 됩니다. 대신 컨트롤러 클래스 이름만 전달하면 됩니다.

use App\Http\Controllers\ProvisionServer;

Route::post('/server', ProvisionServer::class);

make:controller Artisan 명령어에서 --invokable 옵션을 사용하면 바로 호출 가능한(invokable) 컨트롤러를 빠르게 생성할 수 있습니다.

php artisan make:controller ProvisionServer --invokable

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

컨트롤러 미들웨어

미들웨어는 라우트 파일에서 컨트롤러의 라우트에 할당할 수 있습니다.

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

또는, 컨트롤러 클래스 안에서 미들웨어를 지정할 수도 있습니다. 이 경우, 컨트롤러가 HasMiddleware 인터페이스를 구현해야 하며, 이 인터페이스는 컨트롤러에 static middleware 메서드를 요구합니다. 이 메서드 내에서 컨트롤러의 액션에 적용할 미들웨어 배열을 반환할 수 있습니다.

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use Illuminate\Routing\Controllers\HasMiddleware;
use Illuminate\Routing\Controllers\Middleware;

class UserController extends Controller implements HasMiddleware
{
/**
* Get the middleware that should be assigned to the controller.
*/
public static function middleware(): array
{
return [
'auth',
new Middleware('log', only: ['index']),
new Middleware('subscribed', except: ['store']),
];
}

// ...
}

컨트롤러 미들웨어를 클로저(Closure)로 정의할 수도 있습니다. 이 방법을 사용하면 별도의 미들웨어 클래스를 만들지 않고도 인라인 미들웨어를 빠르게 작성할 수 있습니다.

use Closure;
use Illuminate\Http\Request;

/**
* Get the middleware that should be assigned to the controller.
*/
public static function middleware(): array
{
return [
function (Request $request, Closure $next) {
return $next($request);
},
];
}

[!WARNING]
Illuminate\Routing\Controllers\HasMiddleware를 구현하는 컨트롤러는 Illuminate\Routing\Controller를 상속받아서는 안 됩니다.

리소스 컨트롤러

애플리케이션에서 각 Eloquent 모델을 "리소스"라고 생각한다면, 보통 각 리소스에 대해 동일한 세트의 작업을 수행하게 됩니다. 예를 들어, Photo 모델과 Movie 모델이 있다면, 사용자는 이 리소스들을 생성, 조회, 수정, 삭제할 수 있습니다.

이런 일반적인 상황을 위해, 라라벨의 리소스 라우팅은 대표적인 생성, 조회, 수정, 삭제(CRUD) 라우트를 단 한 줄의 코드로 컨트롤러에 할당할 수 있습니다. 먼저, make:controller Artisan 명령어의 --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,
]);

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

메서드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');
});

소프트 삭제(Soft Delete) 모델

기본적으로, 암묵적 모델 바인딩은 소프트 삭제된 모델을 조회하지 않고 404 HTTP 응답을 반환합니다. 그러나, 라우트를 정의할 때 withTrashed 메서드를 사용하면 소프트 삭제된 모델도 함께 조회할 수 있습니다.

use App\Http\Controllers\PhotoController;

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

인수를 지정하지 않고 withTrashed를 호출하면, show, edit, update 리소스 라우트에서 소프트 삭제된 모델을 허용합니다. 배열을 전달하면 허용할 라우트만 골라서 설정할 수도 있습니다.

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

리소스 모델 지정하기

라우트 모델 바인딩을 사용하는 경우, 컨트롤러 메서드에서 모델 인스턴스를 타입-힌트(type-hint)로 사용할 수 있습니다. 이를 위해 컨트롤러를 생성할 때 --model 옵션을 사용할 수 있습니다.

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

폼 리퀘스트 클래스 자동 생성

리소스 컨트롤러 생성 시 --requests 옵션을 추가하면, 저장 및 수정 액션에 대한 폼 리퀘스트 클래스도 함께 생성됩니다.

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에서 사용할 리소스 라우트는 템플릿을 반환하는 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,
]);

make:controller 명령어에서 --api 옵션을 활용하면, createedit 메서드가 없는 API 리소스 컨트롤러를 바로 생성할 수 있습니다.

php artisan make:controller PhotoController --api

중첩 리소스

상황에 따라 중첩된 리소스에 대한 라우트가 필요할 수 있습니다. 예를 들어, 포토(photo) 리소스에 여러 개의 코멘트(comment)가 달릴 수 있습니다. "점(dot) 표기법"을 사용해 중첩된 리소스 컨트롤러를 등록할 수 있습니다.

use App\Http\Controllers\PhotoCommentController;

Route::resource('photos.comments', PhotoCommentController::class);

이렇게 등록하면 아래와 같은 형태로 중첩된 리소스 접근이 가능합니다.

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

중첩 리소스 스코프 지정

라라벨의 암묵적 모델 바인딩 기능은 중첩된 모델 바인딩시 자식 모델이 반드시 부모 모델에 속해 있는지 자동으로 확인할 수 있습니다. 중첩 리소스를 선언할 때 scoped 메서드를 사용하면 이 기능을 활성화할 수 있으며, 자식 리소스를 어떤 필드로 가져올지 지정할 수도 있습니다. 자세한 사용법은 리소스 라우트의 스코프 적용 문서를 참고하세요.

얕은 중첩(Shallow Nesting)

실제로는 URI에 부모와 자식의 ID를 모두 포함시키지 않아도 될 때가 있습니다. 예를 들어 자식의 ID(주로 증가형 기본 키)가 유니크하다면, "얕은 중첩(shallow nesting)"을 사용할 수 있습니다.

use App\Http\Controllers\CommentController;

Route::resource('photos.comments', CommentController::class)->shallow();

이렇게 하면 다음과 같은 라우트가 정의됩니다.

메서드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}

리소스 라우트 스코프 적용

라라벨의 scoped 암묵적 모델 바인딩 기능으로, 중첩된 바인딩에서 자식 모델이 부모 모델에 속해 있는지 자동으로 확인할 수 있습니다. 중첩 리소스를 선언할 때 scoped 메서드를 사용하면 자동 스코핑을 활성화하고, 자식 리소스를 어떤 필드로 검색할지 지정할 수 있습니다.

use App\Http\Controllers\PhotoCommentController;

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

이렇게 하면 아래와 같은 URL에서 스코프가 적용된 중첩 리소스를 조회할 수 있습니다.

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

커스텀 키가 적용된 암묵적 바인딩을 중첩 라우트 파라미터로 사용할 때, 라라벨은 부모 모델의 연관관계 명(위 예시에서는 route 파라미터 이름의 복수형, 즉 comments)로 자식 모델을 검색하는 쿼리를 자동으로 스코프합니다.

리소스 URI 현지화

기본적으로 Route::resource는 영어 동사와 복수 규칙을 따릅니다. 만약 createedit 등 액션 동사를 현지화해야 한다면, Route::resourceVerbs 메서드를 사용할 수 있습니다. 이 설정은 애플리케이션의 App\Providers\AppServiceProviderboot 메서드 시작 부분에 넣어줄 수 있습니다.

/**
* Bootstrap any application services.
*/
public function boot(): void
{
Route::resourceVerbs([
'create' => 'crear',
'edit' => 'editar',
]);
}

라라벨의 복수화 기능은 여러 언어를 지원하므로, 필요에 따라 언어를 지정하여 사용할 수 있습니다. 동사와 복수화 언어를 커스터마이즈한 경우, 예를 들어 Route::resource('publicacion', PublicacionController::class)로 등록하면 다음과 같은 URI가 생성됩니다.

/publicacion/crear

/publicacion/{publicaciones}/editar

리소스 컨트롤러 확장

기본 리소스 라우트 외에 추가로 라우트를 더 하고 싶다면, Route::resource 메서드보다 먼저 해당 supplemental(보조) 라우트를 정의해야 합니다. 그렇지 않으면, resource 메서드에서 생성된 라우트가 보조 라우트보다 우선시될 수 있습니다.

use App\Http\Controller\PhotoController;

Route::get('/photos/popular', [PhotoController::class, 'popular']);
Route::resource('photos', PhotoController::class);

[!NOTE]
컨트롤러의 역할이 너무 커지지 않게 주의하세요. 리소스 액션 이외의 메서드를 자주 추가하게 된다면, 컨트롤러를 더 작고 역할이 명확한 두 개 이상의 컨트롤러로 분리하는 것이 좋습니다.

싱글턴 리소스 컨트롤러

애플리케이션에서 한 인스턴스만 존재할 수 있는 리소스가 있을 수 있습니다. 예를 들어, 사용자의 "프로필(profile)" 같은 것은 한 명의 사용자가 여러 개를 가질 수 없습니다. 마찬가지로 이미지에 "썸네일(thumbnail)"이 하나만 있을 수 있습니다. 이러한 경우를 "싱글턴 리소스"라고 하며, 해당 리소스는 오직 하나의 인스턴스만 존재합니다. 이처럼 한 개만 존재하는 리소스를 위해 "싱글턴" 리소스 컨트롤러를 등록할 수 있습니다.

use App\Http\Controllers\ProfileController;
use Illuminate\Support\Facades\Route;

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

위의 싱글턴 리소스 정의는 다음과 같은 라우트를 등록합니다. "생성" 관련 라우트는 등록되지 않으며, 라우트가 식별자를 요구하지 않습니다. 왜냐하면 오직 하나의 인스턴스만 존재하기 때문입니다.

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

싱글턴 리소스는 일반 리소스 내에 중첩시킬 수도 있습니다.

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

이 경우, photos 리소스에는 기본 리소스 라우트가 모두 등록되는 한편, thumbnail에는 아래와 같은 싱글턴 리소스 라우트만 추가됩니다.

메서드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) 라우트도 함께 등록되는 것을 볼 수 있습니다.

메서드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();

의존성 주입과 컨트롤러

생성자(컨스트럭터) 주입

라라벨의 서비스 컨테이너는 모든 컨트롤러를 자동으로 해결(resolve)합니다. 따라서, 컨트롤러의 생성자에서 필요한 의존성을 타입-힌트로 선언하면 자동으로 주입됩니다.

<?php

namespace App\Http\Controllers;

use App\Repositories\UserRepository;

class UserController extends Controller
{
/**
* Create a new controller instance.
*/
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
{
/**
* Store a new user.
*/
public function store(Request $request): RedirectResponse
{
$name = $request->name;

// Store the user...

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
{
/**
* Update the given user.
*/
public function update(Request $request, string $id): RedirectResponse
{
// Update the user...

return redirect('/users');
}
}