본문 바로가기

개발/Laravel

Laravel Dependency Injection Container - 1

라라벨의 제어의 역전(Inversion of Control), 의존성 주입(Dependency Injection) 컨테이너에 대해 작성한다.

 

컨테이너 접근

라라벨의 컨테이너 인스턴스에 접근하는 방법

$container = app();

라라벨의 공식 문서는 $this->app을 사용한다.

 

외부에서 Illuminate\Container 사용

1. https://packagist.org/packages/illuminate/container 설치

2. 아래와 같이 사용

use Illuminate\Container\Container;

$container = Container::getInstance();

기본 사용법

의존성 주입을 원하는 클래스에 생성자에 타입 힌트 사용

class MyClass
{
    /**
     * @var AnotherClass
     */
    private $dependency;

    public function __construct(AnotherClass $dependency)
    {
        $this->dependency = $dependency;
    }
}

new MyClass 사용 대신 컨테이너의 make() 메소드 사용

$instance = $container->make(MyClass::class);

컨테이너는 자동으로 의존성을 인스턴스화

$instance = new MyClass(new AnotherClass());

AnotherClass 가 다른 의존성을 가진 경우 컨테이너는 재귀적으로 의존성을 해결

 

실용 예제

회원 가입 기능에서 메일 기능을 분리하는 예제

class Mailer
{
    public function mail($recipient, $content)
    {
        // Send an email to the recipient
        // ...
    }
}
class UserManager
{
    private $mailer;

    public function __construct(Mailer $mailer)
    {
        $this->mailer = $mailer;
    }

    public function register($email, $password)
    {
        // Create the user account
        // ...

        // Send the user an email to say hello!
        $this->mailer->mail($email, 'Hello and welcome!');
    }
}
use Illuminate\Container\Container;

$container = Container::getInstance();

$userManager = $container->make(UserManager::class);
$userManager->register('dave@davejamesmiller.com', 'MySuperSecurePassword!');

구현 객체 인터페이스 바인딩

컨테이너 이용 시 인터페이스와 구현체를 런타임 시 쉽게 인스턴스화 가능

interface MyInterface { /* ... */ }
interface AnotherInterface { /* ... */ }

이후 인터페이스 구현 클래스 작성

class MyClass implements MyInterface
{
    private $dependency;

    public function __construct(AnotherInterface $dependency)
    {
        $this->dependency = $dependency;
    }
}

bind() 메소드를 사용, 각 인터페이스 구현체(class) 매핑

$container->bind(MyInterface::class, MyClass::class);
$container->bind(AnotherInterface::class, AnotherClass::class);

make() 메소드에 클래스 이름이 아닌 인터페이스 이름 전달

$instance = $container->make(MyInterface::class);

만약 인터페이스 바인딩을 잊었을 경우, 치명적 오류 발생

Fatal error: Uncaught ReflectionException: Class MyInterface does not exist

컨테이너는 new MyInterface, 인터페이스를 인스턴스화 하고자 하기 때문

 

실용 예제

변경 가능 캐시 레이어 구성

interface Cache
{
    public function get($key);
    public function put($key, $value);
}
class RedisCache implements Cache
{
    public function get($key) { /* ... */ }
    public function put($key, $value) { /* ... */ }
}
class Worker
{
    private $cache;

    public function __construct(Cache $cache)
    {
        $this->cache = $cache;
    }

    public function result()
    {
        // Use the cache for something...
        $result = $this->cache->get('worker');

        if ($result === null) {
            $result = do_something_slow();

            $this->cache->put('worker', $result);
        }

        return $result;
    }
}
use Illuminate\Container\Container;

$container = Container::getInstance();
$container->bind(Cache::class, RedisCache::class);

$result = $container->make(Worker::class)->result();

추상, 구상 클래스 바인딩

추상 클래스 바인딩

$container->bind(MyAbstract::class, MyConcreteClass::class);

추상 클래스를 서브 클래스로 대체

$container->bind(MySQLDatabase::class, CustomMySQLDatabase::class);

커스텀 바인딩

클래스가 추가 설정을 요구할 경우, 클래스의 이름 대신 closure를 bind() 메소드의 두번째 매개 변수로 사용 가능

$container->bind(Database::class, function (Container $container) {
    return new MySQLDatabase(MYSQL_HOST, MYSQL_PORT, MYSQL_USER, MYSQL_PASS);
});

데이터베이스 인터페이스 필요 시 새로운 MySQLDatabase 인스턴스 생성

몇가지 지정된 구성값을 필요로 함(단일 인스턴스 공유 시 싱글톤 사용)

 

클로저는 컨테이너 인스턴스를 첫번째 매개 변수로 받으며, 필요한 경우 다른 클래스를 인스턴스화

$container->bind(Logger::class, function (Container $container) {
    $filesystem = $container->make(Filesystem::class);

    return new FileLogger($filesystem, 'logs/error.log');
});

클로저는 구상 클래스가 인스턴스화 하는 법을 별도 지정 가능

$container->bind(GitHub\Client::class, function (Container $container) {
    $client = new GitHub\Client;
    $client->setEnterpriseUrl(GITHUB_HOST);
    return $client;
});

콜백 해결 (컨테이너 이벤트)

resolving() 메소드를 이용, 객체의 의존성을 해결할 때 콜백 사용 가능

$container->resolving(GitHub\Client::class, function ($client, Container $container) {
    $client->setEnterpriseUrl(GITHUB_HOST);
});

콜백이 여러개 존재할 경우, 모든 콜백 호출

(인터페이스나 추상 클래스에서도 동일하게 동작)

$container->resolving(Logger::class, function (Logger $logger) {
    $logger->setLevel('debug');
});

$container->resolving(FileLogger::class, function (FileLogger $logger) {
    $logger->setFilename('logs/debug.log');
});

$container->bind(Logger::class, FileLogger::class);

$logger = $container->make(Logger::class);

모든 의존성 해결에 콜백 사용 가능

$container->resolving(function ($object, Container $container) {
    // ...
});

클래스 확장 (바인딩 확장)

extend() 메소드 사용, 클래스를 감싸 다른 객체로 반환 가능

$container->extend(APIClient::class, function ($client, Container $container) {
    return new APIClientDecorator($client);
});

반환되는 객체는 같은 인터페이스를 구현해야 함

그렇지 않을 경우 타입 힌팅에서 에러 발생

 

싱글톤

자동 바인딩, bind() 메소드 이용 시 필요할 때 마다 새로운 인스턴스 생성 (혹은 클로저 호출)

하나의 인스턴스만 공유하고자 할 경우 bind() 대신 singleton() 을 사용 가능

$container->singleton(Cache::class, RedisCache::class);

또는 클로저 사용 가능

$container->singleton(Database::class, function (Container $container) {
    return new MySQLDatabase('localhost', 'testdb', 'user', 'pass');
});

구상 클래스를 싱글톤으로 생성하고자 할 경우, 두번째 매개 변수를 제외하고 전달

$container->singleton(MySQLDatabase::class);

싱글톤 객체는 필요할 때 단 한번 생성되며 이후 해당 객체를 재사용

재사용을 원하는 인스턴스가 이미 존재할 경우, instance() 메소드 사용

$container->instance(Container::class, $container);

임의의 바인딩 이름

클래스나 인터페이스 이름 대신 임의의 문자열 사용 가능

타입 힌트는 사용할 수 없음, make() 메소드의 사용 필요

$container->bind('database', MySQLDatabase::class);

$db = $container->make('database');

인터페이스와 클래스의 짧은 이름 기능 지원시 alias() 메소드 사용

$container->singleton(Cache::class, RedisCache::class);
$container->alias(Cache::class, 'cache');

$cache1 = $container->make(Cache::class);
$cache2 = $container->make('cache');

assert($cache1 === $cache2);

임의의 값 저장

컨테이너를 임의의 값을 저장하고자 사용 가능

$container->instance('database.name', 'testdb');

$db_name = $container->make('database.name');

배열 접근을 지원함

$container['database.name'] = 'testdb';

$db_name = $container['database.name'];

클로저 바인딩과 같이 사용 가능

$container->singleton('database', function (Container $container) {
    return new MySQLDatabase(
        $container['database.host'],
        $container['database.name'],
        $container['database.user'],
        $container['database.pass']
    );
});

라라벨 자체는 컨테이너를 구성하기 위해 사용하지 않음

설정 파일은 Config 로 분리되어 있으며 설정에 대한 부분은 Illuminate\Config\Repository 클래스에 존재

(PHP-DI는 아님)

$db = $container['database'];

함수와 메소드의 의존성 주입

함수에도 의존성 주입 기능을 사용 가능

function do_something(Cache $cache) { /* ... */ }

$result = $container->call('do_something');

추가적인 매개 변수는 순서 또는 연관 배열로 전달 가능

function show_product(Cache $cache, $id, $tab = 'details') { /* ... */ }

// show_product($cache, 1)
$container->call('show_product', [1]);
$container->call('show_product', ['id' => 1]);

// show_product($cache, 1, 'spec')
$container->call('show_product', [1, 'spec']);
$container->call('show_product', ['id' => 1, 'tab' => 'spec']);

Callable 메소드에 모두 적용 가능

반응형

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

Laravel Eloquent Static Method  (0) 2020.06.24
Laravel Singleton 구현  (0) 2020.06.24
Laravel 6 Auth Login  (0) 2020.06.18
Laravel 6 Custom Exception  (0) 2020.06.10
Laravel 6 Gmail 연결 및 메일 전송 설정  (2) 2020.06.09