본문 바로가기

개발/Laravel

서비스 컨테이너

# Laravel 6.18.18 기준, 출처 라라벨 한글 메뉴얼

 

라라벨의 서비스 컨테이너는 클래스의 의존성을 관리하고 주입하는 강력한 도구이다.

의존성 주입은 클래스간 생성 시 또는 경우에 따라 setter 메소드에 의해서 주입된다는 의미이다.

 

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Repositories\UserRepository;
use App\User;

class UserController extends Controller
{
    /**
     * The user repository implementation.
     *
     * @var UserRepository
     */
    protected $users;

    /**
     * Create a new controller instance.
     *
     * @param  UserRepository  $users
     * @return void
     */
    public function __construct(UserRepository $users)
    {
        $this->users = $users;
    }

    /**
     * Show the profile for the given user.
     *
     * @param  int  $id
     * @return Response
     */
    public function show($id)
    {
        $user = $this->users->find($id);

        return view('user.profile', ['user' => $user]);
    }
}

UserController는 데이터 소스로부터 사용자를 조회하고자 한다. 따라서 사용자를 조회할 수 있는 서비스를 주입할 것이다. UserRepository  Eloquent ORM을 사용하여 데이터베이스로부터 사용자 정보를 조회한다.

repository 가 주입되었기 때문에, 원하는 경우 손쉽게 다른 구현 객체로 변경할 수 있다. 또한 애플리케이션을 테스트할 때 손쉽게 "목킹" 하거나, 더미 UserRepository 구현체를 생성할 수도 있다.

라라벨 서비스 컨테이너를 깊이 이해하는 것은 강력하고 큰 애플리케이션을 구축할 때나 라라벨 코어에 공헌하기 위해서 아주 중요한 부분이다.

 

바인딩

서비스 컨테이너 바인딩의 대부분은 서비스 프로바이더 내에서 등록된다.

아래 예제들은 해당 컨텍스트에서 컨테이너를 사용하는 데모가 된다.

더보기

특정 인터페이스에 대한 의존성이 없을 경우 컨테이너에 클래스를 바인딩할 필요는 없습니다.

이러한 객체들은 리플랙션에 의해 자동으로 의존성이 해결되어 컨테이너가 각각의 객체들이 어떻게 생성될지 알 필요는 없습니다.

간단한 바인딩

서비스 프로바이더 안에는 항상 $this->app 속성을 통해서 컨테이너 인스턴스에 접근할 수 있다.

bind 메소드를 사용하여 클래스나 인터페이스 이름에 대한 의존성을 우리가 원하는 클래스의 인스턴스를 반환하는 Closure 를 등록하여 바인딩 할 수 있다.

$this->app->bind('HelpSpot\API', function ($app) {
    return new HelpSpot\API($app->make('HttpClient'));
});

클로저에서 컨테이너 자신을 인자로 전달 받고 있다는 것에 유의하며, 이를 통해서 연결된 객체의 의존성 문제를 위해서 컨테이너 자신을 사용할 수 있다.

 

싱글톤으로 바인딩하기

singleton 메소드로 클래스나 인터페이스를 바인딩 하면 컨테이너는 한 번만 해당 의존성을 해결한다.

싱글톤 바인딩으로 의존성이 해결되면, 컨테이너의 다른 부분에서 호출될 때 동일한 객체 인스턴스가 반환된다.

$this->app->singleton('HelpSpot\API', function ($app) {
    return new HelpSpot\API($app->make('HttpClient'));
});

인스턴스를 바인딩하기

instance 메소드를 사용하여 이미 존재하는 객체의 인스턴스를 컨테이너에 바인딩 할 수 있다.

이후 컨테이너에서 호출이 될 때는 매번 주어진 인스턴스가 반환된다.

$api = new HelpSpot\API(new HttpClient);

$this->app->instance('HelpSpot\API', $api);

기본 타입 바인딩

클래스가 주입되는 클래스들을 받아들일 수도 있지만, 정수형과 같은 기본 타입의 값들을 주입할 필요가 있을 수 있다. 여러분은 손쉽게 문맥에 따라 조건적 바인딩을 통해서 클래스가 필요한 값을 주입할 수 있다.

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

인터페이스에 구현객체 바인딩하기

서비스 컨테이너의 강력한 기능 중 하나는 주어진 구현 객체에 인터페이스를 바인딩 할 수 있다는 것이다.

예를 들어 EventPusher 인터페이스와 RedisEventPusher 구현이 있다고 가정할 때, 이 인터페이스를 구현한 RedisEventPusher 객체를 구성한 뒤에 이 객체를 다음과 같이 서비스 컨테이너에 등록할 수 있다.

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

이 구문은 EventPusher 인터페이스의 구현 객체가 필요할 때 컨테이너가 RedisEventPusher 을 주입해준다는 것을 말한다. EventPusher 인터페이스에 대한 타입을 생성자에 지정하면 어디에서라도 서비스 컨테이너가 의존성을 주입해준다.

use App\Contracts\EventPusher;

/**
 * Create a new class instance.
 *
 * @param  EventPusher  $pusher
 * @return void
 */
public function __construct(EventPusher $pusher)
{
    $this->pusher = $pusher;
}

문맥에 따른 조건적 바인딩

때때로 동일한 인터페이스에 대한 2가지 구현 객체가 있고, 각각의 클래스마다 다른 구현 객체를 전달하고자 할 수도 있다. 예를 들어 각각의 컨트롤러가 다른 Illuminate\Contracts\Filesystem\Filesystem contract 구현체에 의존한다면, 라라벨은 간단하고 유연한 인터페이스를 통해서 다음 행동을 정의한다.

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

태깅

가끔은, 바인딩의 특정 "카테고리" 전체에 대한 의존성 해결을 해야 할 때도 있다. 예를 들어, 서로 다른 Report 인터페이스의 구현 객체를 포함하는 배열을 전달받는 보고서 수집기를 개발하고 있을 때, Report 구현 객체를 등록한 뒤에, tag 메소드를 사용하여 태그를 달 수 있다.

$this->app->bind('SpeedReport', function () {
    //
});

$this->app->bind('MemoryReport', function () {
    //
});

$this->app->tag(['SpeedReport', 'MemoryReport'], 'reports');

서비스에 태그가 붙으면 tagged 메소드를 사용하여 손쉽게 의존성을 해결할 수 있다.

$this->app->bind('ReportAggregator', function ($app) {
    return new ReportAggregator($app->tagged('reports'));
});

extend 메소드로 서비스의 의존성을 수정할 수 있다. 예를 들어, 서비스의 의존성이 해결되었을 때, 서비스를 꾸미거나(decorate) 혹은 설정하는 위한 추가 코드를 실행할 수 있다.

클로저는 해결중인 서비스와 컨테이너 인스턴스를 입력 받는다.

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

의존성 해결

make 메소드

컨테이너 밖에서 클래스 인스턴스에 대한 의존성을 해결하기 위해서 make 메소드를 사용할 수 있다. 

make 메소드는 의존성 해결을 위해 원하는 클래스나 인터페이스에 대한 이름을 전달받는다.

$api = $this->app->make('HelpSpot\API');

$app 변수에 대한 접근을 가지고 있지 않은 코드에 위치하고 있다면, 글로벌 resolve 헬퍼 함수를 사용할 수 있다.

$api = resolve('HelpSpot\API');

클래스의 의존성이 컨테이너를 통해서 해결될 수 없다면, makeWith 메소드에 관련된 인자를 배열로 전달할 수도 있다.

$api = $this->app->makeWith('HelpSpot\API', ['id' => 1]);

자동 주입

앞서 이야기한 방법과 다르게, 그리고 가장 중요한 것은 컨트롤러, 이벤트 리스너, 미들웨어 등 을 포함하여 클래스의 생성자에 "타입-힌트" 를 선언함으로써 컨테이너가 의존성을 해결할 수 있도록 하는 것이다.

또한 queued jobs handle 메소드에도 종속성을 입력할 수 있다. 실제로 이 방법이 개발에서 컨테이너에 의해서 객체의 의존성을 해결할 때 가장 많이 사용되는 방법이다.

예를 들어 컨트롤러의 생성자에서 타입 힌트로 지정된 Repository를 정의했다고 가정했을 때, 해당 Repository는 자동으로 의존성이 해결되어 클래스에 주입될 것이다.

<?php

namespace App\Http\Controllers;

use App\Users\Repository as UserRepository;

class UserController extends Controller
{
    /**
     * The user repository instance.
     */
    protected $users;

    /**
     * Create a new controller instance.
     *
     * @param  UserRepository  $users
     * @return void
     */
    public function __construct(UserRepository $users)
    {
        $this->users = $users;
    }

    /**
     * Show the user with the given ID.
     *
     * @param  int  $id
     * @return Response
     */
    public function show($id)
    {
        //
    }
}

컨테이너 이벤트

서비스 컨테이너는 객체의 의존성 해결을 수행할 때마다 이벤트를 발생시키며, resolving 메소드를 사용하여 이 이벤트들에 대응할 수 있다.

$this->app->resolving(function ($object, $app) {
    // Called when container resolves object of any type...
});

$this->app->resolving(HelpSpot\API::class, function ($api, $app) {
    // Called when container resolves objects of type "HelpSpot\API"...
});

 

의존성이 해결된 객체가 콜백에 전달되어 최종적으로 객체를 필요로 하는 대상에 전달하기 전에 추가적으로 객체의 속성을 설정할 수 있다.

 

PSR-11

라라벨의 서비스 컨테이너는 PSR-11 인터페이스를 구성한다.

PSR-11 인터페이스를 타입힌트 하여 라라벨의 컨테이너 인스턴스에 접근이 가능하다.

use Psr\Container\ContainerInterface;

Route::get('/', function (ContainerInterface $container) {
    $service = $container->get('Service');

    //
});

지정된 식별자를 해결할 수 없는 경우는 예외가 던져진다.

식별자가 바인드 되지 않은 경우 예외는 Psr\Container\NotFoundExceptionInterface 인스턴스이다.

식별자가 바인드 되었지만 해결할 수 없는 경우에는 Psr\Container\ContainerExceptionInterface 인스턴스가 던져진다.

반응형

'개발 > Laravel' 카테고리의 다른 글

Laravel Dependency Injection Container - 1  (0) 2020.06.23
Laravel 6 Auth Login  (0) 2020.06.18
Laravel 6 Custom Exception  (0) 2020.06.10
Laravel 6 Gmail 연결 및 메일 전송 설정  (2) 2020.06.09
Request 라이프사이클  (0) 2020.06.05