03강: 컨트롤러(Controller) 아키텍처 & HTTP 요청(Request) 해부
실습 프로젝트: 지니샵(JinyShop) 온라인 도서 쇼핑몰
주제: 리소스 컨트롤러(BookController), 단일 액션(__invoke),Request객체로부터 도서 검색/필터 데이터 안전 추출
등장인물: 🐱 지니(시니어 멘토), 👧 도로시(주니어 개발자), 🐶 토토(개발 보조견)
공식 문서 연계: 컨트롤러 (Controllers), 요청 (Requests)
💬 1. 지니와 도로시의 티키타카 회의
👧 도로시: “지니! 라우트 파일(routes/web.php)에 함수 클로저로 코드를 계속 짜다 보니까 파일이 수백 줄로 길어지고 너무 복잡해졌어요! 도서 목록을 필터링하고 검색하는 로직은 따로 깔끔하게 분리할 수 없나요?”
🐱 지니: “당연하지 도로시! 라우트 파일은 도로의 표지판 역할만 해야 한단다. 실제 서점의 주문을 받고 책을 포장하는 주방장 역할은 바로 컨트롤러(Controller)가 담당해야 하지! 라라벨의 리소스 컨트롤러(Resource Controller)를 쓰면 단 한 줄의 명령어로 CRUD(생성, 조회, 수정, 삭제) 7대 표준 메서드가 깔끔하게 정돈된 클래스를 만들어 준단다.”
🐶 토토: “멍멍! 고객이 주소창에 ?category=it&keyword=라라벨&in_stock=1 처럼 다양한 쿼리스트링을 보내올 때, $request->input(), $request->boolean(), $request->filled() 메서드를 쓰면 안전하게 값을 골라낼 수 있다멍!”
🎯 2. 실습 목표 및 서점 시나리오
- 아티산 명령어로 도서 관리 전용 리소스 컨트롤러
BookController를 생성합니다. - 베스트셀러 전용 단일 액션(Invokable) 컨트롤러
BestSellerController를 작성합니다. Request객체를 주입받아 도서 검색 키워드, 카테고리 필터, 재고 유무 플래그를 정밀하게 추출하고 검증합니다.
🛠️ 3. 단계별 실습 절차 (Step-by-Step)
[Step 1] 도서 리소스 컨트롤러 생성
터미널에서 아티산 명령어를 실행하여 7대 CRUD 메서드가 포함된 컨트롤러 뼈대를 생성합니다:
php artisan make:controller BookController --resource
생성된 app/Http/Controllers/BookController.php 파일을 확인하면 다음 표준 액션들이 준비되어 있습니다:
index(): 도서 목록 조회 (검색 및 필터링)create(): 신규 도서 등록 폼 화면store(Request $request): 신규 도서 저장 처리show(string $id): 특정 도서 상세 정보edit(string $id): 도서 정보 수정 폼 화면update(Request $request, string $id): 도서 정보 수정 처리destroy(string $id): 도서 삭제(소프트 삭제) 처리
[Step 2] BookController@index에서 Request 파라미터 정밀 추출
도서 목록 조회 메서드에서 고객이 전달한 검색 조건들을 안전하게 파싱하는 로직을 작성합니다.
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
class BookController extends Controller
{
/**
* 지니샵 도서 카탈로그 목록 (검색 및 필터)
*/
public function index(Request $request): JsonResponse
{
// 1. 문자열 검색어 추출 (공백 트림 및 없을 경우 기본값 null)
$keyword = $request->string('keyword')->trim()->value();
// 2. 카테고리 필터 (존재 여부 확인)
$category = $request->filled('category') ? $request->input('category') : 'all';
// 3. 재고 있는 책만 보기 플래그 (true/false 불리언 자동 변환)
$inStockOnly = $request->boolean('in_stock_only', false);
// 4. 페이지 번호 (정수형 강제 변환)
$page = $request->integer('page', 1);
// 5. 정렬 기준 (인기순, 신간순, 가격순)
$sort = $request->input('sort', 'popular');
return response()->json([
'status' => 'success',
'filters' => [
'keyword' => $keyword,
'category' => $category,
'in_stock_only' => $inStockOnly,
'page' => $page,
'sort' => $sort,
],
'sample_books' => [
['id' => 1, 'title' => '라라벨 13 마스터 클래스', 'author' => '지니', 'price' => 32000],
['id' => 2, 'title' => '도로시의 모던 PHP 탐험', 'author' => '도로시', 'price' => 28000],
],
]);
}
/**
* 특정 도서 상세 보기
*/
public function show(string $id): JsonResponse
{
return response()->json([
'id' => (int) $id,
'title' => '라라벨 13 마스터 클래스',
'isbn' => '9788966261234',
'publisher' => '지니출판사',
'price' => 32000,
'description' => '초보자도 쉽게 따라 할 수 있는 14주 핸드온 라라벨 완벽 가이드북',
]);
}
}
[Step 3] routes/web.php에 리소스 라우트 한 줄 등록
이제 routes/web.php에서 수십 줄의 개별 라우트 대신 Route::resource() 단 한 줄로 연결합니다!
use App\Http\Controllers\BookController;
Route::resource('books', BookController::class);
이 한 줄이 다음 7개의 표준 URL 라우트를 일제히 자동 생성해 줍니다:
php artisan route:list --path=books
| HTTP Method | URI | Controller Action | Route Name |
| :— | :— | :— | :— |
| GET | books | BookController@index | books.index |
| POST | books | BookController@store | books.store |
| GET | books/create | BookController@create | books.create |
| GET | books/{book} | BookController@show | books.show |
| PUT/PATCH | books/{book} | BookController@update | books.update |
| DELETE | books/{book} | BookController@destroy | books.destroy |
| GET | books/{book}/edit | BookController@edit | books.edit |
[Step 4] 단일 액션(Invokable) 컨트롤러 제작
단 하나의 특별한 목적(예: 주간 베스트셀러 10위 집계)만 수행하는 컨트롤러는 __invoke 단일 액션 컨트롤러로 분리하면 코드가 극도로 단순해집니다.
php artisan make:controller BestSellerController --invokable
// app/Http/Controllers/BestSellerController.php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
class BestSellerController extends Controller
{
public function __invoke(Request $request): JsonResponse
{
return response()->json([
'title' => '🔥 지니샵 실시간 베스트셀러 TOP 3',
'ranking' => [
['rank' => 1, 'title' => '라라벨 13 마스터 클래스', 'sales_count' => 1520],
['rank' => 2, 'title' => '도로시의 객체지향 설계 이야기', 'sales_count' => 980],
['rank' => 3, 'title' => '토토와 함께하는 TDD 테스트', 'sales_count' => 840],
],
]);
}
}
routes/web.php에 등록할 때는 메서드 이름 없이 컨트롤러 클래스만 넘기면 됩니다:
use App\Http\Controllers\BestSellerController;
Route::get('/bestsellers', BestSellerController::class)->name('bestsellers');
🔍 4. 라라벨 공식 문서 원리 심층 분석 (Deep Dive)
Request 객체의 타입 캐스팅 헬퍼
- 클라이언트가 폼이나 URL로 전달하는 값은 HTTP 프로토콜 특성상 기본적으로 모두 ‘문자열(String)’입니다.
- 라라벨 13의
Request객체는 개발자가 번거롭게(int) $request->input('page')나filter_var()를 호출하지 않도록,integer(),float(),boolean(),string(),date()등의 자동 타입 변환 헬퍼를 기본 제공합니다. - 이는 타입 안정성(Type Safety)을 극대화하고 예기치 못한 버그를 사전 예방합니다.
🐶 5. 토토의 트러블슈팅 & 주의사항
🐶 토토의 멍멍 가이드!
$request->has('category')vs$request->filled('category')차이가 뭐예요?
고객이 주소창에?category=처럼 빈 값을 보냈을 때,has()는 키가 있으니true를 뱉지만,filled()는 빈 문자열이 아니어야만true를 뱉는다멍! 검색어 필터링에는 꼭filled()를 써야 안전하다멍!
💡 6. 3강 자가진단 과제
- 브라우저에서
http://127.0.0.1:8000/books?keyword=라라벨&category=programming&in_stock_only=true&page=2에 접속하여 컨트롤러가 파라미터들을 올바르게 JSON으로 응답하는지 확인하세요. http://127.0.0.1:8000/bestsellers에 접속하여 Invokable 컨트롤러가 단일 액션으로 동작하는 것을 확인하세요.