서비스 컨테이너

번역일: 2026년 7월 2일

서비스 컨테이너

소개

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

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

<?php namespace App\Http\Controllers; use App\Http\Controllers\Controller; use App\Repositories\UserRepository; use App\Models\User; use Illuminate\View\View; class UserController extends Controller { /** * 새 컨트롤러 인스턴스를 생성합니다. */ public function __construct( protected UserRepository $users, ) {} /** * 주어진 사용자의 프로필을 표시합니다. */ public function show(string $id): View { $user = $this->users->find($id); return view('user.profile', ['user' => $user]); } }

이 예시에서 UserController는 데이터 소스에서 사용자를 가져와야 합니다. 이를 위해 사용자를 조회할 수 있는 서비스를 주입받습니다. 여기서 UserRepositoryEloquent를 사용해 데이터베이스에서 사용자 정보를 가져오는 역할을 합니다. 저장소가 주입 방식으로 제공되므로, 다른 구현체로 손쉽게 교체할 수 있고, 테스트 시에는 UserRepository의 가짜(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 개발자와 공유할 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)); });

인스턴스 바인딩

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

나중에 Redis 대신 다른 메시지 브로커로 교체해야 할 경우, 구현체 코드를 수정하는 대신 바인딩만 변경하면 됩니다. 인터페이스를 타입 힌트로 사용한 모든 클래스는 자동으로 새 구현체를 주입받습니다.

컨텍스트 바인딩

동일한 인터페이스를 사용하는 두 클래스가 각각 다른 구현체를 주입받아야 하는 경우가 있습니다. 예를 들어, 두 컨트롤러가 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'); });

PhotoController에는 로컬 스토리지를, VideoControllerUploadController에는 S3 스토리지를 주입하는 예시입니다. 같은 인터페이스라도 클래스에 따라 다른 구현체를 주입할 수 있습니다.

기본값(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');

타입 가변 인자 바인딩

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

<?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 인터페이스 구현체를 배열로 받아 분석하는 리포트 분석기를 만든다고 가정해보겠습니다. 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 메서드를 사용하면 컨테이너에서 해결된 서비스를 수정할 수 있습니다. 서비스가 해결될 때 추가적인 설정이나 데코레이션을 적용할 때 유용합니다. 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 메서드에 연관 배열로 직접 값을 전달할 수 있습니다. 예를 들어, Transistor 서비스의 생성자에 필요한 $id 인자를 직접 넘기는 경우입니다.

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

자동 주입

컨테이너를 직접 다루는 것보다, 실무에서는 대부분 자동 주입을 활용합니다. 컨트롤러, 이벤트 리스너, 미들웨어, 큐 Jobhandle 메서드 등에서 생성자 타입 힌트만으로 컨테이너가 자동으로 의존성을 주입합니다.

예를 들어, 컨트롤러 생성자에서 리포지터리를 타입 힌트로 선언하면 컨테이너가 자동으로 해결해 주입합니다.

<?php namespace App\Http\Controllers; use App\Repositories\UserRepository; use App\Models\User; class UserController extends Controller { /** * 새 컨트롤러 인스턴스를 생성합니다. */ public function __construct( protected UserRepository $users, ) {} /** * 주어진 ID의 사용자를 반환합니다. */ public function show(string $id): User { $user = $this->users->findOrFail($id); return $user; } }

메서드 호출과 주입

객체 인스턴스의 특정 메서드를 호출할 때 컨테이너가 그 메서드의 의존성을 자동으로 주입하도록 할 수 있습니다. 예를 들어, 다음과 같은 클래스가 있다고 가정합니다.

<?php namespace App; use App\Repositories\UserRepository; class UserReport { /** * 새 사용자 리포트를 생성합니다. */ public function generate(UserRepository $repository): array { return [ // ... ]; } }

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

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

call 메서드는 PHP callable이라면 무엇이든 받을 수 있습니다. 클로저에도 의존성을 자동 주입하며 호출할 수 있습니다.

use App\Repositories\UserRepository; use Illuminate\Support\Facades\App; $result = App::call(function (UserRepository $repository) { // ... });

컨테이너 이벤트

서비스 컨테이너는 객체를 해결할 때마다 이벤트를 발생시킵니다. 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) { // 컨테이너가 어떤 타입의 객체든 해결할 때 호출됩니다. });

콜백에는 해결된 객체가 전달되므로, 객체가 사용자에게 전달되기 전에 추가 프로퍼티를 설정하거나 가공할 수 있습니다.

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일