PHP Laravel 프레임워크 면접 질문 2026: Eloquent, 큐, 아키텍처 패턴 완벽 가이드

2026년 Laravel 기술 면접에서 자주 출제되는 Eloquent ORM, 큐 설계, 서비스 컨테이너 관련 질문과 답변 예시를 상세히 설명합니다. N+1 문제 해결부터 잡 배칭, DI 패턴까지 포괄적으로 다룹니다.

PHP Laravel 프레임워크 면접 질문 2026: Eloquent, 큐, 아키텍처 패턴 완벽 가이드

PHP Laravel 프레임워크 면접 질문은 단순한 문법 지식 이상을 요구합니다. 2026년에 채용을 진행하는 기업들은 Eloquent 관계 최적화, 큐 장애 처리, 서비스 컨테이너 바인딩에 대해 프로덕션 수준의 상세한 설명을 기대합니다. Laravel 13에서는 AI 도구, JSON:API 리소스, 벡터 검색 기능이 도입되어 기술 면접에 새로운 차원이 추가되었습니다.

면접관이 평가하는 핵심 영역

시니어 Laravel 포지션에서는 세 가지 영역에 초점을 맞춥니다: Eloquent 성능(N+1 방지, 청킹, 커서 페이지네이션), 큐 신뢰성(재시도, 데드 레터 처리, 잡 배칭), 그리고 아키텍처 결정(서비스 프로바이더, 파사드 vs 인젝션, 리포지토리 패턴).

Eloquent ORM: 관계 로딩과 쿼리 최적화

Eloquent 면접 질문에서는 N+1 문제가 일관되게 다뤄집니다. 지원자는 with(), load(), loadMissing()을 언제 사용하는지, 그리고 대규모 데이터셋의 이거 로딩에서 메모리 트레이드오프를 설명할 수 있어야 합니다.

UserController.phpphp
// 불필요한 데이터 로딩을 피하기 위해 제약 조건과 함께 이거 로드
$users = User::query()
    ->with(['posts' => function (Builder $query) {
        $query->where('published', true)
              ->select('id', 'user_id', 'title', 'published_at');
    }])
    ->withCount('posts')
    ->paginate(25);

// 잘못된 예: 루프 내에서 posts에 접근하면 N+1 발생
foreach ($users as $user) {
    $user->posts; // 각 반복마다 데이터베이스에 쿼리 실행
}

withCount() 메서드는 관련 레코드를 로드하지 않고 카운트하는 서브쿼리를 추가합니다. 이를 통해 목록 페이지에서 카운트를 표시할 때 메모리 오버헤드를 방지할 수 있습니다. 접근되지 않을 수 있는 관계에는 loadMissing()을 사용하여 상위에서 이미 이거 로드된 경우 중복 쿼리를 방지할 수 있습니다.

자주 나오는 후속 질문으로 커서 페이지네이션과 오프셋 페이지네이션의 차이가 있습니다. 커서 페이지네이션은 cursorPaginate()를 사용하며, 대규모 테이블에서 오프셋 페이지네이션이 겪는 급격한 성능 저하를 방지합니다. 트레이드오프로, 커서 페이지네이션에서는 임의 페이지로의 점프가 불가능합니다.

고급 Eloquent: 다형성 관계와 쿼리 스코프

다형성 관계에서는 morph 맵과 인덱스 전략에 대한 정확한 이해가 필요합니다. 면접관은 게시물, 동영상, 제품에 속하는 댓글 시나리오를 제시하고 지원자에게 쿼리 최적화를 요청하는 경우가 많습니다.

Comment.phpphp
class Comment extends Model
{
    // morphTo는 commentable_type 열에서 부모 타입을 자동으로 해석
    public function commentable(): MorphTo
    {
        return $this->morphTo();
    }
}

// AppServiceProvider.php
// morph 맵은 데이터베이스에 클래스명 노출을 방지하고 쿼리 성능을 향상
Relation::enforceMorphMap([
    'post' => Post::class,
    'video' => Video::class,
    'product' => Product::class,
]);

morph 맵은 두 가지 목적을 제공합니다: 데이터베이스 값을 클래스명에서 분리하고(마이그레이션 없이 리팩토링 가능), 더 짧고 인덱싱 가능한 문자열을 생성합니다. morph 맵이 없으면 Laravel은 App\Models\Post와 같은 전체 클래스명을 commentable_type 열에 저장합니다.

쿼리 스코프는 재사용 가능한 쿼리 로직의 이해를 보여줍니다. 글로벌 스코프는 자동으로 적용되고, 로컬 스코프는 명시적 호출이 필요합니다. 전형적인 면접 질문에서는 각각을 언제 사용하는지 묻습니다.

Post.phpphp
// 글로벌 스코프: 명시적으로 제거하지 않는 한 모든 쿼리에 적용
protected static function booted(): void
{
    static::addGlobalScope('published', function (Builder $builder) {
        $builder->where('published', true);
    });
}

// 로컬 스코프: ->popular()로 명시적으로 호출
public function scopePopular(Builder $query, int $minViews = 1000): Builder
{
    return $query->where('views', '>=', $minViews);
}

// 사용 예: Post::popular(5000)->get()
// 글로벌 스코프 제거: Post::withoutGlobalScope('published')->get()

큐 아키텍처: 잡 설계와 장애 처리

큐 관련 질문에서는 프로덕션 대응 능력이 평가됩니다. Laravel 13.31에서는 연결의 모든 잡을 카운트하는 Queue::totalSize()와 그레이스풀 셧다운 처리를 위한 JobInterrupted 이벤트가 추가되었습니다. 지원자는 sync, database, Redis, SQS 드라이버의 차이점과 각각이 적합한 상황을 이해해야 합니다.

Laravel 큐 패턴에 대한 더 자세한 내용은 큐 & 잡 면접 질문 모듈을 참조하시기 바랍니다.

ProcessOrder.phpphp
class ProcessOrder implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 3;           // 실패 전 최대 시도 횟수
    public int $backoff = 60;        // 재시도 간 초
    public int $timeout = 120;       // 최대 실행 시간
    public int $maxExceptions = 2;   // 2회 미처리 예외 후 실패

    public function __construct(
        public readonly Order $order
    ) {}

    public function handle(PaymentGateway $gateway): void
    {
        // SerializesModels는 잡 실행 시 Order를 데이터베이스에서 새로 가져옴
        $gateway->charge($this->order);
    }

    public function failed(Throwable $exception): void
    {
        // 모든 재시도 실패 후 호출 - 팀 알림, 환불 등
        Log::critical('Order processing failed', [
            'order_id' => $this->order->id,
            'exception' => $exception->getMessage(),
        ]);
    }
}

SerializesModels 트레이트는 전체 모델이 아닌 모델 ID만 큐 페이로드에 저장합니다. 잡 실행 시 Eloquent가 새 인스턴스를 가져옵니다. 이를 통해 오래된 데이터를 방지할 수 있지만, 삭제된 모델은 ModelNotFoundException을 발생시킵니다. 이는 deleteWhenMissingModels로 처리합니다.

잡 배칭 관련 질문도 자주 출제됩니다. 배치는 잡을 그룹화하고 완료, 실패, 취소 시 콜백을 정의할 수 있습니다.

OrderBatchController.phpphp
public function processBatch(array $orderIds): PendingBatch
{
    $jobs = collect($orderIds)
        ->map(fn (int $id) => new ProcessOrder(Order::find($id)));

    return Bus::batch($jobs)
        ->name('Process Orders ' . now()->toDateString())
        ->allowFailures()  // 일부 잡이 실패해도 배치 계속
        ->then(function (Batch $batch) {
            // 모든 잡이 성공적으로 완료
            Notification::send(Admin::all(), new BatchCompleted($batch));
        })
        ->catch(function (Batch $batch, Throwable $e) {
            // 배치 내 첫 번째 실패
            Log::warning('Batch job failed', ['batch_id' => $batch->id]);
        })
        ->finally(function (Batch $batch) {
            // 배치 종료(성공 또는 실패)
        })
        ->dispatch();
}

Laravel 면접 준비가 되셨나요?

인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.

서비스 컨테이너: 바인딩, 컨텍스트 인젝션, 프로바이더

서비스 컨테이너는 Laravel의 핵심입니다. 면접 질문에서는 바인딩 타입, 해석 순서, 각 접근 방식을 언제 사용하는지에 대한 이해가 테스트됩니다.

AppServiceProvider.phpphp
public function register(): void
{
    // 싱글톤: 요청 라이프사이클 전체에서 동일한 인스턴스
    $this->app->singleton(PaymentGateway::class, function (Application $app) {
        return new StripeGateway(
            apiKey: config('services.stripe.secret'),
            logger: $app->make(LoggerInterface::class)
        );
    });

    // 바인드: 해석할 때마다 새 인스턴스
    $this->app->bind(ReportGenerator::class, function (Application $app) {
        return new PdfReportGenerator($app->make(ViewFactory::class));
    });

    // 컨텍스트 바인딩: 컨슈머별로 다른 구현
    $this->app->when(PhotoController::class)
        ->needs(Filesystem::class)
        ->give(fn () => Storage::disk('photos'));

    $this->app->when(DocumentController::class)
        ->needs(Filesystem::class)
        ->give(fn () => Storage::disk('documents'));
}

컨텍스트 바인딩은 별도의 인터페이스를 생성하지 않고 다른 클래스에 다른 구현을 인젝트하는 문제를 해결합니다. 컨테이너는 어떤 클래스가 요청하는지에 따라 Filesystem을 다르게 해석합니다.

시니어 레벨 질문에서는 지연 프로바이더에 대해 묻습니다. 표준 서비스 프로바이더는 서비스가 사용되지 않더라도 모든 요청에서 등록됩니다. 지연 프로바이더는 선언된 서비스 중 하나가 해석될 때만 등록됩니다.

ReportingServiceProvider.phpphp
class ReportingServiceProvider extends ServiceProvider implements DeferrableProvider
{
    public function register(): void
    {
        $this->app->singleton(ReportingService::class, function () {
            return new ReportingService(/* 무거운 초기화 */);
        });
    }

    // ReportingService가 요청될 때만 컨테이너가 이 프로바이더를 로드
    public function provides(): array
    {
        return [ReportingService::class];
    }
}

미들웨어: 요청/응답 흐름과 터미네이블 미들웨어

미들웨어 질문에서는 요청 라이프사이클에 대한 이해도를 확인합니다. 미들웨어는 컨트롤러 전후 모두에서 코드를 실행할 수 있으며, 터미네이블 미들웨어는 응답이 클라이언트에 전송된 후 실행됩니다.

LogRequestMetrics.phpphp
class LogRequestMetrics
{
    public function handle(Request $request, Closure $next): Response
    {
        $request->attributes->set('start_time', microtime(true));
        
        // 다음 미들웨어/컨트롤러로 진행
        return $next($request);
    }

    // 터미네이블: 응답 전송 후 실행
    public function terminate(Request $request, Response $response): void
    {
        $duration = microtime(true) - $request->attributes->get('start_time');
        
        Log::info('Request completed', [
            'path' => $request->path(),
            'status' => $response->getStatusCode(),
            'duration_ms' => round($duration * 1000, 2),
        ]);
    }
}

터미네이블 미들웨어는 분석, 로깅, 정리 등 응답 시간에 영향을 주지 않아야 하는 작업에 유용합니다. terminate 메서드는 응답이 클라이언트에 전송된 후 컨테이너에 의해 호출됩니다.

리포지토리 패턴: 사용해야 할 때와 피해야 할 때

리포지토리 패턴은 논쟁의 대상입니다. Laravel의 Eloquent는 이미 Active Record를 구현하고 있습니다. 리포지토리를 추가하면 일부 팀이 불필요하다고 느끼는 추상화가 생깁니다. 면접관은 지원자에게 자신의 입장을 정당화하도록 요청합니다.

리포지토리의 장점:

  • 비즈니스 로직을 Eloquent에서 분리하여 데이터베이스 전환 가능(실제로는 드묾)
  • 리포지토리 인터페이스를 목킹하여 테스트 단순화
  • 여러 컨트롤러가 유사한 쿼리를 사용할 때 쿼리 로직 중앙화

반대 의견:

  • CRUD 작업에 대해 명확한 이점 없이 보일러플레이트 추가
  • Eloquent의 쿼리 빌더는 이미 표현력 있고 테스트 가능
  • Laravel의 DI는 모델과 직접 작동 가능
OrderRepository.php(리포지토리가 가치를 추가하는 경우)php
class OrderRepository
{
    public function __construct(
        private readonly Order $model
    ) {}

    public function findPendingForUser(User $user): Collection
    {
        return $this->model->query()
            ->where('user_id', $user->id)
            ->where('status', OrderStatus::Pending)
            ->with('items.product')
            ->orderByDesc('created_at')
            ->get();
    }

    public function calculateRevenueForPeriod(CarbonPeriod $period): Money
    {
        $total = $this->model->query()
            ->whereBetween('completed_at', [$period->start, $period->end])
            ->where('status', OrderStatus::Completed)
            ->sum('total_cents');

        return Money::ofMinor($total, 'USD');
    }
}

균형 잡힌 답변: 쿼리 로직이 복잡하거나 컨텍스트 간에 공유되거나 팀이 인터페이스 목킹을 통한 테스트 용이성을 우선시할 때 리포지토리는 가치를 추가합니다. 단순한 CRUD의 경우 모델을 직접 인젝트하여 코드를 간결하게 유지할 수 있습니다.

테스트: 피처 테스트, 목킹, 데이터베이스 전략

Laravel 테스트 관련 질문에서는 지원자가 신뢰할 수 있고 빠른 테스트를 작성할 수 있는지 검증합니다. 프레임워크는 서로 다른 트레이드오프를 가진 RefreshDatabase, DatabaseTransactions, LazilyRefreshDatabase 트레이트를 제공합니다.

OrderTest.phpphp
class OrderTest extends TestCase
{
    use RefreshDatabase;  // 클래스당 1회 마이그레이션, 테스트당 트랜잭션

    public function test_user_can_place_order(): void
    {
        // Arrange
        $user = User::factory()->create();
        $product = Product::factory()->create(['price_cents' => 2999]);

        // Act
        $response = $this->actingAs($user)
            ->postJson('/api/orders', [
                'items' => [
                    ['product_id' => $product->id, 'quantity' => 2],
                ],
            ]);

        // Assert
        $response->assertCreated()
            ->assertJsonPath('data.total_cents', 5998);

        $this->assertDatabaseHas('orders', [
            'user_id' => $user->id,
            'status' => 'pending',
        ]);
    }

    public function test_order_dispatches_processing_job(): void
    {
        Queue::fake();

        $user = User::factory()->create();
        $order = Order::factory()->for($user)->create();

        $this->actingAs($user)
            ->postJson("/api/orders/{$order->id}/process");

        Queue::assertPushed(ProcessOrder::class, function ($job) use ($order) {
            return $job->order->id === $order->id;
        });
    }
}

RefreshDatabase 트레이트는 테스트 클래스당 1회 마이그레이션을 실행하고 각 테스트를 롤백하는 트랜잭션으로 래핑합니다. 이를 통해 속도와 격리의 균형을 맞춥니다. 대규모 테스트 스위트에서는 LazilyRefreshDatabase가 스키마가 일치할 때 마이그레이션을 건너뜁니다.

외부 서비스 목킹은 테스트가 실제 API에 접근하는 것을 방지합니다.

PaymentTest.phpphp
public function test_payment_failure_returns_error(): void
{
    $gateway = $this->mock(PaymentGateway::class);
    $gateway->shouldReceive('charge')
        ->once()
        ->andThrow(new PaymentFailedException('Card declined'));

    $user = User::factory()->create();
    $order = Order::factory()->for($user)->create();

    $response = $this->actingAs($user)
        ->postJson("/api/orders/{$order->id}/pay");

    $response->assertStatus(402)
        ->assertJsonPath('error', 'Card declined');
}

연습을 시작하세요!

면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.

면접에서 출제될 가능성이 높은 Laravel 13 기능

Laravel 13은 2026년 3월에 릴리스되었으며, 면접관이 지원자에게 이해를 기대하는 기능들을 포함하고 있습니다. 공식 릴리스 노트에서 이에 대한 자세한 내용을 확인할 수 있습니다.

AI SDK: Laravel 13에는 텍스트 생성, 임베딩, 도구 호출 에이전트를 위한 통합 API를 갖춘 퍼스트파티 AI 도구가 포함되어 있습니다. 기존 애플리케이션에 AI 기능 통합에 대한 질문이 나올 수 있습니다.

JSON:API 리소스: 새로운 JsonApiResource 클래스는 관계, 스파스 필드셋, 컴파운드 문서를 포함한 JSON:API 준수 응답 구축을 단순화합니다.

벡터 검색: AsVector Eloquent 캐스트는 시맨틱 검색을 위한 벡터 열을 처리합니다. PostgreSQL pgvector 및 MariaDB의 바이너리 벡터 형식과 함께 작동합니다.

Cloud 파사드: Cloud::hosted()는 Laravel Cloud 배포를 감지하고, Cloud::usesManagedQueues()는 관리형 큐 연결을 확인합니다. 이를 통해 설정 확인 없이 환경별 동작이 가능합니다.

Laravel 면접 준비 핵심 사항

  • with(), loadMissing(), withCount()를 사용한 N+1 방지를 설명합니다. 대규모 데이터셋의 이거 로딩에서 메모리 영향을 이해합니다.
  • 큐 잡 설계를 설명합니다: 재시도 전략, SerializesModels 동작, 배칭 콜백, deleteWhenMissingModels 사용 시점.
  • 서비스 컨테이너 바인딩 타입을 명확히 합니다: 싱글톤 vs 바인드, 컨텍스트 바인딩, 성능 향상을 위한 지연 프로바이더.
  • 미들웨어 요청/응답 흐름과 응답 후 작업을 위한 터미네이블 미들웨어를 시연합니다.
  • 교조적인 규칙보다 구체적인 트레이드오프로 리포지토리 패턴에 대한 입장을 취합니다.
  • RefreshDatabase를 적절히 사용하고, 외부 서비스를 목킹하며, 큐 디스패치를 어설션하는 테스트를 작성합니다.
  • Laravel 13 기능, 특히 AI SDK, JSON:API 리소스, 벡터 검색 기능에 대한 최신 지식을 유지합니다.
오늘의 챌린지

Laravel 코드의 버그를 찾을 수 있나요

실제 코드 한 조각, 숨은 버그 하나, 하루 한 번. 계정 없이 바로 도전할 수 있습니다.

Anthony Fillion-Maillet

작성자

Anthony Fillion-Maillet

SharpSkill 창업자

10년 이상 풀스택 개발을 해왔습니다. SharpSkill을 운영하며 이곳에 게시되는 모든 내용에 책임을 집니다.

2026년 9월 16일 업데이트

공유

관련 기사