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

컨트롤러 (Controllers)

소개

모든 요청 처리 로직을 라우트 파일에서 클로저로 정의하는 대신, "컨트롤러" 클래스를 사용해 이러한 동작을 체계적으로 관리할 수 있습니다. 컨트롤러는 관련된 요청 처리 로직을 하나의 클래스에 모아둘 수 있습니다. 예를 들어, UserController 클래스가 사용자의 조회, 생성, 수정, 삭제 등과 같이 사용자와 관련된 모든 요청을 처리하도록 할 수 있습니다. 기본적으로 컨트롤러는 app/Http/Controllers 디렉토리에 저장됩니다.

컨트롤러 작성

기본 컨트롤러

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

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
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 메서드가 호출되고, 라우트 파라미터가 해당 메서드에 전달됩니다.

[!TIP] 컨트롤러는 반드시 기본 클래스를 상속할 필요는 없습니다. 그러나 기본 클래스를 상속하지 않으면 middleware, authorize와 같은 편리한 기능을 사용할 수 없습니다.

단일 액션 컨트롤러

컨트롤러에서 처리하는 액션이 아주 복잡하다면, 해당 액션만을 위한 전용 컨트롤러 클래스를 만드는 것이 편할 수 있습니다. 이럴 때는 컨트롤러 안에 __invoke 메서드만 하나 정의하면 됩니다.

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
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

[!TIP] 컨트롤러 스텁은 스텁 게시를 통해 커스터마이즈 할 수 있습니다.

컨트롤러 미들웨어

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

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

리소스 모델 지정

라우트 모델 바인딩을 활용하며, 리소스 컨트롤러의 메서드에서 모델 인스턴스를 타입힌트로 지정하고 싶다면, 컨트롤러를 생성할 때 --model 옵션을 사용할 수 있습니다.

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

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

리소스 컨트롤러를 생성할 때 --requests 옵션도 함께 주면, 컨트롤러의 저장 및 수정 메서드에서 사용할 폼 리퀘스트 클래스가 Artisan에 의해 자동 생성됩니다.

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처럼 HTML 템플릿을 제공하는 라우트를 보통 제외하게 됩니다. 이런 경우를 위해 apiResource 메서드를 사용하면 두 라우트가 자동으로 빠집니다.

use App\Http\Controllers\PhotoController;

Route::apiResource('photos', PhotoController::class);

아래와 같이 여러 API 리소스 컨트롤러도 한 번에 등록할 수 있습니다.

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

중첩 리소스

경우에 따라 중첩된 리소스에 대한 라우트가 필요할 수 있습니다. 예를 들어, 포토 리소스에는 여러 개의 댓글이 달릴 수 있습니다. 이런 중첩 리소스 컨트롤러를 라우트 선언에서 "점" 표기법으로 정의할 수 있습니다.

use App\Http\Controllers\PhotoCommentController;

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

이 라우트는 다음과 같은 URI로 중첩 리소스를 접근할 수 있게 됩니다.

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

중첩 리소스 스코핑

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

얕은 중첩(Shallow Nesting)

일반적으로, 자식 리소스의 ID가 이미 고유한 경우 URI에 부모와 자식 ID를 모두 포함할 필요가 없습니다. 예를 들어, 오토 인크리먼트된 기본 키 등 고유 식별자를 URI 세그먼트로 사용하는 경우, "얕은(Shallow) 중첩"을 선택할 수 있습니다.

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 클래스의 boot 메서드 초입에서 Route::resourceVerbs 메서드를 사용할 수 있습니다.

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

// ...
}

동사를 커스터마이징 한 뒤, 예를 들어 Route::resource('fotos', PhotoController::class)를 등록하면, 아래와 같은 URI가 생성됩니다.

/fotos/crear

/fotos/{foto}/editar

리소스 컨트롤러 보완

기본 리소스 라우트 외에 추가 라우트가 필요하다면, 반드시 Route::resource 호출 이전에 보조 라우트를 정의해야 합니다. 그렇지 않으면 resource 메서드가 생성하는 라우트가 직접 정의한 추가 라우트보다 우선 적용될 수 있습니다.

use App\Http\Controller\PhotoController;

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

[!TIP] 컨트롤러의 책임을 명확히 하세요. 만약 리소스 기본 액션 외의 메서드가 자주 필요하다면, 컨트롤러를 더 작고 목적별로 분리하는 것이 좋습니다.

의존성 주입과 컨트롤러

생성자 인젝션

라라벨의 서비스 컨테이너는 모든 컨트롤러를 자동으로 resolve(해결)합니다. 따라서, 컨트롤러의 생성자에 필요한 의존성을 타입힌트로 지정하면 라라벨이 자동으로 주입해 줍니다. 아래 예제를 참고하세요.

<?php

namespace App\Http\Controllers;

use App\Repositories\UserRepository;

class UserController extends Controller
{
/**
* UserRepository 인스턴스입니다.
*/
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)
{
//
}
}