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

컨트롤러 (Controllers)

소개

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

컨트롤러 작성하기

기본 컨트롤러

기본 컨트롤러의 예제를 살펴보겠습니다. 컨트롤러는 라라벨에서 제공하는 기본 컨트롤러 클래스인 App\Http\Controllers\Controller를 확장합니다.

<?php

namespace App\Http\Controllers;

use App\Models\User;

class UserController extends Controller
{
/**
* 지정한 사용자의 프로필을 보여줍니다.
*
* @param int $id
* @return \Illuminate\View\View
*/
public function show($id)
{
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] 컨트롤러가 반드시 기본 클래스를 상속해야 하는 것은 아닙니다. 하지만 상속하지 않으면 middlewareauthorize와 같은 편리한 기능을 사용할 수 없습니다.

단일 액션 컨트롤러

컨트롤러의 액션이 특히 복잡하다면, 그 액션만을 위한 별도의 컨트롤러 클래스를 만들 수도 있습니다. 이를 위해 컨트롤러에 __invoke 메서드 하나만 정의하면 됩니다.

<?php

namespace App\Http\Controllers;

use App\Models\User;

class ProvisionServer extends Controller
{
/**
* 새 웹 서버를 프로비저닝합니다.
*
* @return \Illuminate\Http\Response
*/
public function __invoke()
{
// ...
}
}

단일 액션 컨트롤러를 라우트에 등록할 때는 메서드명을 따로 지정하지 않고, 컨트롤러 이름만 전달하면 됩니다.

use App\Http\Controllers\ProvisionServer;

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

make:controller Artisan 명령어에서 --invokable 옵션을 사용하면 단일 액션 컨트롤러를 쉽게 생성할 수 있습니다.

php artisan make:controller ProvisionServer --invokable

[!NOTE] 컨트롤러 스텁은 스텁 커스터마이징을 통해 사용자 정의가 가능합니다.

컨트롤러 미들웨어

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

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

또는, 컨트롤러의 생성자에서 미들웨어를 지정할 수도 있습니다. 컨트롤러 생성자에서 middleware 메서드를 사용하면 컨트롤러의 액션에 미들웨어를 할당할 수 있습니다.

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

컨트롤러에서는 미들웨어를 클로저(익명 함수)로도 등록할 수 있습니다. 이를 통해 컨트롤러 내에서만 사용하는 간단한 미들웨어를 별도의 클래스 없이 정의할 수 있습니다.

$this->middleware(function ($request, $next) {
return $next($request);
});

리소스 컨트롤러

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

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

VerbURI액션라우트 이름
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 HTTP 응답을 반환합니다. 하지만 라우트 정의 시 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 옵션을 추가하면, 저장 및 업데이트 메서드용 폼 리퀘스트 클래스도 자동 생성됩니다.

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에서 사용할 리소스 라우트를 선언할 때는, 일반적으로 create, edit처럼 HTML 템플릿을 제공하는 라우트는 제외하는 것이 일반적입니다. 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();

이 정의는 다음과 같은 라우트를 만듭니다.

VerbURI액션라우트 이름
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 메서드를 사용하면 자동 스코핑과 함께 하위 리소스가 어떤 필드로 조회되는지 지정할 수 있습니다.

use App\Http\Controllers\PhotoCommentController;

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

이렇게 하면, 다음과 같이 접근 가능한 스코프된 중첩 리소스가 등록됩니다.

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

커스텀 키를 사용하는 암묵적 바인딩이 중첩 라우트 파라미터로 사용될 때, 라라벨은 해당 중첩 모델을 상위 모델의 관계를 통해 범위(스코프)로 제한합니다. 즉, 위 예제에서는 Photo 모델이 comments라는 관계를 가지고 있다고 가정하여 Comment 모델을 조회하게 됩니다.

리소스 URI 로컬라이즈

기본적으로 Route::resource는 리소스 URI를 영어 동사와 복수 규칙으로 생성합니다. createedit 같은 액션 동사를 현지화하려면, 애플리케이션의 App\Providers\RouteServiceProvider 내에서 Route::resourceVerbs 메서드를 사용할 수 있습니다. 보통 boot 메서드에 작성합니다.

/**
* 라우트 모델 바인딩, 패턴 필터 등을 정의합니다.
*
* @return void
*/
public function boot()
{
Route::resourceVerbs([
'create' => 'crear',
'edit' => 'editar',
]);

// ...
}

라라벨의 복수화 기능은 여러 언어를 지원하며, 필요에 따라 설정할 수 있습니다. 동사와 복수화 언어를 변경한 뒤에는, 예를 들어 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] 컨트롤러는 한 가지 책임에 집중하도록 설계하는 것이 좋습니다. 만약 자주 기본 리소스 액션 외의 별도 메서드가 필요하다면, 컨트롤러를 더 작고 여러 개로 분리하는 것을 고려해보세요.

싱글턴 리소스 컨트롤러

애플리케이션에 하나의 인스턴스만 존재할 수 있는 리소스도 있을 수 있습니다. 예를 들어 한 사용자의 "프로필"은 하나만 존재하며, 이미지를 대표하는 "썸네일"도 마찬가지입니다. 이처럼 하나만 존재할 수 있는 자원을 "싱글턴 리소스"라고 하며, 이런 경우 "싱글턴" 리소스 컨트롤러를 등록할 수 있습니다.

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

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

위의 싱글턴 리소스 등록은 다음과 같은 라우트를 생성합니다. "생성" 라우트는 등록되지 않으며, 해당 리소스는 한 개만 존재하기 때문에 식별자를 따로 받지 않습니다.

VerbURI액션라우트 이름
GET/profileshowprofile.show
GET/profile/editeditprofile.edit
PUT/PATCH/profileupdateprofile.update

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

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

이 예시에서는, photos 리소스는 표준 리소스 라우트를 모두 가지게 되고, thumbnail은 아래와 같은 싱글턴 리소스로 제공됩니다.

VerbURI액션라우트 이름
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 라우트 또한 등록됩니다.

VerbURI액션라우트 이름
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
{
/**
* 유저 리포지토리 인스턴스.
*/
protected $users;

/**
* 새 컨트롤러 인스턴스를 생성합니다.
*
* @param \App\Repositories\UserRepository $users
* @return void
*/
public function __construct(UserRepository $users)
{
$this->users = $users;
}
}

메서드 주입

생성자 주입 외에도 컨트롤러 메서드의 인자로 의존성을 타입힌트로 지정할 수 있습니다. 가장 일반적인 예로는, 컨트롤러 메서드에서 Illuminate\Http\Request 인스턴스를 주입받는 경우가 많습니다.

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class UserController extends Controller
{
/**
* 새 사용자를 저장합니다.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
$name = $request->name;

//
}
}

컨트롤러 메서드에서 라우트 파라미터의 값도 함께 전달받아야 할 경우, 의존성 인자 뒤에 라우트 인수를 나열하면 됩니다. 예를 들어 라우트가 다음과 같이 정의되어 있다면,

use App\Http\Controllers\UserController;

Route::put('/user/{id}', [UserController::class, 'update']);

컨트롤러 메서드에서 Illuminate\Http\Request와 함께 id 파라미터도 다음과 같이 받을 수 있습니다.

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class UserController extends Controller
{
/**
* 지정된 사용자를 업데이트합니다.
*
* @param \Illuminate\Http\Request $request
* @param string $id
* @return \Illuminate\Http\Response
*/
public function update(Request $request, $id)
{
//
}
}