1 개요[ | ]
- Service Container
- 서비스 컨테이너
https://laravel.com/docs/11.x/container
2 소개[ | ]
Laravel 서비스 컨테이너는 클래스 의존성을 관리하고 의존성 주입을 수행하는 데 강력한 도구입니다. 의존성 주입은 본질적으로 클래스 의존성이 생성자나 경우에 따라 "세터(setter)" 메소드를 통해 클래스에 "주입"된다는 것을 의미합니다.
간단한 예제를 살펴봅시다:
<?php
namespace App\Http\Controllers;
use App\Repositories\UserRepository;
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
는 데이터 소스로부터 사용자를 조회해야 합니다. 따라서 사용자를 조회할 수 있는 서비스를 주입할 것입니다. 이 문맥에서 UserRepository
는 데이터베이스에서 사용자 정보를 조회하기 위해 주로 Eloquent를 사용할 것입니다. 그러나 리포지토리가 주입되었기 때문에 쉽게 다른 구현으로 교체할 수 있습니다. 또한 애플리케이션을 테스트할 때 UserRepository
의 더미 구현을 쉽게 "모킹"할 수 있습니다.
Laravel 서비스 컨테이너에 대한 깊은 이해는 강력하고 대규모 애플리케이션을 구축하거나 Laravel 코어 자체에 기여하는 데 필수적입니다.
2.1 제로 설정 해결[ | ]
클래스에 의존성이 없거나 다른 구체 클래스(인터페이스가 아님)에만 의존하는 경우, 컨테이너는 해당 클래스를 해결하는 방법에 대해 지시할 필요가 없습니다. 예를 들어, 다음 코드를 routes/web.php
파일에 넣을 수 있습니다:
<?php
class Service
{
// ...
}
Route::get('/', function (Service $service) {
die($service::class);
});
이 예시에서, 애플리케이션의 /
라우트를 접근하면 Service
클래스를 자동으로 해결하고 해당 클래스를 라우트의 핸들러에 주입합니다. 이는 매우 혁신적입니다. 애플리케이션을 개발하면서 의존성 주입을 활용할 수 있으며, 복잡한 설정 파일에 대해 걱정할 필요가 없다는 의미입니다.
다행히도, Laravel 애플리케이션을 구축할 때 작성하게 되는 많은 클래스들이 컨테이너를 통해 자동으로 의존성을 받습니다. 여기에는 컨트롤러, 이벤트 리스너, 미들웨어 등이 포함됩니다. 또한, 큐에 있는 작업의 handle
메소드에서도 의존성을 타입 힌팅할 수 있습니다. 자동 및 제로 설정 의존성 주입의 강력함을 한 번 맛보면, 이를 빼놓고 개발하는 것이 불가능하게 느껴질 것입니다.
2.2 컨테이너를 언제 활용하는가[ | ]
제로 설정 해결 덕분에, 종종 컨테이너와 수동으로 상호작용하지 않고도 라우트, 컨트롤러, 이벤트 리스너 및 기타 장소에서 의존성을 타입 힌트할 수 있습니다. 예를 들어, 현재 요청에 쉽게 접근할 수 있도록 Illuminate\Http\Request
객체를 라우트 정의에서 타입 힌트할 수 있습니다. 이 코드를 작성할 때는 컨테이너와 상호작용할 필요가 없지만, 사실 이 의존성의 주입은 뒤에서 컨테이너가 관리하고 있습니다:
use Illuminate\Http\Request;
Route::get('/', function (Request $request) {
// ...
});
많은 경우, 자동 의존성 주입과 파사드 덕분에 컨테이너에서 수동으로 바인딩하거나 해결하지 않고도 Laravel 애플리케이션을 구축할 수 있습니다. 그렇다면 언제 컨테이너와 수동으로 상호작용해야 할까요? 두 가지 상황을 살펴보겠습니다.
첫째, 인터페이스를 구현하는 클래스를 작성하고 그 인터페이스를 라우트나 클래스 생성자에서 타입 힌트하려는 경우, 컨테이너에 해당 인터페이스를 어떻게 해결할지 알려주어야 합니다. 둘째, 다른 Laravel 개발자들과 공유할 계획인 Laravel 패키지를 작성하는 경우, 패키지의 서비스를 컨테이너에 바인딩해야 할 수 있습니다.
3 바인딩[ | ]
3.1 바인딩 기본[ | ]
- 간단한 바인딩
거의 모든 서비스 컨테이너 바인딩은 서비스 제공자 내에서 등록됩니다. 따라서 대부분의 예제는 이 컨텍스트에서 컨테이너를 사용하는 방법을 보여줍니다.
서비스 제공자 내에서 $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
인터페이스에 의존하지 않는 클래스는 컨테이너에 바인딩할 필요가 없습니다. 이러한 객체는 리플렉션을 사용하여 자동으로 해결할 수 있기 때문에 컨테이너가 이러한 객체를 빌드하는 방법을 지시할 필요가 없습니다.
- 싱글톤 바인딩
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 애플리케이션이 새로운 "수명주기"를 시작할 때마다 플러시됩니다. 예를 들어, Laravel Octane 워커]]가 새로운 요청을 처리하거나 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));
});
- 인스턴스 바인딩
instance
메소드를 사용하여 기존 객체 인스턴스를 컨테이너에 바인딩할 수도 있습니다. 주어진 인스턴스는 이후 컨테이너에 대한 호출 시 항상 반환됩니다:
use App\Services\Transistor;
use App\Services\PodcastParser;
$service = new Transistor(new PodcastParser);
$this->app->instance(Transistor::class, $service);
3.2 인터페이스를 구현체에 바인딩하기[ | ]
서비스 컨테이너의 매우 강력한 기능 중 하나는 인터페이스를 특정 구현체에 바인딩할 수 있는 능력입니다. 예를 들어, 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
) {}
이렇게 하면 서비스 컨테이너는 필요한 곳에 적절한 구현체를 자동으로 주입해줍니다.
3.3 컨텍스트 바인딩[ | ]
때때로 동일한 인터페이스를 사용하는 두 클래스가 있을 수 있으며, 각 클래스에 서로 다른 구현체를 주입하고자 할 수 있습니다. 예를 들어, 두 컨트롤러가 서로 다른 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');
});
위의 코드를 통해 PhotoController
클래스는 'local'
디스크의 Filesystem
구현체를 주입받고, VideoController
와 UploadController
클래스는 's3'
디스크의 Filesystem
구현체를 주입받게 됩니다.
3.4 프리미티브 바인딩[ | ]
어떤 클래스가 몇 가지 주입된 클래스를 받는 동시에 정수와 같은 주입된 프리미티브 값을 필요로 할 수도 있습니다. 이럴 때는 컨텍스트 바인딩을 사용하여 클래스가 필요로 하는 값을 쉽게 주입할 수 있습니다:
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');
3.5 타입지정된 가변인자 바인딩[ | ]
때때로 클래스는 가변인자 생성자 인수를 사용하여 타입지정된 객체 배열을 받습니다:
<?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),
];
});
편의를 위해, 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');
3.6 태깅[ | ]
때때로 특정 "카테고리"의 바인딩을 모두 해결해야 할 때가 있습니다. 예를 들어, 다양한 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'));
});
3.7 바인딩 확장[ | ]
extend
메소드는 해결된 서비스의 수정을 허용합니다. 예를 들어, 서비스가 해결될 때 추가 코드를 실행하여 서비스를 데코레이트하거나 설정할 수 있습니다. extend
메소드는 확장하려는 서비스 클래스와 수정된 서비스를 반환해야 하는 클로저를 두 개의 인수로 받습니다. 클로저는 해결된 서비스와 컨테이너 인스턴스를 인수로 받습니다:
$this->app->extend(Service::class, function (Service $service, Application $app) {
return new DecoratedService($service);
});
4 해결[ | ]
4.1 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 파사드 또는 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
) {}
4.2 자동 주입[ | ]
대안으로, 그리고 중요하게도, 컨트롤러, 이벤트 리스너, 미들웨어 등 컨테이너에 의해 해결되는 클래스의 생성자에서 의존성을 타입 힌트할 수 있습니다. 또한, 큐에 있는 작업의 handle
메소드에서도 의존성을 타입 힌트할 수 있습니다. 실제로, 대부분의 객체는 이렇게 컨테이너에 의해 해결되고 주입되어야 합니다.
예를 들어, 애플리케이션에서 정의한 리포지토리를 컨트롤러의 생성자에 타입 힌트할 수 있습니다. 리포지토리는 자동으로 해결되고 클래스에 주입됩니다:
<?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;
}
}
5 메소드 호출과 주입[ | ]
객체 인스턴스에서 메소드를 호출할 때 컨테이너가 자동으로 해당 메소드의 의존성을 주입하도록 하려는 경우가 있습니다. 예를 들어, 다음 클래스가 있다고 가정해 봅시다:
namespace App;
use App\Repositories\UserRepository;
class UserReport
{
/**
* 새 사용자 보고서를 생성합니다.
*/
public function generate(UserRepository $repository): array
{
return [
// ...
];
}
}
컨테이너를 통해 generate
메소드를 다음과 같이 호출할 수 있습니다:
use App\UserReport;
use Illuminate\Support\Facades\App;
$report = App::call([new UserReport, 'generate']);
call
메소드는 모든 PHP 호출가능한 객체를 받습니다. 컨테이너의 call
메소드는 클로저를 호출할 때도 의존성을 자동으로 주입할 수 있습니다:
use App\Repositories\UserRepository;
use Illuminate\Support\Facades\App;
$result = App::call(function (UserRepository $repository) {
// ...
});
6 컨테이너 이벤트[ | ]
서비스 컨테이너는 객체를 해결할 때마다 이벤트를 발생시킵니다. 이 이벤트를 리슨하려면 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) {
// 컨테이너가 모든 타입의 객체를 해석할 때 호출됩니다...
});
보다시피, 해결된 객체는 콜백에 전달되어, 객체가 소비자에게 전달되기 전에 추가 속성을 설정할 수 있습니다.
7 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
의 인스턴스가 예외로 발생합니다.