서비스 컨테이너
소개
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과 같은 데이터 소스에서 팟캐스트를 검색해야 합니다. 따라서 팟캐스트를 검색할 수 있는 서비스를 주입할 것입니다. 서비스가 주입되었으므로 애플리케이션을 테스트할 때 AppleMusic 서비스의 "모의" 또는 더미 구현을 쉽게 만들 수 있습니다.
Laravel 서비스 컨테이너에 대한 깊은 이해는 강력하고 큰 애플리케이션을 구축하고 Laravel 코어 자체에 기여하는 데 필수적입니다.
제로 구성 해결
클래스에 종속성이 없거나 다른 구체적인 클래스(인터페이스가 아닌)에만 종속되는 경우, 컨테이너는 해당 클래스를 해결하는 방법을 지시할 필요가 없습니다. 예를 들어 routes/web.php 파일에 다음 코드를 배치할 수 있습니다.
<?php class Service{ // ...} Route::get('/', function (Service $service) { die($service::class);});
이 예에서 애플리케이션의 / 경로를 누르면 Service 클래스가 자동으로 해결되고 경로의 핸들러에 주입됩니다. 이것은 획기적인 변화입니다. 이는 복잡한 구성 파일에 대해 걱정할 필요 없이 애플리케이션을 개발하고 의존성 주입을 활용할 수 있음을 의미합니다.
다행히도 Laravel 애플리케이션을 빌드할 때 작성할 많은 클래스는 컨트롤러, 이벤트 리스너, 미들웨어 등을 포함하여 컨테이너를 통해 자동으로 종속성을 받습니다. 또한 큐에 넣은 작업의 handle 메서드에서 종속성을 타입 힌트할 수 있습니다. 자동 및 제로 구성 의존성 주입의 힘을 맛보면 그것 없이는 개발하기가 불가능하다고 느껴집니다.
컨테이너를 활용해야 할 때
제로 구성 해결 덕분에 컨테이너와 수동으로 상호 작용하지 않고도 경로, 컨트롤러, 이벤트 리스너 및 기타 위치에서 종속성을 타입 힌트하는 경우가 많습니다. 예를 들어 현재 요청에 쉽게 액세스할 수 있도록 경로 정의에서 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));});
인터페이스에 의존하지 않는 클래스는 컨테이너에 바인딩할 필요가 없습니다. 컨테이너는 리플렉션을 사용하여 이러한 객체를 자동으로 처리할 수 있으므로, 이러한 객체를 생성하는 방법에 대한 지시를 받을 필요가 없습니다.
싱글톤 바인딩
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 요청/작업 라이프사이클 내에서 한 번만 확인되어야 하는 클래스 또는 인터페이스를 컨테이너에 바인딩합니다. 이 메소드는 singleton 메소드와 유사하지만, scoped 메소드를 사용하여 등록된 인스턴스는 Laravel Octane 워커가 새 요청을 처리하거나 Laravel 큐 워커가 새 작업을 처리하는 등 Laravel 애플리케이션이 새로운 "라이프사이클"을 시작할 때마다 플러시됩니다.
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));});
인스턴스 바인딩
instance 메소드를 사용하여 기존 객체 인스턴스를 컨테이너에 바인딩할 수도 있습니다. 주어진 인스턴스는 컨테이너에 대한 후속 호출에서 항상 반환됩니다.
use App\Services\Transistor;use App\Services\PodcastParser; $service = new Transistor(new PodcastParser); $this->app->instance(Transistor::class, $service);
인터페이스를 구현에 바인딩
서비스 컨테이너의 매우 강력한 기능은 인터페이스를 주어진 구현에 바인딩하는 기능입니다. 예를 들어, EventPusher 인터페이스와 RedisEventPusher 구현이 있다고 가정해 보겠습니다. 이 인터페이스의 RedisEventPusher 구현을 코딩했다면 다음과 같이 서비스 컨테이너에 등록할 수 있습니다.
use App\Contracts\EventPusher;use App\Services\RedisEventPusher; $this->app->bind(EventPusher::class, RedisEventPusher::class);
이 구문은 클래스에 EventPusher의 구현이 필요할 때 컨테이너가 RedisEventPusher를 주입해야 함을 컨테이너에 알립니다. 이제 컨테이너에서 확인되는 클래스의 생성자에서 EventPusher 인터페이스를 타입 힌트할 수 있습니다. Laravel 애플리케이션 내의 컨트롤러, 이벤트 리스너, 미들웨어 및 기타 다양한 유형의 클래스는 항상 컨테이너를 사용하여 확인된다는 점을 기억하십시오.
use App\Contracts\EventPusher; /** * 새 클래스 인스턴스를 만듭니다. */public function __construct( protected EventPusher $pusher,) {}
상황별 바인딩
때로는 동일한 인터페이스를 사용하는 두 개의 클래스가 있지만 각 클래스에 다른 구현을 주입하고 싶을 수 있습니다. 예를 들어, 두 개의 컨트롤러가 Illuminate\Contracts\Filesystem\Filesystem 계약의 서로 다른 구현에 의존할 수 있습니다. Laravel은 이러한 동작을 정의하기 위한 간단하고 유려한 인터페이스를 제공합니다.
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'); });
상황별 속성
상황별 바인딩은 드라이버 또는 구성 값의 구현을 주입하는 데 자주 사용되므로 Laravel은 서비스 제공자에서 상황별 바인딩을 수동으로 정의하지 않고도 이러한 유형의 값을 주입할 수 있도록 다양한 상황별 바인딩 속성을 제공합니다.
예를 들어, 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 속성 외에도 Laravel은 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, ) { // ... }}
또한 Laravel은 현재 인증된 사용자를 주어진 경로 또는 클래스에 주입하기 위한 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 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; }}
컨텍스트 바인딩을 사용하면 Filter 인스턴스의 확인된 배열을 반환하는 클로저를 give 메서드에 제공하여 이 종속성을 확인할 수 있습니다.
$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), ]; });
편의상, Firewall이 Filter 인스턴스가 필요할 때마다 컨테이너에서 확인할 클래스 이름 배열을 제공할 수도 있습니다.
$this->app->when(Firewall::class) ->needs(Filter::class) ->give([ NullFilter::class, ProfanityFilter::class, TooLongFilter::class, ]);
가변 태그 종속성
때로는 클래스에 특정 클래스(Report ...$reports)로 타입 힌트된 가변 종속성이 있을 수 있습니다. needs 및 giveTagged 메서드를 사용하면 지정된 종속성에 대한 태그가 있는 모든 컨테이너 바인딩을 쉽게 주입할 수 있습니다.
$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);});
확인
make 메서드
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 facade 또는 app helper를 사용하여 컨테이너에서 클래스 인스턴스를 확인할 수 있습니다.
use App\Services\Transistor;use Illuminate\Support\Facades\App; $transistor = App::make(Transistor::class); $transistor = app(Transistor::class);
Laravel 컨테이너 인스턴스 자체가 컨테이너에서 해결되는 클래스에 주입되기를 원하면 클래스 생성자에서 Illuminate\Container\Container 클래스를 타입 힌트할 수 있습니다.
use Illuminate\Container\Container; /** * 새 클래스 인스턴스를 만듭니다. */public function __construct( protected Container $container,) {}
자동 주입
또는 중요하게는 컨트롤러, 이벤트 리스너, 미들웨어 등을 포함하여 컨테이너에서 해결되는 클래스의 생성자에 종속성을 타입 힌트할 수 있습니다. 또한 큐에 넣은 작업의 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 [ // ... ]; }}
컨테이너를 통해 다음과 같이 generate 메서드를 호출할 수 있습니다.
use App\PodcastStats;use Illuminate\Support\Facades\App; $stats = App::call([new PodcastStats, 'generate']);
call 메서드는 PHP 호출 가능 항목을 허용합니다. 컨테이너의 call 메서드는 종속성을 자동으로 주입하면서 클로저를 호출하는 데에도 사용할 수 있습니다.
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) { // },); // 새 바인딩은 재바인딩 클로저를 트리거합니다...$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의 인스턴스가 발생합니다.