평범한 이야기들

[TIL] PHP Laravel - 컨테이너 본문

평범한 개발 이야기/Laravel

[TIL] PHP Laravel - 컨테이너

songsariya 2021. 3. 10. 16:45
728x90

서비스 컨테이너, 의존성 주입 컨테이너는 모든 기능 중에서 가장 핵심

컨테이너는 인터페이스와 클래스의 인스턴스를 연결하고 의존성을 해결하는데 사용할 수 있음



의존성 주입 훑어보기

의존성 주입이란 어떤 로직을 처리하기 위해서 생성해야 되는 의존 객체를 new 키워드로 직접 생성하는 대신에 외부에서 주입하는 것을 의미

이때 주입 방식에 따라 생성자 주입이 가장 흔히 사용 (객체 생성 시 주입되는 것을 뜻함), 세터(Setter) 주입, 메서드 주입이 있음

// 생성자 의존성 주입

class UserMailer
{
    protected $mailer;

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

    public function welcome($user) {
        return $this->mailer->mail($user->email, 'Welcome!');
    }
}

UserMailer 클래스는 인스턴스화(객체화) 될때 Mailer 타입의 객체가 주입될 것으로 예상

welcome 메서드에서 Mailer 인스턴스를 사용하여 작업 처리



의존성 주입과 라라벨

// 간단한 수동 의존성 주입
$mailer = new MailgunMailer($mailgunKey, $mailgunSecret, $mailgunOptions);
$userMailer = new UserMailer($mailer);

$userMailer->welcome($user);

//여기서 슬랙과 로그에도 자료를 넣고 싶다면 계속 가져와서 수동으로 입력해주어야 한다.
$mailer = new MailgunMailer($mailgunKey, $mailgunSecret, $mailgunOptions);
$logger = new Logger($logPath, $minimumLogLevel):
$slack = new Slack($slackKey,$slackSecret,$channelName,$channelIcon);

$userMailer = new UserMailer($mailer, $logger, $slack);

$userMailer->welcome($user);

수동으로 하게 되면 의존성 주입 자체는 좋지만 코드가 복잡해짐



오토와이어링

class Bar
{
    public function __construct(){}
}

class Baz
{
    public function __construct(){}
}

class Foo
{
    public function __construct(Bar $bar, Baz $baz){}
}

$foo = app(Foo::class);

컨테이너는 Foo 생성자가 생성자에 있는 타입힌트를 읽고 Bar, Baz 인스턴스를 확인한 다음 Foo 인스턴스가 생성될 때 이들을 주입함 → 오토와이어링 (자동으로 연결된다라는 의미)

개발자가 명시적으로 클래스를 컨테이너에 연결할 필요 없이 타입힌트에 기반해 인스턴스를 식별

의존성을 해결할 수 없는 생성자 메서드 파라미터를 가진 클래스만 의존성 해결 방법을 따로 알려주면 됨

컨테이너에 대상 클래스 또는 인터페이스를 어떻게 인스턴스화 하는지 명시적으로 등록(바인딩) 하면 됨



컨테이너에 클래스 바인딩 하기

라라벨 컨테이너에 클래스를 바인딩 한다는 것은 본질적으로 컨테이너에게 '인터페이스의 인스턴스를 요구하면 이 파라미터를 가지고 의존성을 해결하라' 하는 이야기 하는 것


클로저를 바인딩 하기

//기본적인 컨테이너 바인딩
// 어떤 서비스 프로바이더 내에서
public function register()
{
    $this->app->bind(Logger::class, function($data) {
        return new Logger('/log/path/here', 'error');
    });
}

$this→app 는 모든 서비스 프로바이더에서 사용할 수 있는 컨테이너 인스턴스

bind() 메서드는 컨테이너에 바인딩할 때 사용하는 메서드
- 첫 번째 파라미터는 식별용 '키' (대개 정규화된 클래스명)
- 두 번째 파라미터는 무엇을 바인딩하냐에 따라 달라지지만 연결된 인스턴스를 만들어서 돌려주려면 어떤 과정을 거쳐야 하는 지 컨테이너에게 알려주는 무엇

// 컨테이너 클로저 바인딩에서 전달받은 $app 인자 사용하기
// 이 바인딩은 기술적으로 특별한 게 없음, 컨테이너의 오토와이어링으로 의존성이 해결 됨
$this->app->bind(UserMailer::class, function($app) {
    return new UserMailer(
        $app->make(Mailer::class),
        $app->make(Logger::class),
        $app->make(Slack::class)
    );
});

이 클로저는 클래스의 새로운 인스턴스를 요청할 때마다 실행되고, 새로운 인스턴스 결과물이 변환될 것이라는 점 유의


싱글턴, 별칭, 인스턴스에 바인딩하기

싱글턴패턴(singletone()) : bind() 메서드와 다르게 바인딩한 클로저의 결과를 캐싱해서 인스턴스를 요청할 때마다 새로운 인스턴스를 만들지 않고 캐싱한 인스턴스를 반환하게 할 수 있음

// 컨테이너에 싱클턴 바인딩 하기
public function register()
{
    $this->app->singleton(Logger::class, function(){
        return new Logger('\log\path\here', 'error');
    });
}

싱글턴으로 반환하길 원하는 객체의 인스턴스를 이미 가지고 있는경우 싱글턴과 유사한 instance() 메서드를 이용가능

public function register()
{
    $logger = new Logger('\log\path\here', 'error');
    $this->app->instance(Logger::class, $logger);
}

클래스에 별칭을 붙이거나 단축 문자열 키에 클래스를 바인드하거나, 클래스에 단축 문자열 키를 바인딩 할 수 있음

// Logger를 요청받으면 FirstLogger를 준다
$this->app->bind(Logger::class, FirstLogger::class);

// log를 요청받으면 FirstLogger를 준다
$this->app->bind('log',FirstLogger::class);

// log를 요청받으면 FirstLogger를 준다.
$this->app->alias(FirstLogger::class, 'log');

인스턴스를 인터페이스에 바인딩 하기

// 타입힌팅과 인터페이스에 바인딩하기
use Interfaces\Mailer as MailerInterface;

class UserMailwer
{
    protected $mailer;

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

// service provider
public function register()
{
    $this->app->bind(\Interfaces\Mailer::class, function(){
        return new MailgunMailer(...);
    });
}

Mailer나 Logger 인터페이스를 코드 전반에 걸쳐 타입힌트할 수 있고, 모든 곳에서 사용할 특정 메일러나 로거를 서비스 프로바이더에서 한 번만 선택하면 됨

큰 이점은 나중에 MailgunMailer가 아닌 다른 메일 송신 서비스를 사용하기로 했을 때, 새로운 송신 서비스용 메일 클래스가 Mailer 인터페이스를 구현하기만 하면, 서비스 프로바이더에서 코드 교체만 해도 잘 작동함 (즉 코드의 재사용이 가능)


상황에 따라 바인딩하기

로그의 경우 syslog에 남기거나 파일로 남길 수 있기 때문에 구분해주어야 함

// 서비스 프로바이더 안에서
public function register() 
{
    // FileWrangler 클래스에서 의존성 해결할 경우 Syslog 반환
    $this->app->when(FileWrangle::class)
                        ->needs(Interface\Logger::class)
                        ->give(Loggers\Syslog::class;

    // SendWelcomeEmail 클래스에서 의존성 해결할 경우 PaperTrail 반환
    $this->app->when(Jobs\SendWelcomeEmail::class)
                        ->needs(Interface\Logger::class)
                        ->give(Loggers\PaperTrail::class);
}



라라벨 프레임워크의 주요 클래스의 생성자 주입

모든 컨트롤러는 컨테이너가 인스턴스화 함, 컨트롤러에서 로거 인스턴스를 사용하고 싶으면, 컨트롤러 생성자에 로거 클래스를 타입힌트 함, 라라벨이 컨트롤러를 생성할 때 컨테이너에서 로거 인스턴스를 가져와 사용할 수 있게 해줌.

// 컨트롤러의 생성자 메서드에 의존성 주입하기
class MyController extends Controller
{
    protected $logger;

    public function __construct(Logger $logger)
    {
        $this->logger = $logger;
    }

    public function index()
    {
        //코드
        $this->logger->error('Error');
    }
}

컨테이너는 컨트롤러, 미들웨어, 대기 큐 작업, 이벤트 리스너, 애플리케이션 라이프사이클의 처리 과정 중에서 라라벨이 자동으로 생성하는 모든 클래스의 의존성을 해결

따라서 이 클래스들은 모두 생성자에 의존 객체를 타입힌트 할 수 있고, 타입힌트된 의존 객체는 자동으로 주입된다고 예상할 수 있음



메서드 주입

메서드 주입을 가장 많이 하는 곳은 컨트롤러 메서드

// 컨트롤러 메서드에 의존성 주입하기
class MyController extends Controller
{
    ...
    // 메서드 의존성은 라우트 파라미터 앞이나 뒤에 적어줄 수 있다.
    public function show(Logger $logger, $id) 
    {
        $logger->error('error');
    }
}

서비스 프로바이더의 boot() 메서드에서도 메서드 주입을 할 수 있음

class Foo
{
    public function bar($parameter1){}
}

// 첫 번째 파라미터에 `value` 라는 값을 전달하면서 Foo 에 있는 bar 메서드 호출
app()->call('Foo@bar',['paramter1' => 'value']);


퍼사드와 컨테이너

라라벨의 퍼사드는 라라벨의 핵심 컨테이너에 쉽게 접근하게 하는 클래스

퍼사드에는 2가지 특정 존재

  • 모든 퍼사드는 글로벌 네임스페이스로 사용할 수 있음 (\Log 는 \Illumiate\Support\Facades\Log의 별칭)
  • 정적 메서드를 이용해 비정적 자원에 접근
// 퍼사드
Log::alert('Something has gone wrong!');

// 일반
$logger = app('log');
$logger->alert('Something has gone wrong!');

퍼사드는 정척 호출을 인스턴스의 일반 메서드 호출로 바꾸어줌


퍼사드 작동방법

모든 퍼사드는 getFacasdeAccessor() 라는 단 하나의 메서드만 가지고 있음

해당 메서드는 라라벨이 컨테이너에서 이 퍼사드에 해당하는 인스턴스를 찾는 데 사용할 키를 알려줌

<?php
namespace Illuminate\Support\Facades;

class Cache extends Facade
{
    protected static function getFacadeAccessor()
    {
        return 'cache';
    }
}

Cache::get('key');

// 위 코드는 아래와 같다.
app('cache')->get('key');

더 자세한건 매뉴얼 확인 → 라라벨코리아 한글매뉴얼


실시간 퍼사드

클래스 인스턴스 메서드를 정적 메서드로 호출하기 위해 새로운 클래스를 만드는 대신, 간단하게 정규화된 클래스명 앞에 Facades\를 붙이고 퍼사드처럼 쓸 수 있는 기능

// 실시간 퍼사드 사용하기
namespace App;

class Charts
{
    public function burndown()
    {
        ...
    }
}

// 사용
<h2>Burndown Chart</h2>
{{ Facades\App\Charts::burndown() }}

라라벨 프레임워크를 설치하면 기본적으로 제공되는 App\Providers\AppServiceProvider에 바인딩을 몰아 넣어도 되지만 식별하기 편하도록 기능을 그룹별로 나누어 고유한 서비스 프로바이더를 만들고 register() 메서드에 필요한 클래스를 등록하는 것을 추천

728x90
Comments