의존성 자동 주입

Webman에서 의존성 자동 주입은 선택적 기능이며, 기본값은 비활성화 되어 있습니다. 의존성 자동 주입이 필요하다면 php-di 사용을 추천하며, 아래는 Webman과 php-di의 사용법입니다.

설치

composer require psr/container ^1.1.1 php-di/php-di ^6.3 doctrine/annotations ^1.14

구성 파일 config/container.php를 수정하고, 최종 내용은 아래와 같습니다:

$builder = new \DI\ContainerBuilder();
$builder->addDefinitions(config('dependence', []));
$builder->useAutowiring(true);
$builder->useAnnotations(true);
return $builder->build();

config/container.php는 최종적으로 PSR-11 규격에 맞는 컨테이너 인스턴스를 반환합니다. php-di를 사용하고 싶지 않은 경우, 이곳에서 다른 PSR-11 규격의 컨테이너 인스턴스를 생성하여 반환할 수 있습니다.

생성자 주입

app/service/Mailer.php를 새로 만들고 (디렉토리가 존재하지 않으면 직접 생성) 내용은 아래와 같습니다:

<?php
namespace app\service;

class Mailer
{
    public function mail($email, $content)
    {
        // 이메일 전송 코드 생략
    }
}

app/controller/UserController.php의 내용은 아래와 같습니다:

<?php
namespace app\controller;

use support\Request;
use app\service\Mailer;

class UserController
{
    private $mailer;

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

    public function register(Request $request)
    {
        $this->mailer->mail('hello@webman.com', '안녕하세요, 환영합니다!');
        return response('ok');
    }
}

정상적인 경우, app\controller\UserController의 인스턴스를 만들기 위해서는 아래의 코드가 필요합니다:

$mailer = new Mailer;
$user = new UserController($mailer);

php-di를 사용할 경우, 개발자는 컨트롤러 내의 Mailer를 수동으로 인스턴스화할 필요가 없으며, Webman이 자동으로 처리해 줍니다. 만약 Mailer 인스턴스를 만드는 과정에서 다른 클래스의 의존성이 있다면, Webman은 자동으로 인스턴스화하여 주입해 줍니다. 개발자는 초기화 작업을 전혀 할 필요가 없습니다.

주의
반드시 프레임워크나 php-di에서 생성된 인스턴스만 의존성 자동 주입이 가능합니다. 수동으로 new를 사용하여 생성된 인스턴스는 의존성 자동 주입을 받을 수 없습니다. 주입이 필요한 경우, support\Container 인터페이스를 사용하여 new 문장을 대체해야 합니다, 예를 들어:

use app\service\UserService;
use app\service\LogService;
use support\Container;

// new 키워드로 생성된 인스턴스는 의존성 주입이 불가능합니다
$user_service = new UserService;
// new 키워드로 생성된 인스턴스는 의존성 주입이 불가능합니다
$log_service = new LogService($path, $name);

// Container로 생성된 인스턴스는 의존성 주입이 가능합니다
$user_service = Container::get(UserService::class);
// Container로 생성된 인스턴스는 의존성 주입이 가능합니다
$log_service = Container::make(LogService::class, [$path, $name]);

주석 주입

생성자 의존성 자동 주입 외에도 주석을 사용하여 주입할 수 있습니다. 위의 예를 이어서, app\controller\UserController를 아래와 같이 변경합니다:

<?php
namespace app\controller;

use support\Request;
use app\service\Mailer;
use DI\Annotation\Inject;

class UserController
{
    /**
     * @Inject
     * @var Mailer
     */
    private $mailer;

    public function register(Request $request)
    {
        $this->mailer->mail('hello@webman.com', '안녕하세요, 환영합니다!');
        return response('ok');
    }
}

이 예제는 @Inject 주석을 통해 주입되며, @var 주석을 사용하여 객체 타입을 선언합니다. 이 방식은 생성자 주입과 동일한 효과를 가지지만 코드가 더 간결합니다.

주의
Webman은 1.4.6 버전 이전까지 컨트롤러 매개변수 주입을 지원하지 않습니다. 예를 들어, 아래 코드는 Webman <= 1.4.6에서는 지원하지 않습니다.

<?php
namespace app\controller;

use support\Request;
use app\service\Mailer;

class UserController
{
    // 1.4.6 버전 이전에는 컨트롤러 매개변수 주입을 지원하지 않습니다
    public function register(Request $request, Mailer $mailer)
    {
        $mailer->mail('hello@webman.com', '안녕하세요, 환영합니다!');
        return response('ok');
    }
}

사용자 정의 생성자 주입

때때로 생성자에 전달하는 인자가 클래스의 인스턴스가 아니라 문자열, 숫자, 배열 등일 수 있습니다. 예를 들어, Mailer 생성자는 SMTP 서버 IP와 포트를 전달받아야 합니다:

<?php
namespace app\service;

class Mailer
{
    private $smtpHost;

    private $smtpPort;

    public function __construct($smtp_host, $smtp_port)
    {
        $this->smtpHost = $smtp_host;
        $this->smtpPort = $smtp_port;
    }

    public function mail($email, $content)
    {
        // 이메일 전송 코드 생략
    }
}

이 경우, 이전에 설명한 생성자 자동 주입을 직접 사용할 수 없습니다. 왜냐하면 php-di$smtp_host$smtp_port의 값을 알 수 없기 때문입니다. 이럴 때는 사용자 정의 주입을 시도할 수 있습니다.

config/dependence.php (파일이 존재하지 않으면 직접 생성) 에 아래 코드를 추가합니다:

return [
    // ... 다른 설정은 생략

    app\service\Mailer::class => new app\service\Mailer('192.168.1.11', 25);
];

이렇게 하면 의존성 주입이 app\service\Mailer 인스턴스를 가져오면, 자동으로 이 설정에서 생성된 app\service\Mailer 인스턴스를 사용하게 됩니다.

우리는 config/dependence.php에서 new를 사용하여 Mailer 클래스를 인스턴스화한 것을 알 수 있습니다. 이 예에서는 문제가 없지만, 만약 Mailer 클래스가 다른 클래스를 의존하거나 내부에서 주석 주입을 사용하는 경우, new로 초기화하면 의존성 자동 주입이 작동하지 않습니다. 해결책은 사용자 정의 인터페이스 주입을 활용하여, Container::get(클래스명) 또는 Container::make(클래스명, [생성자 매개변수]) 메서드를 사용하여 클래스를 초기화하는 것입니다.

사용자 정의 인터페이스 주입

실제 프로젝트에서는 구체적인 클래스보다 인터페이스를 활용한 프로그래밍을 선호합니다. 예를 들어 app\controller\UserController에서는 app\service\MailerInterface를 가져와야 하며 app\service\Mailer를 가져오면 안 됩니다.

MailerInterface 인터페이스를 정의합니다.

<?php
namespace app\service;

interface MailerInterface
{
    public function mail($email, $content);
}

MailerInterface 인터페이스의 구현을 정의합니다.

<?php
namespace app\service;

class Mailer implements MailerInterface
{
    private $smtpHost;

    private $smtpPort;

    public function __construct($smtp_host, $smtp_port)
    {
        $this->smtpHost = $smtp_host;
        $this->smtpPort = $smtp_port;
    }

    public function mail($email, $content)
    {
        // 이메일 전송 코드 생략
    }
}

구체적인 구현 대신 MailerInterface 인터페이스를 가져옵니다.

<?php
namespace app\controller;

use support\Request;
use app\service\MailerInterface;
use DI\Annotation\Inject;

class UserController
{
    /**
     * @Inject
     * @var MailerInterface
     */
    private $mailer;

    public function register(Request $request)
    {
        $this->mailer->mail('hello@webman.com', '안녕하세요, 환영합니다!');
        return response('ok');
    }
}

config/dependence.php에서 MailerInterface 인터페이스의 구현을 아래와 같이 정의합니다.

use Psr\Container\ContainerInterface;
return [
    app\service\MailerInterface::class => function(ContainerInterface $container) {
        return $container->make(app\service\Mailer::class, ['smtp_host' => '192.168.1.11', 'smtp_port' => 25]);
    }
];

이렇게 하면 비즈니스에서 MailerInterface 인터페이스를 사용할 때, 자동으로 Mailer 구현체를 사용하게 됩니다.

인터페이스를 이용한 프로그래밍의 장점은 구성 요소를 교체해야 할 경우, 비즈니스 코드를 변경할 필요 없이 config/dependence.php에서 구체적인 구현만 변경하면 된다는 점입니다. 이는 단위 테스트를 할 때도 매우 유용합니다.

기타 사용자 정의 주입

config/dependence.php는 클래스의 의존성뿐만 아니라 문자열, 숫자, 배열 등의 다른 값도 정의할 수 있습니다.

예를 들어 config/dependence.php에서 아래와 같이 정의할 수 있습니다:

return [
    'smtp_host' => '192.168.1.11',
    'smtp_port' => 25
];

이 때 우리는 @Inject를 통해 smtp_hostsmtp_port를 클래스 속성에 주입할 수 있습니다.

<?php
namespace app\service;

use DI\Annotation\Inject;

class Mailer
{
    /**
     * @Inject("smtp_host")
     */
    private $smtpHost;

    /**
     * @Inject("smtp_port")
     */
    private $smtpPort;

    public function mail($email, $content)
    {
        // 이메일 전송 코드 생략
        echo "{$this->smtpHost}:{$this->smtpPort}\n"; // 192.168.1.11:25가 출력됩니다
    }
}

주의: @Inject("key") 내부는 큰따옴표이어야 합니다.

더 많은 내용

자세한 내용은 php-di 매뉴얼을 참조해 주시기 바랍니다.