Laravel Service Container

정종일·2023년 10월 4일
0

Laravel

목록 보기
6/9
💡 서비스 컨테이너는 클래스의 의존성을 관리하고 의존성 주입을 실행하는 강력한 도구이다

Zero Configuration Resolution

클래스의 의존성이 없거나 콘크리트 클래스에 대한 의존성만 가지는 경우 별도의 설정할 필요 없이 서비스 컨테이너를 통해 의존성을 주입할 수 있어 파일의 크기를 작게 유지할 수 있다

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

Laravel을 구성하는 대부분의 클래스 (controller, event listener, middleware …)들은 서비스 컨테이너에 의해 자동으로 의존성이 주입된다

When To Use Container

아래처럼 대부분 타입힌팅 의존성을 사용해 주입하기에 우리는 서비스 컨테이너와 직접적인 상호작용이 없이도 클래스들을 사용할 수 있고 이러한 의존성 주입은 대부분 보이지 않게 일어나기 때문에 편리하게 사용이 가능하다

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

Binding

추가적으로 우리가 직접 바인딩을 통해 서비스 컨테이너에 의존성 주입 설정을 할 수가 있다

Binding Basics

Simple Bindings

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

// PodcastParser를 파라미터로 받는 Transistor를 서비스 컨테이너에 등록
// PodcastParser는 서비스 컨테이너에 등록되어있을 수도 아닐 수도 있다
// 서비스 컨테이너에 등록되어 있지 않으면 해당 클래스를 찾아 매핑하기 때문

서비스 프로바이더 이외에서 서비스 컨테이너에 접근하고 싶다면 App facade를 사용한다

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

Binding A Singleton

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

bindIf 와 마찬가지로 singletonIf 를 사용해 등록되지 않았다면 바인딩하도록 할 수 있다

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

Binding Scoped Singleton

Laravel의 lifecycle 한 주기마다 새롭게 바인딩되는 scoped 메서드. 한 주기를 시작할 때마다 플러시 된다

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

Binding Instances

클래스가 아닌 인스턴스를 바인딩할 수도 있다. 이후에는 resolve 때마다 바인딩한 인스턴스가 호출된다

use App\Services\Transistor;
use App\Services\PodcastParser;
 
$service = new Transistor(new PodcastParser);
 
$this->app->instance(Transistor::class, $service);

Binding Interfaces To Implementations

인터페이스를 구현체에 바인딩할 수 있다. 이렇게 하면 클래스에서 해당 인터페이스를 구현할 때 구현체가 필수 요소가 되도록 할 수 있다.

use App\Contracts\EventPusher;
use App\Services\RedisEventPusher;
 
$this->app->bind(EventPusher::class, RedisEventPusher::class);

Contextual Binding

같은 인터페이스의 다른 구현체를 사용해야하는 여러 클래스가 있을 경우 아래와 같이 선언 가능하다

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

Binding Primitives

정수와 같은 기본값을 주입해야 하는 경우 아래처럼 사용 가능하다

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

태그를 포함한 모든 컨테이너에 대한 바인딩 가능

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

config 주입도 가능하다

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

Binding Typed Variadics

가변 생성자 인수를 이용해 객체를 파라미터로 받는 경우 아래처럼 사용 가능

<?php
 
use App\Models\Filter;
use App\Services\Logger;
 
class Firewall
{
    /**
     * The filter instances.
     *
     * @var array
     */
    protected $filters;
 
    /**
     * Create a new class instance.
     */
    public function __construct(
        protected Logger $logger,
        Filter ...$filters,
    ) {
        $this->filters = $filters;
    }
}

contextual 바인딩을 통해 resolve된 클래스 인스턴스의 배열을 리턴하는 closure를 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),
                ];
          });

조금 더 편리한 사용을 위해 클래스 이름만 제공해도 된다

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

Variadic Tag Dependencied

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

Tagging

바인딩한 클래스들을 특정 키테고리로 묶고 싶다면 tag 메서드를 이용한다

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

위와 같이 tag를 통해 묶으면 tag name을 통해 쉽게 resolve가 가능하다

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

Extending Bindings

extend 메서드 사용 시 resolve된 서비스에 대한 수정이 가능

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

Resolving

The make Method

서비스 컨테이너에서 클래스 인스턴스를 resolve하기 위한 메서드

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 클래스를 use해 사용하면 된다

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

Laravel Container 자체를 주입하는 것도 가능하다

use Illuminate\Container\Container;
 
/**
 * Create a new class instance.
 */
public function __construct(
    protected Container $container
) {}

Automatic Injection

서비스 컨테이너에게 자동 주입을 위임할 수 있다. constructor에 타입 힌트 형식으로 의존성을 주입하면 컨테이너가 자동으로 주입한다

<?php
 
namespace App\Http\Controllers;
 
use App\Repositories\UserRepository;
use App\Models\User;
 
class UserController extends Controller
{
    /**
     * Create a new controller instance.
     */
    public function __construct(
        protected UserRepository $users,
    ) {}
 
    /**
     * Show the user with the given ID.
     */
    public function show(string $id): User
    {
        $user = $this->users->findOrFail($id);
 
        return $user;
    }
}

Method Invocation & Injection

호출과 동시에 의존성 자동 주입을 하고 싶은 경우

<?php
 
namespace App;
 
use App\Repositories\UserRepository;
 
class UserReport
{
    /**
     * Generate a new user report.
     */
    public function generate(UserRepository $repository): array
    {
        return [
            // ...
        ];
    }
}

컨테이너를 통해 generate 메서드 호출하고 싶은 경우

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

call 메서드는 모든 PHP 호출 기능을 허용

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

Container Events

서비스 컨테이너는 객체 resolve마다 이벤트를 발생시킨다. 이를 resolving 메서드로 받을 수 있다.

use App\Services\Transistor;
use Illuminate\Contracts\Foundation\Application;
 
$this->app->resolving(Transistor::class, function (Transistor $transistor, Application $app) {
    // Called when container resolves objects of type "Transistor"...
});
 
$this->app->resolving(function (mixed $object, Application $app) {
    // Called when container resolves object of any type...
});

PSR-11

Laravel의 서비스 컨테이너는 PSR-11 인터페이스를 상속받는다. 따라서 Laravel의 컨테이너 인스턴스를 사용하기 위해 PSR-11 컨테이너를 타입힌트 할 수 있다

use App\Services\Transistor;
use Psr\Container\ContainerInterface;
 
Route::get('/', function (ContainerInterface $container) {
    $service = $container->get(Transistor::class);
 
    // ...
});
profile
제어할 수 없는 것에 의지하지 말자

0개의 댓글