메인 콘텐츠로 건너뛰기

서비스 컨테이너란

Laravel의 서비스 컨테이너는 클래스의 의존 관계를 관리하고 의존성 주입을 수행하기 위한 구조입니다. 의존성 주입이란 클래스가 필요로 하는 의존을 생성자나 경우에 따라서는 세터 메서드를 통해 클래스로 “주입”하는 것을 말합니다. 다음 예시를 봅시다.
<?php

namespace App\Http\Controllers;

use App\Services\AppleMusic;
use Illuminate\View\View;

class PodcastController extends Controller
{
    /**
     * 새 컨트롤러 인스턴스를 생성한다
     */
    public function __construct(
        protected AppleMusic $apple,
    ) {}

    /**
     * 지정된 팟캐스트 정보를 표시한다
     */
    public function show(string $id): View
    {
        return view('podcasts.show', [
            'podcast' => $this->apple->findPodcast($id)
        ]);
    }
}
이 예시에서 PodcastController는 Apple Music 같은 데이터 소스에서 Podcast를 가져와야 합니다. 그래서 Podcast를 가져올 수 있는 서비스를 주입합니다. 서비스를 주입함으로써 테스트 시에 AppleMusic 서비스의 모의 객체(더미 구현)를 간단하게 교체할 수 있습니다.
서비스 컨테이너를 깊이 이해하는 것은 대규모 Laravel 애플리케이션을 구축할 때 필수적입니다. Laravel 코어 자체에 기여하는 데도 도움이 됩니다.

제로 컨피규레이션 해결

클래스가 다른 구체 클래스(인터페이스가 아닌)에만 의존하고 있는 경우, 컨테이너에 그 해결 방법을 알려 줄 필요는 없습니다. 예를 들어 다음 코드를 routes/web.php에 작성했다고 합시다.
<?php

class Service
{
    // ...
}

Route::get('/', function (Service $service) {
    dd($service::class);
});
이 예시는 라우트 파일 내에서 클래스를 정의하고 있지만, 이는 데모용입니다. 실제 애플리케이션에서는 서비스 클래스는 app/Services 디렉터리에 정의해 주세요.
이 라우트에 접근하면 Laravel은 자동으로 Service 클래스를 해결해 라우트 핸들러로 주입합니다. 설정 파일을 준비하지 않고도 의존성 주입의 혜택을 누릴 수 있습니다. 컨트롤러, 이벤트 리스너, 미들웨어 등 Laravel 애플리케이션에서 작성하는 클래스의 대부분은 컨테이너를 통해 자동으로 의존이 주입됩니다.

바인딩

기본 바인딩

대부분의 바인딩은 서비스 프로바이더 안에서 등록합니다. 서비스 프로바이더 안에서는 $this->app 프로퍼티를 통해 컨테이너에 접근할 수 있습니다.

bind

bind 메서드를 사용해 클래스나 인터페이스명과 클로저를 전달하여 바인딩을 등록합니다.
use App\Services\Transistor;
use App\Services\PodcastParser;
use Illuminate\Contracts\Foundation\Application;

$this->app->bind(Transistor::class, function (Application $app) {
    return new Transistor($app->make(PodcastParser::class));
});
클로저의 인수로 컨테이너 자체를 받을 수 있습니다. 이를 사용해 하위 의존을 해결할 수 있습니다. 서비스 프로바이더 밖에서 컨테이너를 조작하고 싶은 경우에는 App 파사드를 사용합니다.
use App\Services\Transistor;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\Facades\App;

App::bind(Transistor::class, function (Application $app) {
    // ...
});
인터페이스에 의존하지 않는 클래스는 컨테이너에 바인딩할 필요가 없습니다. 컨테이너는 리플렉션을 사용해 이 객체들을 자동으로 해결할 수 있습니다.

singleton

singleton 메서드는 클래스나 인터페이스를 한 번만 해결하도록 바인딩합니다. 한 번 해결된 싱글턴은 이후의 컨테이너 호출에서 같은 인스턴스가 반환됩니다.
use App\Services\Transistor;
use App\Services\PodcastParser;
use Illuminate\Contracts\Foundation\Application;

$this->app->singleton(Transistor::class, function (Application $app) {
    return new Transistor($app->make(PodcastParser::class));
});

instance

기존의 객체 인스턴스를 instance 메서드로 컨테이너에 바인딩할 수도 있습니다. 이후 컨테이너 호출에서는 항상 그 인스턴스가 반환됩니다.
use App\Services\Transistor;
use App\Services\PodcastParser;

$service = new Transistor(new PodcastParser);

$this->app->instance(Transistor::class, $service);

인터페이스를 구현에 바인딩하기

서비스 컨테이너의 강력한 기능 중 하나는 인터페이스를 특정 구현에 바인딩할 수 있다는 것입니다. 예를 들어 EventPusher 인터페이스와 RedisEventPusher 구현이 있다고 합시다.
use App\Contracts\EventPusher;
use App\Services\RedisEventPusher;

$this->app->bind(EventPusher::class, RedisEventPusher::class);
이를 통해 컨테이너는 EventPusher 구현이 필요한 클래스에 RedisEventPusher를 주입하게 됩니다. 나머지는 생성자에서 EventPusher 인터페이스를 타입 힌트하기만 하면 됩니다.
use App\Contracts\EventPusher;

/**
 * 새 클래스 인스턴스를 생성한다
 */
public function __construct(
    protected EventPusher $pusher,
) {}
인터페이스에 의존함으로써 구현을 교체해도 코드를 변경할 필요가 없어집니다. 이를 통해 테스트나 미래의 변경이 용이해집니다.

자동 해결(타입 힌트에 의한 DI)

서비스 컨테이너는 컨트롤러, 이벤트 리스너, 미들웨어 같은 클래스를 해결할 때 생성자의 타입 힌트를 보고 의존을 자동으로 주입합니다.
<?php

namespace App\Http\Controllers;

use App\Repositories\UserRepository;

class UserController extends Controller
{
    /**
     * 새 컨트롤러 인스턴스를 생성한다
     */
    public function __construct(
        protected UserRepository $users,
    ) {}
}
UserRepository가 인터페이스에 의존하지 않는다면 컨테이너로의 등록은 불필요합니다. 다음 라우트에 접근하는 것만으로 컨테이너가 자동으로 의존을 해결해 컨트롤러에 주입합니다.

컨테이너에서의 해결

make 메서드

make 메서드를 사용해 컨테이너에서 클래스 인스턴스를 해결할 수 있습니다.
use App\Services\Transistor;

$transistor = app()->make(Transistor::class);
클래스의 의존이 컨테이너에서 해결되지 않는 경우, makeWith 메서드를 사용해 추가 인수를 전달할 수도 있습니다.
$transistor = $this->app->makeWith(Transistor::class, ['id' => 1]);

자동 주입

실제로는 make 메서드를 직접 호출하는 경우가 거의 없습니다. 컨테이너가 해결하는 클래스(컨트롤러, 이벤트 리스너, 미들웨어 등)의 생성자에 타입 힌트를 추가하기만 하면 컨테이너가 자동으로 주입해 줍니다.

파사드와 컨테이너의 관계

Laravel의 파사드는 컨테이너 내의 객체로의 정적 인터페이스를 제공합니다. 예를 들어 Cache::get()은 내부적으로 컨테이너에서 Cache 서비스를 가져와 호출하고 있습니다.
use Illuminate\Support\Facades\Cache;

// 파사드를 사용한 호출
Cache::get('key');

// 컨테이너를 직접 사용한 동등한 호출
app('cache')->get('key');
파사드는 컨테이너의 편리한 래퍼입니다. 테스트 시에는 파사드를 모의 객체로 교체할 수도 있습니다.
use Illuminate\Support\Facades\Cache;

Cache::shouldReceive('get')
    ->once()
    ->with('key')
    ->andReturn('value');

생성자 주입의 실전 예시

실제 애플리케이션에서의 전형적인 패턴을 봅시다.
1

인터페이스를 정의한다

<?php

namespace App\Contracts;

interface PaymentGateway
{
    public function charge(int $amount, string $token): bool;
}
2

구현 클래스를 생성한다

<?php

namespace App\Services;

use App\Contracts\PaymentGateway;

class StripePaymentGateway implements PaymentGateway
{
    public function charge(int $amount, string $token): bool
    {
        // Stripe API를 사용한 결제 처리...
        return true;
    }
}
3

서비스 프로바이더에서 바인딩한다

use App\Contracts\PaymentGateway;
use App\Services\StripePaymentGateway;

$this->app->singleton(PaymentGateway::class, StripePaymentGateway::class);
4

컨트롤러에서 주입을 받는다

<?php

namespace App\Http\Controllers;

use App\Contracts\PaymentGateway;
use Illuminate\Http\Request;

class OrderController extends Controller
{
    public function __construct(
        protected PaymentGateway $payment,
    ) {}

    public function store(Request $request)
    {
        $this->payment->charge(
            $request->amount,
            $request->payment_token
        );

        // ...
    }
}
이 패턴을 통해 결제 서비스를 Stripe에서 다른 프로바이더로 전환하는 경우에도 바인딩을 한 곳만 변경하면 대응할 수 있습니다.

다음 단계

서비스 프로바이더

서비스 프로바이더를 사용해 바인딩을 등록하는 방법을 배웁니다.
마지막 수정일 2026년 7월 13일