Laravel과 PHP 면접 질문: 2026년 핵심 25선
Laravel과 PHP 면접에서 가장 자주 출제되는 25가지 질문을 상세히 다룹니다. Eloquent ORM, 미들웨어, 큐, 테스트, 아키텍처 패턴에 대한 상세한 답변과 코드 예제를 제공합니다.

Laravel 면접에서는 PHP의 대표적인 프레임워크에 대한 숙련도, Eloquent ORM의 이해, MVC 아키텍처에 대한 지식, 그리고 견고하고 유지보수 가능한 애플리케이션을 구축하는 능력이 평가됩니다. 본 가이드에서는 Laravel 기초부터 프로덕션 배포 패턴까지, 가장 빈출되는 25가지 질문을 다룹니다.
Laravel의 아키텍처적 설계 결정을 설명할 수 있는 지원자는 면접관으로부터 높은 평가를 받습니다. 프레임워크가 특정 규칙을 채택한 이유(설정보다 규칙, 서비스 컨테이너)를 이해하는 것이 면접에서 큰 차별화 요소가 됩니다.
Laravel 기초
질문 1: Laravel의 리퀘스트 라이프사이클을 설명하십시오
Laravel의 리퀘스트 라이프사이클은 컨트롤러에 도달하기 전에 여러 레이어를 거칩니다. 이 사이클을 이해하는 것은 디버깅과 성능 최적화에 필수적입니다.
// Entry point for all HTTP requests
require __DIR__.'/../vendor/autoload.php';
// Load the Laravel application
$app = require_once __DIR__.'/../bootstrap/app.php';
// The HTTP kernel handles the request
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
$response = $kernel->handle(
$request = Illuminate\Http\Request::capture()
);
$response->send();
$kernel->terminate($request, $response);전체 사이클은 다음과 같습니다: index.php → 오토로드 → 부트스트랩 → 서비스 프로바이더 → 미들웨어 → 라우팅 → 컨트롤러 → 응답 → 터미네이트. 각 단계는 인터셉트하여 커스터마이징할 수 있습니다.
질문 2: 서비스 컨테이너란 무엇이며 의존성 주입은 어떻게 동작하는가
서비스 컨테이너는 Laravel의 핵심입니다. 클래스 인스턴스화를 관리하고 생성자 주입을 통해 의존성을 자동으로 해결합니다.
// Service with automatically injected dependencies
namespace App\Services;
use App\Contracts\PaymentGatewayInterface;
use App\Repositories\OrderRepository;
use Illuminate\Support\Facades\Log;
class PaymentService
{
public function __construct(
private PaymentGatewayInterface $gateway, // Interface resolved by container
private OrderRepository $orders // Concrete class auto-resolved
) {}
public function processPayment(int $orderId, float $amount): bool
{
$order = $this->orders->find($orderId);
try {
$result = $this->gateway->charge($amount, $order->customer);
$order->markAsPaid($result->transactionId);
return true;
} catch (PaymentException $e) {
Log::error('Payment failed', ['order' => $orderId, 'error' => $e->getMessage()]);
return false;
}
}
}// Binding an interface to a concrete implementation
public function register(): void
{
// Simple binding: new instance on each injection
$this->app->bind(
PaymentGatewayInterface::class,
StripeGateway::class
);
// Singleton: same instance shared everywhere
$this->app->singleton(
CacheService::class,
fn($app) => new CacheService($app['config']['cache.driver'])
);
}의존성 주입은 클래스 간의 결합도를 낮추고 모킹을 통한 단위 테스트를 용이하게 합니다.
질문 3: 파사드와 의존성 주입의 차이점은 무엇인가
파사드는 컨테이너 서비스에 정적 문법을 제공하고, 의존성 주입은 의존 관계를 명시적으로 만듭니다.
// Using Facades: concise syntax but implicit dependencies
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
class ReportController extends Controller
{
public function generate()
{
// Facades: static access to services
$data = Cache::remember('report_data', 3600, fn() => $this->fetchData());
Log::info('Report generated');
return view('report', compact('data'));
}
}
// Dependency Injection: explicit and testable dependencies
use Illuminate\Contracts\Cache\Repository as CacheContract;
use Psr\Log\LoggerInterface;
class ReportController extends Controller
{
public function __construct(
private CacheContract $cache,
private LoggerInterface $logger
) {}
public function generate()
{
// Same functionality, explicit dependencies
$data = $this->cache->remember('report_data', 3600, fn() => $this->fetchData());
$this->logger->info('Report generated');
return view('report', compact('data'));
}
}테스트 용이성 측면에서 비즈니스 클래스에는 의존성 주입이 권장됩니다. 파사드는 헬퍼나 일회성 코드에 적합합니다.
질문 4: Laravel의 서비스 프로바이더는 어떻게 동작하는가
서비스 프로바이더는 애플리케이션 설정의 중심점입니다. 각 프로바이더는 서비스 등록, 바인딩 구성, 컴포넌트 초기화를 담당합니다.
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Services\PaymentService;
use App\Contracts\PaymentGatewayInterface;
use App\Gateways\StripeGateway;
class PaymentServiceProvider extends ServiceProvider
{
// Register method: bindings and registrations
// Don't access other services here (not yet loaded)
public function register(): void
{
$this->app->singleton(PaymentGatewayInterface::class, function ($app) {
return new StripeGateway(
config('services.stripe.key'),
config('services.stripe.secret')
);
});
}
// Boot method: initialization after all providers
// Full access to all application services
public function boot(): void
{
// Register macros, event listeners, routes, etc.
$this->loadRoutesFrom(__DIR__.'/../routes/payment.php');
$this->loadViewsFrom(__DIR__.'/../resources/views', 'payment');
// Publish files for packages
$this->publishes([
__DIR__.'/../config/payment.php' => config_path('payment.php'),
], 'payment-config');
}
}실행 순서는 모든 register() 메서드가 먼저 실행된 후, 모든 boot() 메서드가 실행됩니다. 이 순서를 통해 부트 시점에 의존성이 사용 가능하다는 것이 보장됩니다.
Eloquent ORM
질문 5: Eloquent 관계(Relationships)와 그 차이점을 설명하십시오
Eloquent는 테이블 간 연관을 모델링하기 위한 여러 관계 유형을 제공합니다. 각 유형에는 특정 사용 사례가 있습니다.
class User extends Model
{
// A user has one profile (1:1)
public function profile(): HasOne
{
return $this->hasOne(Profile::class);
}
// A user has many articles (1:N)
public function articles(): HasMany
{
return $this->hasMany(Article::class);
}
// A user has many roles via pivot table (N:N)
public function roles(): BelongsToMany
{
return $this->belongsToMany(Role::class)
->withPivot('assigned_at') // Additional pivot columns
->withTimestamps(); // created_at/updated_at on pivot
}
// A user has many comments through articles (HasManyThrough)
public function comments(): HasManyThrough
{
return $this->hasManyThrough(
Comment::class, // Final model
Article::class // Intermediate model
);
}
}
// app/Models/Article.php
class Article extends Model
{
// An article belongs to a user (inverse of hasMany)
public function author(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
// Polymorphic relationship: an article can have tags, like other models
public function tags(): MorphToMany
{
return $this->morphToMany(Tag::class, 'taggable');
}
}다형성 관계(morphOne, morphMany, morphToMany)는 하나의 모델이 단일 관계를 통해 여러 다른 모델 유형과 연결되는 것을 가능하게 합니다.
질문 6: N+1 문제란 무엇이며 Eager Loading으로 어떻게 해결하는가
N+1 문제는 메인 쿼리가 관계 로딩을 위해 N개의 추가 쿼리를 발생시킬 때 나타납니다. Laravel 애플리케이션에서 가장 흔한 성능 저하 원인입니다.
// ❌ PROBLEM: N+1 queries
// 1 query for articles + 1 query PER article for the author
$articles = Article::all();
foreach ($articles as $article) {
echo $article->author->name; // SQL query on each iteration!
}
// ✅ SOLUTION 1: with() - Eager Loading
// Only 2 queries (articles + users with IN clause)
$articles = Article::with('author')->get();
foreach ($articles as $article) {
echo $article->author->name; // Already loaded, no query
}
// ✅ SOLUTION 2: Nested Eager Loading
// Loads articles, their authors, and author roles
$articles = Article::with(['author.roles', 'comments.user'])->get();
// ✅ SOLUTION 3: Eager Loading with constraints
$articles = Article::with([
'comments' => function ($query) {
$query->where('approved', true)
->orderBy('created_at', 'desc')
->limit(5);
}
])->get();
// ✅ SOLUTION 4: Default Eager Loading in the model
class Article extends Model
{
// These relationships are always loaded automatically
protected $with = ['author', 'category'];
}개발 중 N+1 문제 감지에는 Laravel Telescope의 php artisan telescope:prune가 효과적입니다.
질문 7: 쿼리 스코프를 어떻게 생성하며 언제 사용하는가
쿼리 스코프는 재사용 가능한 쿼리 조건을 모델 레벨에서 캡슐화하여 코드 가독성과 DRY 원칙을 향상시킵니다.
class Article extends Model
{
// Global Scope: automatically applied to ALL queries
protected static function booted(): void
{
// Excludes soft-deleted articles by default
static::addGlobalScope('published', function (Builder $builder) {
$builder->where('status', 'published');
});
}
// Local Scope: called explicitly via scopeScopeName
public function scopePopular(Builder $query, int $minViews = 1000): Builder
{
return $query->where('view_count', '>=', $minViews);
}
public function scopeByAuthor(Builder $query, User $author): Builder
{
return $query->where('user_id', $author->id);
}
public function scopeRecent(Builder $query, int $days = 7): Builder
{
return $query->where('created_at', '>=', now()->subDays($days));
}
public function scopeWithStats(Builder $query): Builder
{
return $query->withCount('comments')
->withSum('reactions', 'score');
}
}
// Usage: fluent scope chaining
$articles = Article::popular(500)
->recent(30)
->byAuthor($user)
->withStats()
->orderByDesc('comment_count')
->paginate(20);
// Ignore a Global Scope
$allArticles = Article::withoutGlobalScope('published')->get();로컬 스코프는 가독성을 향상시키고 쿼리 로직을 집중화합니다. 글로벌 스코프는 멀티테넌시나 소프트 삭제에 적합합니다.
Laravel 면접 준비가 되셨나요?
인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.
미들웨어와 라우팅
질문 8: Laravel의 미들웨어는 어떻게 동작하는가
미들웨어는 수신 HTTP 요청을 필터링하고 발신 응답을 수정할 수 있습니다. 각 요청은 미들웨어 스택을 거칩니다.
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class CheckSubscription
{
public function handle(Request $request, Closure $next, string $plan = 'basic'): Response
{
$user = $request->user();
// Check before the controller
if (!$user || !$user->hasActiveSubscription($plan)) {
if ($request->expectsJson()) {
return response()->json(['error' => 'Subscription required'], 403);
}
return redirect()->route('subscription.plans');
}
// Pass to next middleware or controller
$response = $next($request);
// Modify response after the controller
$response->headers->set('X-Subscription-Plan', $user->subscription->plan);
return $response;
}
}
// bootstrap/app.php (Laravel 11+)
return Application::configure(basePath: dirname(__DIR__))
->withMiddleware(function (Middleware $middleware) {
// Global middleware (all requests)
$middleware->append(LogRequestMiddleware::class);
// Aliases for use in routes
$middleware->alias([
'subscription' => CheckSubscription::class,
'role' => EnsureUserHasRole::class,
]);
// Middleware groups
$middleware->group('api', [
ThrottleRequests::class.':api',
SubstituteBindings::class,
]);
});// Applying middlewares to routes
Route::middleware(['auth', 'subscription:premium'])->group(function () {
Route::get('/dashboard', DashboardController::class);
Route::resource('projects', ProjectController::class);
});미들웨어의 순서는 중요합니다. 진입 시 위에서 아래로, 반환 시 아래에서 위로 실행됩니다.
질문 9: 라우트 모델 바인딩과 그 변형을 설명하십시오
라우트 모델 바인딩은 URL 파라미터를 기반으로 Eloquent 모델을 컨트롤러에 자동 주입합니다.
// Implicit binding: Laravel automatically resolves by ID
Route::get('/articles/{article}', [ArticleController::class, 'show']);
// Binding by slug instead of ID
Route::get('/articles/{article:slug}', [ArticleController::class, 'show']);
// Binding with relationship (automatic scope)
Route::get('/users/{user}/articles/{article}', function (User $user, Article $article) {
// Laravel automatically verifies that the article belongs to the user
return $article;
})->scopeBindings();class Article extends Model
{
// Customize the default resolution key
public function getRouteKeyName(): string
{
return 'slug'; // Resolves by slug instead of id
}
// Customize the resolution query
public function resolveRouteBinding($value, $field = null): ?Model
{
return $this->where($field ?? 'slug', $value)
->where('status', 'published')
->firstOrFail();
}
}// Explicit custom binding
public function boot(): void
{
Route::bind('article', function (string $value) {
return Article::where('slug', $value)
->published()
->with('author')
->firstOrFail();
});
}라우트 모델 바인딩은 보일러플레이트 코드를 줄이고 해결 로직을 중앙에서 관리합니다.
큐와 잡
질문 10: Laravel에서 잡과 큐를 어떻게 구현하는가
큐를 사용하면 무거운 작업을 백그라운드에서 실행하여 애플리케이션 응답 속도를 향상시킬 수 있습니다.
namespace App\Jobs;
use App\Models\Podcast;
use App\Services\AudioProcessor;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ProcessPodcast implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
// Number of attempts before final failure
public int $tries = 3;
// Timeout in seconds
public int $timeout = 600;
// Delay between attempts (exponential backoff)
public array $backoff = [30, 60, 120];
public function __construct(
public Podcast $podcast
) {}
public function handle(AudioProcessor $processor): void
{
// The job executes in the background
$processor->transcode($this->podcast->audio_path);
$processor->generateWaveform($this->podcast);
$this->podcast->update(['status' => 'processed']);
}
// Failure handling
public function failed(\Throwable $exception): void
{
$this->podcast->update(['status' => 'failed']);
// Admin notification, logging, etc.
}
// Conditions for retrying the job
public function retryUntil(): \DateTime
{
return now()->addHours(24);
}
}// Dispatching the job
ProcessPodcast::dispatch($podcast); // Default queue
ProcessPodcast::dispatch($podcast)->onQueue('audio'); // Specific queue
ProcessPodcast::dispatch($podcast)->delay(now()->addMinutes(10)); // Delay
// Job chaining (sequential execution)
Bus::chain([
new ProcessPodcast($podcast),
new GenerateThumbnail($podcast),
new NotifySubscribers($podcast),
])->dispatch();
// Job batch (parallel execution with tracking)
Bus::batch([
new ProcessPodcast($podcast1),
new ProcessPodcast($podcast2),
new ProcessPodcast($podcast3),
])->then(function (Batch $batch) {
// All jobs succeeded
})->catch(function (Batch $batch, \Throwable $e) {
// First failure
})->finally(function (Batch $batch) {
// All jobs finished (success or failure)
})->dispatch();php artisan queue:work --queue=high,default로 워커를 시작하면 우선순위에 따라 여러 큐를 처리할 수 있습니다.
질문 11: 잡, 이벤트, 리스너의 차이점은 무엇인가
세 가지 개념 모두 코드의 느슨한 결합에 기여하지만, 각각 다른 의도를 가지고 있습니다.
// Jobs: single task to execute
// Used for heavy or deferred operations
class SendWelcomeEmail implements ShouldQueue
{
public function handle(Mailer $mailer): void
{
$mailer->send(new WelcomeEmail($this->user));
}
}
// Events: notification that something happened
// The event contains only data, not logic
class UserRegistered
{
public function __construct(
public User $user,
public string $source
) {}
}
// Listeners: react to events
// An event can have multiple listeners
class SendWelcomeNotification implements ShouldQueue
{
public function handle(UserRegistered $event): void
{
$event->user->notify(new WelcomeNotification());
}
}
class TrackRegistration
{
public function handle(UserRegistered $event): void
{
Analytics::track('user_registered', [
'user_id' => $event->user->id,
'source' => $event->source,
]);
}
}protected $listen = [
UserRegistered::class => [
SendWelcomeNotification::class, // Queued
TrackRegistration::class, // Sync
CreateDefaultSettings::class, // Sync
],
];
// Triggering the event
event(new UserRegistered($user, 'web'));
// Or
UserRegistered::dispatch($user, 'web');이벤트는 느슨한 결합 아키텍처를 촉진합니다. 이벤트를 발생시키는 코드는 그 결과에 대해 알 필요가 없습니다.
보안과 인증
질문 12: Laravel은 CSRF 공격으로부터 어떻게 보호하는가
Laravel은 세션별로 고유한 CSRF 토큰을 자동 생성하고, 모든 POST, PUT, PATCH, DELETE 요청에서 이 토큰을 검증합니다.
// In Blade forms
<form method="POST" action="/profile">
@csrf {{-- Generates a hidden field with the token --}}
@method('PUT') {{-- HTTP method spoofing --}}
<input type="text" name="name" value="{{ $user->name }}">
<button type="submit">Update</button>
</form>
// For AJAX requests, the token is in the meta tag
<meta name="csrf-token" content="{{ csrf_token() }}">
// Axios configuration to automatically send the token
axios.defaults.headers.common['X-CSRF-TOKEN'] =
document.querySelector('meta[name="csrf-token"]').content;// Exclude routes from CSRF verification (external webhooks)
return Application::configure(basePath: dirname(__DIR__))
->withMiddleware(function (Middleware $middleware) {
$middleware->validateCsrfTokens(except: [
'stripe/webhook', // Stripe webhook authenticated by signature
'api/*', // API authenticated by token
]);
});CSRF 보호를 전역적으로 비활성화해서는 안 됩니다. 다른 인증 수단으로 보호되는 엔드포인트에만 예외를 적용해야 합니다.
질문 13: Laravel Sanctum으로 인증을 어떻게 구현하는가
Laravel Sanctum은 SPA, 모바일 앱, 토큰 기반 API를 위한 경량 인증을 제공합니다.
// Installation and configuration
// php artisan install:api (Laravel 11+)
// app/Models/User.php
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
}class AuthController extends Controller
{
public function login(Request $request): JsonResponse
{
$credentials = $request->validate([
'email' => 'required|email',
'password' => 'required',
]);
if (!Auth::attempt($credentials)) {
return response()->json(['message' => 'Invalid credentials'], 401);
}
$user = Auth::user();
// Create a token with abilities (permissions)
$token = $user->createToken('api-token', [
'articles:read',
'articles:write',
'profile:update',
]);
return response()->json([
'user' => $user,
'token' => $token->plainTextToken,
]);
}
public function logout(Request $request): JsonResponse
{
// Revoke the current token
$request->user()->currentAccessToken()->delete();
// Or revoke all tokens
// $request->user()->tokens()->delete();
return response()->json(['message' => 'Logged out']);
}
}
// routes/api.php
Route::post('/login', [AuthController::class, 'login']);
Route::middleware('auth:sanctum')->group(function () {
Route::get('/user', fn(Request $r) => $r->user());
Route::post('/logout', [AuthController::class, 'logout']);
// Ability verification
Route::middleware('ability:articles:write')->group(function () {
Route::post('/articles', [ArticleController::class, 'store']);
});
});Sanctum은 동일 도메인 SPA를 위한 쿠키 인증도 지원하며, 자동 CSRF 보호를 제공합니다.
질문 14: 정책(Policies)과 게이트(Gates)로 API를 어떻게 보호하는가
정책과 게이트는 인가 로직을 중앙에서 관리하며, 비즈니스 규칙을 컨트롤러에서 분리합니다.
namespace App\Policies;
use App\Models\Article;
use App\Models\User;
class ArticlePolicy
{
// Pre-check: admins have all rights
public function before(User $user, string $ability): ?bool
{
if ($user->isAdmin()) {
return true; // Allow everything
}
return null; // Continue to specific method
}
public function view(?User $user, Article $article): bool
{
// Published articles visible to all
if ($article->status === 'published') {
return true;
}
// Drafts visible only to author
return $user?->id === $article->user_id;
}
public function update(User $user, Article $article): bool
{
return $user->id === $article->user_id;
}
public function delete(User $user, Article $article): bool
{
return $user->id === $article->user_id
&& $article->comments()->count() === 0;
}
}class ArticleController extends Controller
{
public function update(Request $request, Article $article)
{
// Check policy, throws 403 if unauthorized
$this->authorize('update', $article);
$article->update($request->validated());
return redirect()->route('articles.show', $article);
}
}
// In Blade
@can('update', $article)
<a href="{{ route('articles.edit', $article) }}">Edit</a>
@endcan
// Gates for non-model authorizations
Gate::define('access-admin', function (User $user) {
return $user->role === 'admin';
});
// Usage
if (Gate::allows('access-admin')) {
// ...
}정책은 모델에 연결되며, 게이트는 범용적인 인가에 사용됩니다.
유효성 검사와 폼
질문 15: 커스텀 유효성 검사 규칙을 어떻게 생성하는가
Laravel은 복잡성과 재사용성 요구사항에 따라 커스텀 유효성 검사를 생성하는 여러 방법을 제공합니다.
// Custom rule as a class (reusable)
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
class StrongPassword implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$errors = [];
if (strlen($value) < 12) {
$errors[] = 'at least 12 characters';
}
if (!preg_match('/[A-Z]/', $value)) {
$errors[] = 'at least one uppercase letter';
}
if (!preg_match('/[a-z]/', $value)) {
$errors[] = 'at least one lowercase letter';
}
if (!preg_match('/[0-9]/', $value)) {
$errors[] = 'at least one digit';
}
if (!preg_match('/[@$!%*?&#]/', $value)) {
$errors[] = 'at least one special character';
}
if (!empty($errors)) {
$fail("The password must contain: " . implode(', ', $errors) . '.');
}
}
}// Form Request with complex validation
namespace App\Http\Requests;
use App\Rules\StrongPassword;
use Illuminate\Foundation\Http\FormRequest;
class RegisterRequest extends FormRequest
{
public function authorize(): bool
{
return true; // Or authorization logic
}
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'unique:users,email'],
'password' => ['required', 'confirmed', new StrongPassword()],
'company' => ['required_if:account_type,business', 'string'],
'vat_number' => [
'nullable',
'string',
// Closure for inline validation
function ($attribute, $value, $fail) {
if ($value && !$this->isValidVatNumber($value)) {
$fail('The VAT number is invalid.');
}
},
],
];
}
public function messages(): array
{
return [
'email.unique' => 'This email address is already in use.',
'password.confirmed' => 'The passwords do not match.',
];
}
protected function prepareForValidation(): void
{
// Normalization before validation
$this->merge([
'email' => strtolower(trim($this->email)),
]);
}
}Form Request는 유효성 검사, 인가, 에러 메시지를 집중화하여 컨트롤러의 부담을 줄입니다.
Laravel 면접 준비가 되셨나요?
인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.
테스트
질문 16: Laravel에서 테스트 구조를 어떻게 설계하는가
Laravel은 애플리케이션의 각 레이어를 테스트하기 위한 전용 헬퍼가 포함된 PHPUnit을 제공합니다.
namespace Tests\Feature;
use App\Models\Article;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ArticleControllerTest extends TestCase
{
use RefreshDatabase; // Resets DB between each test
public function test_guest_can_view_published_articles(): void
{
$article = Article::factory()->published()->create();
$response = $this->get('/articles/' . $article->slug);
$response->assertStatus(200);
$response->assertSee($article->title);
}
public function test_authenticated_user_can_create_article(): void
{
$user = User::factory()->create();
$response = $this->actingAs($user)
->post('/articles', [
'title' => 'My new article',
'content' => 'Test content',
]);
$response->assertRedirect();
$this->assertDatabaseHas('articles', [
'title' => 'My new article',
'user_id' => $user->id,
]);
}
public function test_user_cannot_update_others_article(): void
{
$owner = User::factory()->create();
$other = User::factory()->create();
$article = Article::factory()->for($owner)->create();
$response = $this->actingAs($other)
->put('/articles/' . $article->id, [
'title' => 'Modified title',
]);
$response->assertStatus(403);
}
}namespace Tests\Unit\Services;
use App\Services\PaymentService;
use App\Contracts\PaymentGatewayInterface;
use App\Repositories\OrderRepository;
use Mockery;
use Tests\TestCase;
class PaymentServiceTest extends TestCase
{
public function test_process_payment_charges_correct_amount(): void
{
// Mock dependencies
$gateway = Mockery::mock(PaymentGatewayInterface::class);
$gateway->shouldReceive('charge')
->once()
->with(99.99, Mockery::any())
->andReturn((object) ['transactionId' => 'tx_123']);
$orders = Mockery::mock(OrderRepository::class);
$orders->shouldReceive('find')
->with(1)
->andReturn($this->createOrder());
$service = new PaymentService($gateway, $orders);
$result = $service->processPayment(1, 99.99);
$this->assertTrue($result);
}
}Feature 테스트(HTTP, 통합 테스트)와 Unit 테스트(모킹을 활용한 단위 테스트)를 분리하여 관리합니다.
질문 17: 팩토리와 시더를 효과적으로 사용하는 방법
팩토리는 현실적인 테스트 데이터를 생성하고, 시더는 데이터베이스에 데이터를 채웁니다.
namespace Database\Factories;
use App\Models\Article;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
class ArticleFactory extends Factory
{
protected $model = Article::class;
public function definition(): array
{
return [
'user_id' => User::factory(),
'title' => fake()->sentence(6),
'slug' => fake()->unique()->slug(),
'content' => fake()->paragraphs(5, true),
'status' => 'draft',
'view_count' => fake()->numberBetween(0, 10000),
'created_at' => fake()->dateTimeBetween('-1 year'),
];
}
// States for different configurations
public function published(): static
{
return $this->state(fn(array $attr) => [
'status' => 'published',
'published_at' => fake()->dateTimeBetween('-6 months'),
]);
}
public function draft(): static
{
return $this->state(['status' => 'draft', 'published_at' => null]);
}
public function popular(): static
{
return $this->state(['view_count' => fake()->numberBetween(10000, 100000)]);
}
// Relationship configuration
public function configure(): static
{
return $this->afterCreating(function (Article $article) {
// Create tags after article creation
$article->tags()->attach(
\App\Models\Tag::factory()->count(3)->create()
);
});
}
}
// Usage in tests
$article = Article::factory()->published()->create();
$articles = Article::factory()->count(10)->for($user)->create();
$articleWithComments = Article::factory()
->has(Comment::factory()->count(5))
->create();State를 사용하면 팩토리 로직을 중복하지 않고 다양한 변형을 생성할 수 있습니다.
아키텍처와 패턴
질문 18: Laravel에서 리포지토리 패턴을 어떻게 구현하는가
리포지토리 패턴은 데이터 접근을 추상화하고 쿼리 모킹을 가능하게 하여 테스트를 용이하게 합니다.
namespace App\Contracts;
use App\Models\Article;
use Illuminate\Pagination\LengthAwarePaginator;
interface ArticleRepositoryInterface
{
public function find(int $id): ?Article;
public function findBySlug(string $slug): ?Article;
public function getPublished(int $perPage = 20): LengthAwarePaginator;
public function getByAuthor(int $userId, int $perPage = 20): LengthAwarePaginator;
public function create(array $data): Article;
public function update(Article $article, array $data): Article;
public function delete(Article $article): bool;
}namespace App\Repositories;
use App\Contracts\ArticleRepositoryInterface;
use App\Models\Article;
use Illuminate\Pagination\LengthAwarePaginator;
class EloquentArticleRepository implements ArticleRepositoryInterface
{
public function __construct(
private Article $model
) {}
public function find(int $id): ?Article
{
return $this->model->with('author')->find($id);
}
public function findBySlug(string $slug): ?Article
{
return $this->model
->where('slug', $slug)
->with(['author', 'tags'])
->firstOrFail();
}
public function getPublished(int $perPage = 20): LengthAwarePaginator
{
return $this->model
->published()
->with('author')
->orderByDesc('published_at')
->paginate($perPage);
}
public function create(array $data): Article
{
return $this->model->create($data);
}
public function update(Article $article, array $data): Article
{
$article->update($data);
return $article->fresh();
}
public function delete(Article $article): bool
{
return $article->delete();
}
}
// Binding in the ServiceProvider
$this->app->bind(
ArticleRepositoryInterface::class,
EloquentArticleRepository::class
);리포지토리 패턴은 복잡한 애플리케이션에 유용하지만, 단순한 프로젝트에서는 과도할 수 있습니다. 비용 대비 효과를 평가하는 것이 중요합니다.
질문 19: Laravel에서 트랜잭션과 동시성을 어떻게 다루는가
트랜잭션은 여러 작업에 걸친 데이터 무결성을 보장합니다. Laravel은 그 관리를 단순화합니다.
use Illuminate\Support\Facades\DB;
class OrderService
{
public function processOrder(Cart $cart, User $user): Order
{
// Transaction with closure: automatic rollback on exception
return DB::transaction(function () use ($cart, $user) {
// Create the order
$order = Order::create([
'user_id' => $user->id,
'total' => $cart->total(),
'status' => 'pending',
]);
// Create order items
foreach ($cart->items as $item) {
$order->items()->create([
'product_id' => $item->product_id,
'quantity' => $item->quantity,
'price' => $item->product->price,
]);
// Decrement stock with pessimistic locking
$product = Product::lockForUpdate()->find($item->product_id);
if ($product->stock < $item->quantity) {
throw new InsufficientStockException($product);
}
$product->decrement('stock', $item->quantity);
}
// Clear the cart
$cart->clear();
return $order;
}, attempts: 3); // 3 attempts in case of deadlock
}
public function updateOrderStatus(Order $order, string $status): void
{
// Optimistic locking with version/timestamp
$updated = DB::table('orders')
->where('id', $order->id)
->where('updated_at', $order->updated_at) // Version check
->update([
'status' => $status,
'updated_at' => now(),
]);
if ($updated === 0) {
throw new ConcurrencyException('The order was modified in the meantime');
}
}
}lockForUpdate()는 트랜잭션 중 동시 읽기를 방지합니다. 데드락을 피하기 위해 신중하게 사용해야 합니다.
질문 20: Laravel에서 캐싱을 효과적으로 구현하는 방법
캐싱은 반복적인 쿼리를 제거하여 성능을 획기적으로 향상시킵니다.
// Caching strategies
use Illuminate\Support\Facades\Cache;
class ArticleService
{
public function getPopularArticles(): Collection
{
// Cache-Aside: check cache, otherwise load and store
return Cache::remember('articles:popular', 3600, function () {
return Article::published()
->popular()
->with('author')
->limit(10)
->get();
});
}
public function getArticle(string $slug): Article
{
// Cache by dynamic key
return Cache::remember("article:{$slug}", 1800, function () use ($slug) {
return Article::where('slug', $slug)
->with(['author', 'comments.user'])
->firstOrFail();
});
}
public function updateArticle(Article $article, array $data): Article
{
$article->update($data);
// Cache invalidation after modification
Cache::forget("article:{$article->slug}");
Cache::forget('articles:popular');
// Tags for grouped invalidation (Redis only)
Cache::tags(['articles', "user:{$article->user_id}"])->flush();
return $article;
}
public function getArticleWithLock(int $id): Article
{
// Atomic lock to avoid cache stampede
return Cache::lock("article-lock:{$id}", 10)->block(5, function () use ($id) {
return Cache::remember("article:{$id}", 3600, fn() => Article::findOrFail($id));
});
}
}'stores' => [
'redis' => [
'driver' => 'redis',
'connection' => 'cache',
'lock_connection' => 'default',
],
],효율적인 그룹 무효화를 위해 캐시 태그를 사용합니다. 동시 만료로 인한 캐시 스탬피드에 주의가 필요합니다.
성능과 최적화
질문 21: Laravel 애플리케이션의 성능을 어떻게 최적화하는가
최적화는 쿼리, 캐시, 설정, 인프라스트럭처 등 여러 수준에 걸쳐 이루어집니다.
$articles = Article::query()
->select(['id', 'title', 'slug', 'published_at', 'user_id']) // Specific columns
->with(['author:id,name,avatar']) // Selective eager loading
->withCount('comments') // Count in one query
->published()
->latest('published_at')
->cursorPaginate(20); // Cursor pagination (more performant)
// 2. Chunking for mass operations
Article::query()
->where('status', 'published')
->chunkById(1000, function ($articles) {
foreach ($articles as $article) {
// Processing in batches of 1000
ProcessArticle::dispatch($article);
}
});
// 3. Mass update without models
Article::where('published_at', '<', now()->subYear())
->update(['status' => 'archived']); // Single SQL query# Optimization commands for production
php artisan config:cache # Cache configuration
php artisan route:cache # Cache routes
php artisan view:cache # Compile Blade views
php artisan event:cache # Cache event mappings
php artisan optimize # Run all optimizations
# Optimized autoloader
composer install --optimize-autoloader --no-dev이러한 최적화를 통해 프로덕션 환경의 부팅 시간을 50% 이상 단축할 수 있습니다.
질문 22: Laravel 애플리케이션을 어떻게 디버그하고 프로파일링하는가
Laravel은 성능 문제와 버그를 식별하기 위한 여러 도구를 제공합니다.
// Laravel Telescope for development debugging
// Captures requests, jobs, exceptions, etc.
// Query debugging
DB::enableQueryLog();
$articles = Article::with('author')->get();
$queries = DB::getQueryLog();
dump($queries); // Shows all SQL queries
// Debug bar integrated with Blade
@dump($variable) // Display and continue
@dd($variable) // Dump and die
// Structured logging
use Illuminate\Support\Facades\Log;
Log::channel('slack')->critical('Payment failed', [
'user_id' => $user->id,
'amount' => $amount,
'error' => $exception->getMessage(),
'trace' => $exception->getTraceAsString(),
]);
// Context logging
Log::withContext(['request_id' => request()->id()]);
Log::info('Processing order', ['order_id' => $order->id]);'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => ['daily', 'slack'],
'ignore_exceptions' => false,
],
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => 'debug',
'days' => 14,
],
],개발 환경에서는 Telescope를 사용하고, 프로덕션 환경에서는 APM(New Relic, Datadog)을 사용하여 지속적인 모니터링을 수행합니다.
배포와 프로덕션
질문 23: 다운타임 없이 프로덕션 마이그레이션을 실행하는 방법
프로덕션 마이그레이션은 서비스 중단을 방지하기 위해 특별한 주의가 필요합니다.
// Safe migration: add a nullable column first
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
// Step 1: Add nullable column
$table->string('role')->nullable()->after('email');
});
}
// Step 2: Data migration (separate job)
// php artisan tinker
// User::whereNull('role')->update(['role' => 'user']);
// Step 3: Second migration for constraint
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('role')->nullable(false)->default('user')->change();
});
}// For column deletions (3 deployments)
// Deployment 1: Stop using the column in code
// Deployment 2: Delete the column
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('deprecated_field');
});
// Migration with timeout for large tables
public function up(): void
{
DB::statement('SET lock_timeout TO \'5s\'');
Schema::table('large_table', function (Blueprint $table) {
$table->index('status'); // Concurrent index if PostgreSQL
});
}"expand-contract" 전략을 통해 다운타임 없이 컬럼을 추가할 수 있습니다: nullable 추가 → 데이터 마이그레이션 → 비null 제약 조건 적용.
질문 24: Laravel을 고가용성으로 구성하는 방법
고가용성 아키텍처는 상태 비저장(stateless) 컴포넌트의 분리와 공유 상태 관리를 필요로 합니다.
'driver' => env('SESSION_DRIVER', 'redis'),
'connection' => 'session',
// config/cache.php - Shared cache
'default' => env('CACHE_DRIVER', 'redis'),
// config/queue.php - Redis queues for distribution
'default' => env('QUEUE_CONNECTION', 'redis'),
'connections' => [
'redis' => [
'driver' => 'redis',
'connection' => 'default',
'queue' => 'default',
'retry_after' => 90,
'block_for' => null,
],
],
// config/filesystems.php - S3 storage for files
'disks' => [
's3' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'),
],
],// Health check endpoint for load balancer
Route::get('/health', function () {
try {
DB::connection()->getPdo();
Cache::store('redis')->ping();
return response()->json(['status' => 'healthy']);
} catch (\Exception $e) {
return response()->json(['status' => 'unhealthy'], 503);
}
});각 인스턴스는 상태 비저장이어야 합니다. 세션, 캐시, 큐에는 Redis 또는 공유 스토어를 사용해야 합니다.
질문 25: Laravel 배포의 모범 사례란 무엇인가
견고한 배포는 자동화, 검증, 용이한 롤백을 결합한 것입니다.
# deploy.sh - Typical deployment script
#!/bin/bash
set -e
echo "Pulling latest code..."
git pull origin main
echo "Installing dependencies..."
composer install --no-dev --optimize-autoloader
echo "Running migrations..."
php artisan migrate --force
echo "Caching configuration..."
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache
echo "Restarting queue workers..."
php artisan queue:restart
echo "Clearing old cache..."
php artisan cache:clear
echo "Deployment complete!"APP_ENV=production
APP_DEBUG=false
APP_KEY=base64:... # Generated with php artisan key:generate
LOG_CHANNEL=stack
LOG_LEVEL=warning
# Never expose credentials in plain text
# Use secret managers (Vault, AWS Secrets Manager)배포 체크리스트:
php artisan config:cache- 설정 캐시php artisan route:cache- 라우트 캐시php artisan view:cache- 뷰 컴파일composer install --no-dev- 프로덕션 의존성- 배포 전 자동 테스트 실행
- 헬스 체크 구성
- 모니터링 및 알림 구축
결론
본 가이드에서 다룬 25가지 질문은 서비스 컨테이너 기초부터 프로덕션 배포 패턴까지, Laravel과 PHP 면접에 필수적인 지식을 포괄합니다.
준비 체크리스트:
- 서비스 컨테이너와 의존성 주입
- Eloquent ORM: 관계, 스코프, Eager Loading
- 미들웨어, 라우팅, 보안
- 큐, 잡, 비동기 이벤트
- 테스트: Feature 테스트, Unit 테스트, 팩토리
- 고급 패턴: 리포지토리, 트랜잭션, 캐싱
- 배포: 마이그레이션, 최적화, 고가용성
각 질문은 Laravel의 공식 문서를 통해 더 깊이 탐구할 가치가 있습니다. 면접관은 프레임워크의 세부 사항을 이해하고 기술적 선택에 대해 근거를 가지고 설명할 수 있는 지원자를 높이 평가합니다.
연습을 시작하세요!
면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.
태그
공유
관련 기사

Laravel 11: 처음부터 완전한 애플리케이션 구축하기
Laravel 11로 인증, REST API, Eloquent ORM, 배포까지 완전한 애플리케이션을 구축하는 종합 가이드입니다. 실용적인 코드 예제와 함께 설명합니다.

Django와 Python 면접 질문: 2026년 핵심 25선
Django와 Python 면접에서 가장 자주 출제되는 25가지 질문을 상세히 다룹니다. ORM, 뷰, 미들웨어, Django REST Framework, 시그널, 성능 최적화에 대한 상세한 답변과 코드 예제를 제공합니다.

Rust 면접 질문: 2026년 완벽 가이드
Rust 기술 면접에서 자주 출제되는 25개 질문을 완벽하게 다룹니다. 소유권, 빌림, 라이프타임, 트레이트, async/await, 동시성에 대한 상세한 답변과 코드 예제를 제공합니다.