본문 바로가기

서비스 컨테이너

번역일: 2026년 6월 20일

서비스 컨테이너

소개

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 애플리케이션을 잘 설계할 수 있을 뿐만 아니라, Laravel 코어에 기여할 때도 큰 도움이 됩니다.

설정 없는 자동 해석

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

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

/ 라우트에 접근하면 컨테이너가 Service 클래스를 자동으로 해석해 라우트 핸들러에 주입합니다. 설정 파일을 따로 작성할 필요가 없다는 점이 큰 장점입니다.

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

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

설정 없는 자동 해석 덕분에, 대부분의 경우 라우트·컨트롤러·이벤트 리스너 등에서 타입 힌트만 작성하면 컨테이너를 직접 다루지 않아도 됩니다. 예를 들어 라우트에서 Illuminate\Http\Request를 타입 힌트하면, 컨테이너가 뒤에서 자동으로 주입을 처리합니다:

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

자동 주입과 파사드를 활용하면 컨테이너를 직접 조작하지 않고도 대부분의 Laravel 애플리케이션을 개발할 수 있습니다. 그렇다면 언제 컨테이너를 직접 다뤄야 할까요? 크게 두 가지 상황이 있습니다.

첫째, 인터페이스를 구현한 클래스를 작성하고, 라우트나 다른 클래스의 생성자에서 해당 인터페이스를 타입 힌트로 사용하려면 컨테이너에 어떤 구현체를 사용할지 알려줘야 합니다. 둘째, 다른 개발자와 공유할 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)); });

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

스코프 싱글턴 바인딩

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

NOTE

singletonscoped의 차이를 간단히 정리하면: singleton은 애플리케이션 전체 생명주기 동안 동일 인스턴스를 유지하고, scoped는 요청(또는 Job) 단위로 인스턴스를 새로 생성합니다. Octane처럼 한 프로세스에서 여러 요청을 처리하는 환경에서는 scoped를 사용해야 요청 간 상태 오염을 방지할 수 있습니다.

인스턴스 바인딩

이미 생성된 객체 인스턴스를 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를 주입합니다. 컨트롤러, 이벤트 리스너, 미들웨어 등 컨테이너로 해석되는 모든 클래스에서 이 인터페이스를 타입 힌트로 사용할 수 있습니다:

use App\Contracts\EventPusher; /** * 클래스 인스턴스를 생성합니다. */ public function __construct( protected EventPusher $pusher, ) {}

나중에 RedisEventPusher 대신 다른 구현체로 교체하고 싶다면, 바인딩 한 줄만 수정하면 됩니다. 실제 사용 코드는 전혀 바꾸지 않아도 됩니다.

컨텍스트 바인딩

같은 인터페이스를 사용하지만, 클래스마다 서로 다른 구현체를 주입해야 할 때가 있습니다. 예를 들어 두 컨트롤러가 각각 다른 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'); });

PhotoController는 로컬 디스크를, VideoControllerUploadController는 S3 디스크를 주입받습니다.

컨텍스트 어트리뷰트

드라이버나 설정값을 주입할 때 컨텍스트 바인딩을 자주 사용하는 패턴을 위해, Laravel은 PHP 어트리뷰트(Attribute) 형태의 편리한 단축 기능을 제공합니다. 서비스 프로바이더에서 컨텍스트 바인딩을 직접 정의하지 않아도 됩니다.

예를 들어, 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, DB, Log, RouteParameter, Tag 어트리뷰트를 제공합니다:

<?php namespace App\Http\Controllers; use App\Models\Photo; use Illuminate\Container\Attributes\Auth; use Illuminate\Container\Attributes\Cache; use Illuminate\Container\Attributes\Config; use Illuminate\Container\Attributes\DB; 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, #[DB('mysql')] protected Connection $connection, #[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); } }

기본값 바인딩

클래스가 의존성 객체뿐만 아니라 정수나 문자열 같은 기본 타입 값도 함께 필요로 할 때, 컨텍스트 바인딩으로 해당 값을 주입할 수 있습니다:

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

타입 지정 가변 인자 바인딩

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

<?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, ]);

태그 기반 가변 인자 의존성

가변 인자 의존성에 태그를 활용할 수도 있습니다. needsgiveTagged를 조합하면, 해당 태그로 등록된 모든 바인딩을 쉽게 주입할 수 있습니다:

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

태깅

특정 카테고리에 속하는 바인딩을 한꺼번에 해석해야 할 때 태깅을 사용합니다. 예를 들어 여러 Report 인터페이스 구현체를 배열로 받는 리포트 분석기를 만든다면, 먼저 구현체를 등록하고 태그를 부여합니다:

$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 메서드를 사용하면 이미 해석된 서비스를 수정하거나 데코레이터 패턴을 적용할 수 있습니다. 서비스가 해석될 때 추가 처리를 실행하고 싶을 때 유용합니다. extend는 확장할 서비스 클래스와, 수정된 서비스를 반환하는 클로저를 인자로 받습니다. 클로저는 해석된 서비스 인스턴스와 컨테이너를 전달받습니다:

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

메서드 호출과 주입

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

번역일: 2026년 6월 20일