서비스 컨테이너

번역일: 2026년 7월 2일

서비스 컨테이너

소개

Laravel 서비스 컨테이너는 클래스 의존성 관리와 의존성 주입(Dependency Injection)을 위한 강력한 도구입니다. 의존성 주입이란, 클래스가 필요로 하는 의존 객체를 클래스 내부에서 직접 생성하지 않고, 생성자나 setter 메서드를 통해 외부에서 "주입"받는 방식을 말합니다.

간단한 예시를 살펴보겠습니다.

<?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 같은 외부 소스에서 팟캐스트 데이터를 가져와야 합니다. 그래서 해당 기능을 담당하는 서비스를 주입받습니다. 이렇게 하면 테스트 시 AppleMusic 서비스를 가짜(mock) 구현체로 손쉽게 교체할 수 있다는 장점이 있습니다.

서비스 컨테이너를 깊이 이해하면 규모 있는 애플리케이션을 구축하거나 Laravel 코어에 기여할 때 큰 도움이 됩니다.

설정 없는 자동 해결

클래스가 의존성이 없거나, 인터페이스가 아닌 구체 클래스에만 의존한다면 컨테이너에 별도로 등록하지 않아도 자동으로 해결됩니다. 예를 들어, routes/web.php에 다음과 같이 작성할 수 있습니다.

<?php class Service { // ... } Route::get('/', function (Service $service) { dd($service::class); });

/ 라우트에 접근하면 컨테이너가 Service 클래스를 자동으로 해결해 라우트 핸들러에 주입합니다. 복잡한 설정 파일 없이도 의존성 주입을 바로 활용할 수 있다는 뜻입니다.

컨트롤러, 이벤트 리스너, 미들웨어 등 Laravel에서 작성하는 대부분의 클래스는 컨테이너를 통해 자동으로 의존성을 주입받습니다. 큐 Jobhandle 메서드에서도 타입 힌트로 의존성을 주입받을 수 있습니다. 한 번 이 편리함에 익숙해지면 이전 방식으로 돌아가기 어렵습니다.

컨테이너를 직접 사용해야 할 때

자동 해결 덕분에 대부분의 경우 컨테이너를 직접 다루지 않아도 됩니다. 라우트, 컨트롤러, 이벤트 리스너 등에서 타입 힌트만 작성하면 컨테이너가 알아서 처리합니다. 예를 들어 라우트에서 Illuminate\Http\Request를 타입 힌트로 선언하면 현재 요청 객체를 손쉽게 받을 수 있습니다.

use Illuminate\Http\Request; Route::get('/', function (Request $request) { // ... });

자동 의존성 주입과 파사드 덕분에 컨테이너에 직접 바인딩하거나 해결하는 코드를 작성하지 않아도 될 때가 많습니다. 그렇다면 언제 컨테이너를 직접 다뤄야 할까요? 크게 두 가지 상황이 있습니다.

첫째, 인터페이스를 구현한 클래스를 작성하고, 라우트나 생성자에서 그 인터페이스를 타입 힌트로 사용할 때는 컨테이너에 인터페이스와 구현체의 관계를 등록해야 합니다. 둘째, 다른 Laravel 개발자와 공유할 패키지를 개발할 때는 패키지의 서비스를 컨테이너에 바인딩해야 할 수 있습니다.

바인딩

바인딩 기초

기본 바인딩

서비스 컨테이너 바인딩은 대부분 서비스 프로바이더 안에서 등록합니다. 이 섹션의 예시도 서비스 프로바이더를 기준으로 작성합니다.

서비스 프로바이더 내에서는 $this->app 프로퍼티로 컨테이너에 접근할 수 있습니다. 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) { // ... });

해당 타입에 대한 바인딩이 아직 없을 때만 등록하려면 bindIf 메서드를 사용하세요.

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

편의상 클래스나 인터페이스 이름을 별도 인자로 전달하지 않고, 클로저의 반환 타입으로 Laravel이 타입을 추론하도록 할 수도 있습니다.

App::bind(function (Application $app): Transistor { return new Transistor($app->make(PodcastParser::class)); });

NOTE

인터페이스에 의존하지 않는 클래스는 컨테이너에 따로 바인딩할 필요가 없습니다. 컨테이너가 리플렉션(Reflection)을 이용해 자동으로 해결할 수 있기 때문입니다.

싱글톤 바인딩

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)); });

해당 타입에 대한 바인딩이 없을 때만 싱글톤으로 등록하려면 singletonIf를 사용하세요.

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

Singleton 어트리뷰트

클래스나 인터페이스에 #[Singleton] PHP 어트리뷰트를 붙여서 컨테이너에 싱글톤으로 해결되어야 함을 직접 명시할 수도 있습니다.

<?php namespace App\Services; use Illuminate\Container\Attributes\Singleton; #[Singleton] class Transistor { // ... }

스코프 싱글톤 바인딩

scoped 메서드는 하나의 Laravel 요청(request) 또는 Job 라이프사이클 내에서만 한 번 해결되도록 바인딩합니다. singleton과 비슷하지만, Laravel Octane 워커가 새 요청을 처리하거나 큐 워커가 새 Job을 처리할 때처럼 새로운 라이프사이클이 시작되면 인스턴스가 초기화됩니다.

use App\Services\Transistor; use App\Services\PodcastParser; use Illuminate\Contracts\Foundation\Application; $this->app->scoped(Transistor::class, function (Application $app) { return new Transistor($app->make(PodcastParser::class)); });

해당 타입에 대한 바인딩이 없을 때만 스코프 바인딩으로 등록하려면 scopedIf를 사용하세요.

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

Scoped 어트리뷰트

#[Scoped] 어트리뷰트를 사용해 클래스나 인터페이스에 직접 스코프 싱글톤임을 명시할 수도 있습니다.

<?php namespace App\Services; use Illuminate\Container\Attributes\Scoped; #[Scoped] class Transistor { // ... }

인스턴스 바인딩

이미 생성된 객체 인스턴스를 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, ) {}

Bind 어트리뷰트

서비스 프로바이더에 별도 등록 없이, 인터페이스에 #[Bind] 어트리뷰트를 붙여서 어떤 구현체를 주입할지 직접 지정할 수도 있습니다.

또한 환경별로 다른 구현체를 주입하도록 여러 #[Bind] 어트리뷰트를 함께 사용할 수 있습니다.

<?php namespace App\Contracts; use App\Services\FakeEventPusher; use App\Services\RedisEventPusher; use Illuminate\Container\Attributes\Bind; #[Bind(RedisEventPusher::class)] #[Bind(FakeEventPusher::class, environments: ['local', 'testing'])] interface EventPusher { // ... }

#[Singleton]이나 #[Scoped] 어트리뷰트와 함께 사용하면 바인딩 해결 방식도 함께 지정할 수 있습니다.

use App\Services\RedisEventPusher; use Illuminate\Container\Attributes\Bind; use Illuminate\Container\Attributes\Singleton; #[Bind(RedisEventPusher::class)] #[Singleton] interface EventPusher { // ... }

컨텍스트 기반 바인딩

같은 인터페이스를 사용하더라도 클래스마다 다른 구현체를 주입하고 싶을 때가 있습니다. 예를 들어 두 컨트롤러가 동일한 Illuminate\Contracts\Filesystem\Filesystem 컨트랙트를 사용하지만 각각 다른 스토리지 디스크를 써야 한다면 다음과 같이 설정할 수 있습니다.

use App\Http\Controllers\PhotoController; use App\Http\Controllers\UploadController; use App\Http\Controllers\VideoController; use Illuminate\Contracts\Filesystem\Filesystem; use Illuminate\Support\Facades\Storage; $this->app->when(PhotoController::class) ->needs(Filesystem::class) ->give(function () { return Storage::disk('local'); }); $this->app->when([VideoController::class, UploadController::class]) ->needs(Filesystem::class) ->give(function () { return Storage::disk('s3'); });

컨텍스트 어트리뷰트

드라이버 구현체나 설정 값을 주입할 때 서비스 프로바이더에 컨텍스트 바인딩을 일일이 작성하는 대신, 어트리뷰트를 사용하면 더 간결하게 처리할 수 있습니다.

예를 들어 #[Storage] 어트리뷰트로 특정 스토리지 디스크를 주입할 수 있습니다.

<?php namespace App\Http\Controllers; use Illuminate\Container\Attributes\Storage; use Illuminate\Contracts\Filesystem\Filesystem; class PhotoController extends Controller { public function __construct( #[Storage('local')] protected Filesystem $filesystem ) { // ... } }

Storage 외에도 Auth, Cache, Config, Context, DB, Give, Log, RouteParameter, Tag 어트리뷰트를 제공합니다.

<?php namespace App\Http\Controllers; use App\Contracts\UserRepository; use App\Models\Photo; use App\Repositories\DatabaseRepository; use Illuminate\Container\Attributes\Auth; use Illuminate\Container\Attributes\Cache; use Illuminate\Container\Attributes\Config; use Illuminate\Container\Attributes\Context; use Illuminate\Container\Attributes\DB; use Illuminate\Container\Attributes\Give; use Illuminate\Container\Attributes\Log; use Illuminate\Container\Attributes\RouteParameter; use Illuminate\Container\Attributes\Tag; use Illuminate\Contracts\Auth\Guard; use Illuminate\Contracts\Cache\Repository; use Illuminate\Database\Connection; use Psr\Log\LoggerInterface; class PhotoController extends Controller { public function __construct( #[Auth('web')] protected Guard $auth, #[Cache('redis')] protected Repository $cache, #[Config('app.timezone')] protected string $timezone, #[Context('uuid')] protected string $uuid, #[Context('ulid', hidden: true)] protected string $ulid, #[DB('mysql')] protected Connection $connection, #[Give(DatabaseRepository::class)] protected UserRepository $users, #[Log('daily')] protected LoggerInterface $log, #[RouteParameter('photo')] protected Photo $photo, #[Tag('reports')] protected iterable $reports, ) { // ... } }

현재 인증된 사용자를 주입받으려면 #[CurrentUser] 어트리뷰트를 사용하세요.

use App\Models\User; use Illuminate\Container\Attributes\CurrentUser; Route::get('/user', function (#[CurrentUser] User $user) { return $user; })->middleware('auth');

커스텀 어트리뷰트 정의하기

Illuminate\Contracts\Container\ContextualAttribute 컨트랙트를 구현해 직접 컨텍스트 어트리뷰트를 만들 수 있습니다. 컨테이너는 어트리뷰트의 resolve 메서드를 호출하여 주입할 값을 가져옵니다. 아래는 Laravel 기본 제공 Config 어트리뷰트를 직접 구현한 예시입니다.

<?php namespace App\Attributes; use Attribute; use Illuminate\Contracts\Container\Container; use Illuminate\Contracts\Container\ContextualAttribute; #[Attribute(Attribute::TARGET_PARAMETER)] class Config implements ContextualAttribute { /** * 새 어트리뷰트 인스턴스를 생성합니다. */ public function __construct(public string $key, public mixed $default = null) { } /** * 설정 값을 해결합니다. * * @param self $attribute * @param \Illuminate\Contracts\Container\Container $container * @return mixed */ public static function resolve(self $attribute, Container $container) { return $container->make('config')->get($attribute->key, $attribute->default); } }

기본값(Primitive) 바인딩

클래스가 다른 클래스뿐만 아니라 정수나 문자열 같은 기본값도 생성자 인자로 받아야 할 때, 컨텍스트 바인딩을 이용해 값을 주입할 수 있습니다.

use App\Http\Controllers\UserController; $this->app->when(UserController::class) ->needs('$variableName') ->give($value);

클래스가 태그된 인스턴스 배열에 의존한다면 giveTagged 메서드로 해당 태그의 모든 바인딩을 한 번에 주입할 수 있습니다.

$this->app->when(ReportAggregator::class) ->needs('$reports') ->giveTagged('reports');

애플리케이션 설정 파일의 값을 주입하려면 giveConfig 메서드를 사용하세요.

$this->app->when(ReportAggregator::class) ->needs('$timezone') ->giveConfig('app.timezone');

타입이 지정된 가변 인자 바인딩

생성자에서 타입이 지정된 가변 인자(...$args)로 객체 배열을 받는 클래스가 있을 수 있습니다.

<?php use App\Models\Filter; use App\Services\Logger; class Firewall { /** * 필터 인스턴스 배열 * * @var array */ protected $filters; /** * 새 클래스 인스턴스를 생성합니다. */ public function __construct( protected Logger $logger, Filter ...$filters, ) { $this->filters = $filters; } }

컨텍스트 바인딩과 give 메서드에 클로저를 전달해 해결된 Filter 인스턴스 배열을 반환할 수 있습니다.

$this->app->when(Firewall::class) ->needs(Filter::class) ->give(function (Application $app) { return [ $app->make(NullFilter::class), $app->make(ProfanityFilter::class), $app->make(TooLongFilter::class), ]; });

더 간단하게, 클래스 이름 배열만 전달해도 컨테이너가 자동으로 해결합니다.

$this->app->when(Firewall::class) ->needs(Filter::class) ->give([ NullFilter::class, ProfanityFilter::class, TooLongFilter::class, ]);

태그 기반 가변 인자 의존성

가변 인자 의존성이 특정 타입(Report ...$reports)으로 선언되어 있을 때, needsgiveTagged를 조합하면 해당 태그로 등록된 모든 바인딩을 한 번에 주입할 수 있습니다.

$this->app->when(ReportAggregator::class) ->needs(Report::class) ->giveTagged('reports');

태깅

같은 "범주"에 속하는 바인딩 전체를 한 번에 해결해야 할 때가 있습니다. 예를 들어 여러 Report 인터페이스 구현체를 배열로 받는 리포트 분석기를 만들 때, 구현체들을 등록한 뒤 tag 메서드로 태그를 붙일 수 있습니다.

$this->app->bind(CpuReport::class, function () { // ... }); $this->app->bind(MemoryReport::class, function () { // ... }); $this->app->tag([CpuReport::class, MemoryReport::class], 'reports');

태그가 지정된 서비스들은 tagged 메서드로 한 번에 해결할 수 있습니다.

$this->app->bind(ReportAnalyzer::class, function (Application $app) { return new ReportAnalyzer($app->tagged('reports')); });

바인딩 확장

extend 메서드를 사용하면 이미 해결된 서비스를 꾸미거나(decorate) 추가 설정할 수 있습니다. 클로저는 해결된 서비스와 컨테이너 인스턴스를 인자로 받습니다.

$this->app->extend(Service::class, function (Service $service, Application $app) { return new DecoratedService($service); });

해결(Resolving)

`make` 메서드

make 메서드로 컨테이너에서 클래스 인스턴스를 직접 가져올 수 있습니다. 해결할 클래스나 인터페이스 이름을 전달합니다.

use App\Services\Transistor; $transistor = $this->app->make(Transistor::class);

컨테이너가 자동으로 해결할 수 없는 생성자 인자가 있다면, makeWith 메서드에 연관 배열로 직접 전달할 수 있습니다.

use App\Services\Transistor; $transistor = $this->app->makeWith(Transistor::class, ['id' => 1]);

bound 메서드로 클래스나 인터페이스가 컨테이너에 명시적으로 바인딩되어 있는지 확인할 수 있습니다.

if ($this->app->bound(Transistor::class)) { // ... }

서비스 프로바이더 외부에서 $app 변수에 접근할 수 없는 경우, App 파사드app 헬퍼를 사용하세요.

use App\Services\Transistor; use Illuminate\Support\Facades\App; $transistor = App::make(Transistor::class); $transistor = app(Transistor::class);

컨테이너 인스턴스 자체를 주입받고 싶다면 생성자에서 Illuminate\Container\Container를 타입 힌트로 사용하면 됩니다.

use Illuminate\Container\Container; /** * 새 클래스 인스턴스를 생성합니다. */ public function __construct( protected Container $container, ) {}

자동 주입

실제로 가장 많이 사용하는 방식은 클래스 생성자에 타입 힌트를 작성하는 것입니다. 컨트롤러, 이벤트 리스너, 미들웨어, 큐 Job의 handle 메서드 등에서 모두 이 방식을 사용할 수 있으며, 컨테이너가 자동으로 의존성을 해결해 주입합니다.

<?php namespace App\Http\Controllers; use App\Services\AppleMusic; class PodcastController extends Controller { /** * 새 컨트롤러 인스턴스를 생성합니다. */ public function __construct( protected AppleMusic $apple, ) {} /** * 주어진 팟캐스트 정보를 반환합니다. */ public function show(string $id): Podcast { return $this->apple->findPodcast($id); } }

메서드 호출과 주입

컨테이너를 통해 객체의 메서드를 호출하면서 해당 메서드의 의존성을 자동으로 주입하고 싶을 때가 있습니다. 예를 들어 다음 클래스가 있다고 가정합니다.

<?php namespace App; use App\Services\AppleMusic; class PodcastStats { /** * 팟캐스트 통계 리포트를 생성합니다. */ public function generate(AppleMusic $apple): array { return [ // ... ]; } }

컨테이너의 call 메서드로 이 메서드를 호출하면, AppleMusic 의존성이 자동으로 주입됩니다.

use App\PodcastStats; use Illuminate\Support\Facades\App; $stats = App::call([new PodcastStats, 'generate']);

call 메서드는 PHP callable이라면 무엇이든 받을 수 있으며, 클로저의 의존성도 자동으로 주입합니다.

use App\Services\AppleMusic; use Illuminate\Support\Facades\App; $result = App::call(function (AppleMusic $apple) { // ... });

컨테이너 이벤트

서비스 컨테이너는 객체를 해결할 때마다 이벤트를 발생시킵니다. resolving 메서드로 이 이벤트를 수신할 수 있습니다.

use App\Services\Transistor; use Illuminate\Contracts\Foundation\Application; $this->app->resolving(Transistor::class, function (Transistor $transistor, Application $app) { // 컨테이너가 Transistor 타입의 객체를 해결할 때 호출됩니다... }); $this->app->resolving(function (mixed $object, Application $app) { // 컨테이너가 어떤 타입의 객체든 해결할 때 호출됩니다... });

콜백에는 해결된 객체가 전달되므로, 실제 사용자에게 전달되기 전에 추가 프로퍼티를 설정하는 등의 작업을 할 수 있습니다.

리바인딩

rebinding 메서드를 사용하면 이미 등록된 서비스가 다시 바인딩될 때(기존 바인딩이 덮어씌워질 때)를 감지할 수 있습니다. 특정 바인딩이 변경될 때마다 의존성을 갱신하거나 동작을 수정해야 할 경우 유용합니다.

use App\Contracts\PodcastPublisher; use App\Services\SpotifyPublisher; use App\Services\TransistorPublisher; use Illuminate\Contracts\Foundation\Application; $this->app->bind(PodcastPublisher::class, SpotifyPublisher::class); $this->app->rebinding( PodcastPublisher::class, function (Application $app, PodcastPublisher $newInstance) { // }, ); // 새로운 바인딩이 등록되면 rebinding 클로저가 호출됩니다... $this->app->bind(PodcastPublisher::class, TransistorPublisher::class);

PSR-11

Laravel 서비스 컨테이너는 PSR-11 인터페이스를 구현합니다. 따라서 PSR-11 컨테이너 인터페이스를 타입 힌트로 사용해 Laravel 컨테이너 인스턴스를 받을 수 있습니다.

use App\Services\Transistor; use Psr\Container\ContainerInterface; Route::get('/', function (ContainerInterface $container) { $service = $container->get(Transistor::class); // ... });

주어진 식별자를 해결할 수 없으면 예외가 발생합니다. 바인딩 자체가 없었다면 Psr\Container\NotFoundExceptionInterface, 바인딩은 있지만 해결에 실패했다면 Psr\Container\ContainerExceptionInterface가 던져집니다.

이 문서는 Laravel 공식 문서(MIT)를 한국 개발자를 위해 번역·재구성한 것입니다.

번역일: 2026년 7월 2일