Laravel과 PHP 면접 질문: 2026년 핵심 25선

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

Laravel과 PHP 면접 질문 - 종합 가이드

Laravel 면접에서는 PHP의 대표적인 프레임워크에 대한 숙련도, Eloquent ORM의 이해, MVC 아키텍처에 대한 지식, 그리고 견고하고 유지보수 가능한 애플리케이션을 구축하는 능력이 평가됩니다. 본 가이드에서는 Laravel 기초부터 프로덕션 배포 패턴까지, 가장 빈출되는 25가지 질문을 다룹니다.

면접 팁

Laravel의 아키텍처적 설계 결정을 설명할 수 있는 지원자는 면접관으로부터 높은 평가를 받습니다. 프레임워크가 특정 규칙을 채택한 이유(설정보다 규칙, 서비스 컨테이너)를 이해하는 것이 면접에서 큰 차별화 요소가 됩니다.

Laravel 기초

질문 1: Laravel의 리퀘스트 라이프사이클을 설명하십시오

Laravel의 리퀘스트 라이프사이클은 컨트롤러에 도달하기 전에 여러 레이어를 거칩니다. 이 사이클을 이해하는 것은 디버깅과 성능 최적화에 필수적입니다.

public/index.phpphp
// 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의 핵심입니다. 클래스 인스턴스화를 관리하고 생성자 주입을 통해 의존성을 자동으로 해결합니다.

app/Services/PaymentService.phpphp
// 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;
        }
    }
}
app/Providers/AppServiceProvider.phpphp
// 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: 파사드와 의존성 주입의 차이점은 무엇인가

파사드는 컨테이너 서비스에 정적 문법을 제공하고, 의존성 주입은 의존 관계를 명시적으로 만듭니다.

php
// 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의 서비스 프로바이더는 어떻게 동작하는가

서비스 프로바이더는 애플리케이션 설정의 중심점입니다. 각 프로바이더는 서비스 등록, 바인딩 구성, 컴포넌트 초기화를 담당합니다.

app/Providers/PaymentServiceProvider.phpphp
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는 테이블 간 연관을 모델링하기 위한 여러 관계 유형을 제공합니다. 각 유형에는 특정 사용 사례가 있습니다.

app/Models/User.phpphp
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 애플리케이션에서 가장 흔한 성능 저하 원인입니다.

php
// ❌ 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 원칙을 향상시킵니다.

app/Models/Article.phpphp
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 요청을 필터링하고 발신 응답을 수정할 수 있습니다. 각 요청은 미들웨어 스택을 거칩니다.

app/Http/Middleware/CheckSubscription.phpphp
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,
        ]);
    });
routes/web.phpphp
// Applying middlewares to routes
Route::middleware(['auth', 'subscription:premium'])->group(function () {
    Route::get('/dashboard', DashboardController::class);
    Route::resource('projects', ProjectController::class);
});

미들웨어의 순서는 중요합니다. 진입 시 위에서 아래로, 반환 시 아래에서 위로 실행됩니다.

질문 9: 라우트 모델 바인딩과 그 변형을 설명하십시오

라우트 모델 바인딩은 URL 파라미터를 기반으로 Eloquent 모델을 컨트롤러에 자동 주입합니다.

routes/web.phpphp
// 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();
app/Models/Article.phpphp
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();
    }
}
app/Providers/RouteServiceProvider.phpphp
// Explicit custom binding
public function boot(): void
{
    Route::bind('article', function (string $value) {
        return Article::where('slug', $value)
            ->published()
            ->with('author')
            ->firstOrFail();
    });
}

라우트 모델 바인딩은 보일러플레이트 코드를 줄이고 해결 로직을 중앙에서 관리합니다.

큐와 잡

질문 10: Laravel에서 잡과 큐를 어떻게 구현하는가

큐를 사용하면 무거운 작업을 백그라운드에서 실행하여 애플리케이션 응답 속도를 향상시킬 수 있습니다.

app/Jobs/ProcessPodcast.phpphp
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);
    }
}
php
// 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: 잡, 이벤트, 리스너의 차이점은 무엇인가

세 가지 개념 모두 코드의 느슨한 결합에 기여하지만, 각각 다른 의도를 가지고 있습니다.

php
// 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,
        ]);
    }
}
app/Providers/EventServiceProvider.phpphp
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 요청에서 이 토큰을 검증합니다.

php
// 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;
bootstrap/app.phpphp
// 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를 위한 경량 인증을 제공합니다.

php
// 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;
}
app/Http/Controllers/AuthController.phpphp
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를 어떻게 보호하는가

정책과 게이트는 인가 로직을 중앙에서 관리하며, 비즈니스 규칙을 컨트롤러에서 분리합니다.

app/Policies/ArticlePolicy.phpphp
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;
    }
}
app/Http/Controllers/ArticleController.phpphp
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은 복잡성과 재사용성 요구사항에 따라 커스텀 유효성 검사를 생성하는 여러 방법을 제공합니다.

app/Rules/StrongPassword.phpphp
// 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) . '.');
        }
    }
}
app/Http/Requests/RegisterRequest.phpphp
// 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을 제공합니다.

tests/Feature/ArticleControllerTest.phpphp
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);
    }
}
tests/Unit/Services/PaymentServiceTest.phpphp
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: 팩토리와 시더를 효과적으로 사용하는 방법

팩토리는 현실적인 테스트 데이터를 생성하고, 시더는 데이터베이스에 데이터를 채웁니다.

database/factories/ArticleFactory.phpphp
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에서 리포지토리 패턴을 어떻게 구현하는가

리포지토리 패턴은 데이터 접근을 추상화하고 쿼리 모킹을 가능하게 하여 테스트를 용이하게 합니다.

app/Contracts/ArticleRepositoryInterface.phpphp
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;
}
app/Repositories/EloquentArticleRepository.phpphp
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은 그 관리를 단순화합니다.

app/Services/OrderService.phpphp
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에서 캐싱을 효과적으로 구현하는 방법

캐싱은 반복적인 쿼리를 제거하여 성능을 획기적으로 향상시킵니다.

php
// 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));
        });
    }
}
config/cache.php - Recommended Redis configuration for productionphp
'stores' => [
    'redis' => [
        'driver' => 'redis',
        'connection' => 'cache',
        'lock_connection' => 'default',
    ],
],

효율적인 그룹 무효화를 위해 캐시 태그를 사용합니다. 동시 만료로 인한 캐시 스탬피드에 주의가 필요합니다.

성능과 최적화

질문 21: Laravel 애플리케이션의 성능을 어떻게 최적화하는가

최적화는 쿼리, 캐시, 설정, 인프라스트럭처 등 여러 수준에 걸쳐 이루어집니다.

1. Eloquent query optimizationphp
$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
bash
# 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은 성능 문제와 버그를 식별하기 위한 여러 도구를 제공합니다.

php
// 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]);
config/logging.php - Channel configurationphp
'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: 다운타임 없이 프로덕션 마이그레이션을 실행하는 방법

프로덕션 마이그레이션은 서비스 중단을 방지하기 위해 특별한 주의가 필요합니다.

database/migrations/2026_01_30_add_role_to_users_table.phpphp
// 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();
    });
}
php
// 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) 컴포넌트의 분리와 공유 상태 관리를 필요로 합니다.

config/session.php - Sessions shared between instancesphp
'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'),
    ],
],
php
// 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 배포의 모범 사례란 무엇인가

견고한 배포는 자동화, 검증, 용이한 롤백을 결합한 것입니다.

bash
# 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!"
.env.production - Critical variablesphp
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
#php
#interview
#eloquent
#technical interview

공유

관련 기사