[LARAVEL] 컨트롤러

김세연·2025년 9월 23일

Laravel

목록 보기
7/14
post-thumbnail

컨트롤러

컨트롤러는 라우터를 통해 들어온 사용자의 요청을 받아서 실질적인 일을 처리하는 총괄 책임자이다.

컨트롤러의 역할을 이해하는 가장 좋은 방법은 레스토랑에 비유하는 것이다.

  • 손님 (사용자): "파스타 주세요." (GET /pasta 요청)

  • 웨이터 (컨트롤러): 주문을 받아서 주방에 전달하고, 완성된 요리를 손님에게 가져다준다.
    웨이터는 직접 요리하지 않는다.

  • 주방장 (모델): 웨이터에게 주문을 받아 재료(데이터베이스)를 가지고 요리(데이터 처리)를 한다.

  • 완성된 요리 (응답): 손님에게 최종적으로 전달되는 파스타(JSON 데이터 또는 웹 페이지)이다.

이처럼 컨트롤러는 요청을 받고, 필요한 담당자(모델)에게 일을 시킨 다음, 그 결과를 받아 최종적으로 사용자에게 전달하는 중간 관리자 역할을 한다.

컨트롤러의 필요성

"라우트 파일(routes/api.php)에 로직을 전부 다 넣으면 안 되나요?"라고 생각할 수 있다.
간단한 로직은 가능하지만, 애플리케이션이 커지면 심각한 문제가 발생한다.

  • 관심사의 분리 (Separation of Concerns):
    라우트 파일은 '주소 정의'라는 한 가지 관심사만 가져야 한다.
    실질적인 로직 처리라는 다른 관심사는 컨트롤러에 위임해야 코드가 깔끔해지고 관리가 쉬워진다.

  • 정리정돈:
    To-Do List 관련 기능은 TaskController에, 사용자 관련 기능은 UserController에 모아둘 수 있다.
    만약 모든 로직이 하나의 파일에 있다면, 코드가 수천 줄이 넘어 원하는 기능을 찾고 수정하기가 매우 어려워진다.


컨트롤러의 구조와 주요 메서드

php artisan make:controller TaskController --api --resource 명령으로 컨트롤러를 만들면, API의 CRUD(생성, 조회, 수정, 삭제) 처리에 필요한 약속된 메서드들이 미리 만들어진다.

  • index(): 전체 목록을 보여줄 때 사용한다. (예: 모든 할 일 조회)

  • store(): 새로운 데이터를 저장할 때 사용한다. (예: 새 할 일 생성)

  • show(): 특정 데이터 하나를 자세히 보여줄 때 사용한다. (예: 1번 할 일 상세 보기)

  • update(): 기존 데이터를 수정할 때 사용한다. (예: 1번 할 일 내용 변경)

  • destroy(): 데이터를 삭제할 때 사용한다. (예: 1번 할 일 삭제)

To-Do List 컨트롤러 전체 예시

app/Http/Controllers/TaskController.php 파일은 다음과 같은 모습을 갖추게 된다.

<?php

namespace App\Http\Controllers;

use App\Models\Task; // 데이터 처리를 위해 Task 모델을 가져온다.
use Illuminate\Http\Request; // 사용자의 요청(입력값 등)을 다루기 위해 필요하다.

class TaskController extends Controller
{
    // GET /api/tasks - 모든 할 일 목록 조회
    public function index()
    {
        // "주방장(Task 모델)에게 모든 메뉴(Task)를 달라고 요청"
        $tasks = Task::all();
        // "완성된 요리(데이터)를 손님에게 전달"
        return response()->json($tasks);
    }

    // POST /api/tasks - 새 할 일 생성
    public function store(Request $request)
    {
        // "손님(Request)의 주문(입력값)을 받아서"
        $validated = $request->validate(['title' => 'required']);
        // "주방장에게 새로운 메뉴를 만들어달라고 요청"
        $task = Task::create($validated);
        // "방금 만든 따끈한 요리를 손님에게 보여줌 (201 Created 상태와 함께)"
        return response()->json($task, 201);
    }

    // GET /api/tasks/{task} - 특정 할 일 상세 조회
    public function show(Task $task) // 라우트 모델 바인딩으로 이미 Task가 찾아져 있음
    {
        // "웨이터가 이미 특정 요리(Task)를 들고 있음. 그대로 손님에게 전달"
        return response()->json($task);
    }

    // PUT /api/tasks/{task} - 특정 할 일 수정
    public function update(Request $request, Task $task)
    {
        // "손님의 새로운 요청사항(수정할 내용)을 받아서"
        $validated = $request->validate(['title' => 'sometimes|required']);
        // "주방장에게 기존 요리를 수정해달라고 요청"
        $task->update($validated);
        // "수정된 요리를 손님에게 전달"
        return response()->json($task);
    }

    // DELETE /api/tasks/{task} - 특정 할 일 삭제
    public function destroy(Task $task)
    {
        // "주방장에게 이 요리를 치워달라고 요청"
        $task->delete();
        // "다 치웠다고 응답 (내용은 없음 - 204 No Content)"
        return response()->json(null, 204);
    }
}

폼 리퀘스트 (Form Request)

storeupdate 메서드에서 유효성 검사($request->validate()) 로직이 길어지면 컨트롤러가 지저분해진다.
폼 리퀘스트는 이 유효성 검사 로직을 별도의 클래스로 분리하는 방법이다.

  • 문제점:
public function store(Request $request)
{
    $validated = $request->validate([
        'title' => 'required|unique:tasks|max:255',
        'content' => 'required|min:10',
        'due_date' => 'nullable|date',
        // ... 유효성 검사 규칙이 10개가 넘는다면?
    ]);
    // ...
}
  • 해결책:

전용 리퀘스트 클래스를 만든다.

php artisan make:request StoreTaskRequest

생성된 app/Http/Requests/StoreTaskRequest.php 파일에 유효성 검사 규칙을 옮긴다.

public function rules()
{
    return [
        'title' => 'required|unique:tasks|max:255',
        'content' => 'required|min:10',
        'due_date' => 'nullable|date',
    ];
}

컨트롤러에서는 Request 대신 방금 만든 StoreTaskRequest를 사용한다.

public function store(StoreTaskRequest $request) // Request -> StoreTaskRequest
{
    // 유효성 검사는 이 메서드가 실행되기 전에 "자동으로" 끝난다.
    // 실패하면 알아서 이전 페이지로 돌려보내준다.
    // 성공한 데이터만 $request->validated() 로 가져올 수 있다.
    Task::create($request->validated());
    // ...
}

장점: 컨트롤러가 매우 깔끔해지며 유효성 검사 로직을 재사용하기도 쉬워진다.


의존성 주입 (Dependency Injection)

이미 public function store(Request $request)show(Task $task)를 사용하면서 의존성 주입을 경험하였다.
컨트롤러가 필요한 객체(의존성)를 직접 만드는 게 아니라, 메서드 파라미터로 요청하면 라라벨이 알아서 넣어주는(주입) 방식을 말한다.

이 개념을 활용하면 Request나 모델뿐만 아니라, 직접 만든 서비스 클래스 등 어떤 클래스든 주입받을 수 있다.

use App\Services\ImageUploadService; // 직접 만든 이미지 업로드 서비스

public function store(StoreTaskRequest $request, ImageUploadService $uploader)
{
    $data = $request->validated();
    
    if ($request->hasFile('image')) {
        // 직접 new ImageUploadService() 를 하지 않아도,
        // 라라벨이 알아서 $uploader 객체를 준비해준다.
        $data['image_path'] = $uploader->store($request->file('image'));
    }
    
    Task::create($data);
    // ...
}

장점: 클래스 간의 결합도를 낮춰 코드를 유연하고 테스트하기 쉽게 만들어준다.


API 리소스 (API Resources)

API를 만들 때, 모델 데이터를 그대로 반환하면(return $task;) 원치 않는 정보(예: password 필드)가 노출되거나, 추가하고 싶은 정보(예: 작성자 이름)를 넣기 어렵다.
API 리소스는 API의 JSON 응답 형식을 자유자재로 제어하게 해준다.

  • 문제점: Task 모델과 관련된 User 모델의 name도 함께 보여주고 싶을 때.
  • 해결책:

리소스 클래스를 만든다.

php artisan make:resource TaskResource

생성된 app/Http/Resources/TaskResource.php 파일에서 응답 형식을 정의한다.

public function toArray($request)
{
    return [
        'id' => $this->id,
        'task_title' => $this->title, // 'title'을 'task_title'로 변경
        'is_done' => $this->completed,
        'created_date' => $this->created_at->format('Y-m-d'), // 날짜 형식 변경
        'author' => new UserResource($this->whenLoaded('user')), // 관계 데이터 추가
    ];
}

컨트롤러에서 이 리소스를 사용해 데이터를 반환한다.

public function show(Task $task)
{
    // return $task; 대신
    return new TaskResource($task);
}

장점: API 응답 구조를 일관되게 관리할 수 있고, 데이터 노출을 제어하며, 원하는 형태로 데이터를 가공하여 보여줄 수 있다.
전문적인 API를 만들기 위한 필수 단계이다.


단일 액션 컨트롤러 (Single Action Controllers)

트롤러가 단 하나의 역할만 수행할 때가 있다.
(예: '사용자 비밀번호 변경', '보고서 생성' 등) 이런 경우 index, store 등 여러 메서드를 가진 컨트롤러는 낭비이다.

이때는 __invoke 라는 특수 메서드 하나만 가진 컨트롤러를 만들 수 있다.

--invokable 옵션으로 컨트롤러를 생성한다.

php artisan make:controller GenerateReportController --invokable

컨트롤러에는 __invoke 메서드만 존재한다.

class GenerateReportController extends Controller
{
    public function __invoke(Request $request)
    {
        // 보고서를 생성하는 로직...
    }
}

라우트에서는 메서드 이름을 지정할 필요가 없다.

Route::post('/reports', GenerateReportController::class);

장점: 컨트롤러의 역할이 매우 명확해지고 코드가 간결해진다.


결론

컨트롤러는 라라벨 애플리케이션의 '두뇌'이자 '심장'이다.
외부로부터 들어온 모든 요청을 가장 먼저 받아 분석하고, 적절한 담당자에게 일을 분배하며, 최종 결과를 취합하여 사용자에게 보여주는 총괄 지휘관(Orchestrator)의 역할을 수행한다.

컨트롤러의 핵심 임무

  • 요청의 접수:
    라우터(안내 데스크)로부터 사용자의 요청(HTTP Request)을 전달받는다.

  • 유효성 검사:
    폼 리퀘스트(경호원)를 통해 사용자의 입력값이 올바른지 검증한다.

  • 로직 위임:
    핵심 비즈니스 로직을 직접 처리하기보다는, 모델(주방장)이나 서비스 클래스(전문가)에게 작업을 위임한다.

  • 응답 반환:
    처리된 결과를 API 리소스(스타일리스트)를 통해 깔끔하게 포장하여 사용자에게 최종적으로 반환한다.

좋은 컨트롤러의 조건

훌륭한 컨트롤러는 모든 일을 직접 하지 않는다.

  • 날씬하다 (Thin):
    복잡한 비즈니스 로직은 모델이나 서비스 클래스에 위임하고, 자신은 그 흐름을 관리하는 역할에만 집중한다.

  • 집중되어 있다 (Focused):
    하나의 컨트롤러는 하나의 책임(예: UserController는 오직 사용자에 관련된 처리만)을 진다.

  • 안전하다 (Secure):
    미들웨어와 폼 리퀘스트를 통해 들어오는 모든 요청을 철저히 검사하고 보호한다.

컨트롤러는 단순히 코드의 묶음이 아니라, 라우트, 모델, 뷰 등 라라벨의 모든 구성 요소를 한데 묶어 생명력 있는 애플리케이션으로 만들어주는 핵심적인 접착제이다.
컨트롤러의 흐름을 이해했다는 것은 라라벨 애플리케이션이 어떻게 생각하고 동작하는지에 대한 본질을 파악했다는 의미와 같다.

profile
공부 재밌따

0개의 댓글