라라벨 서비스 컨테이너 언제, 왜 쓰는가? free

2019-08-05

서비스 컨테이너는 벨의 핵심 설계 컨셉이지만 다소 어려운 주제입니다. 다행히도 서비스 컨테이너를 몰벨을 사용할 수 있습니다. 아직 의존성 주입 같은 개념이 생소한 분들은 벨에 어느 정도 익숙해질때까지는 그냥 무시하고 지나치는게 좋습니다. 처음부터 너무 좋은 코드를 짜려고 하면 오히려 머리가 너무 복잡해져서 아무것도 못하는 경우가 많기 때문입니다. 그러나 서비스 컨테이너를 이해하지 못하면 좋은 코드를 작성하는데 한계가 있습니다. 어느 정도 벨에 익숙해졌다 싶으면 거기서 머무르지 마시고 꼭 서비스 컨테이너는 주제를 이해하려고 노력해보시기 바랍니다.


오늘 소개할 글은 크리스토프 럼펠의 벨 서비스 컨테이너가 의존성 관리에 도움을 주는 4가지 방법(4 Ways The Laravel Service Container Helps Us Managing Our Dependencies)
입니다. 크리스토프 럼펠은 서비스 컨테이너가 이해하기 힘들기 때문에 다들 “어떻게” 쓰는지만 설명하고 있어서 자신은 “왜”, “언제” 쓰는지에 초점을 맞추어 썼다고 합니다. 예제 위주로 간략히 소개하도록 하겠습니다.



벨 매뉴얼을 보면 서비스 컨테이너는 “클래스의 의존성을 관리하고 의존성을 주입하는 강력한 도구”고 소개하고 있습니다. 클래스의 의존성에 대해서는 제가 최근에 번역한 “클린 아키텍처 인 “에 잘 설명되어 있습니다. 관심있는 분들은 독을 권합니다.



특정 사용자 관련 통계를 CSV 파로 추출하는 클래스가 있다고 합시다.


class UserStatsCsvExporter implements UserStatsExporterContract
{
public function export(int $userId)
{
// Load user statistics...
// Export file...
}
}

아래와 같이 컨트롤러에서 인스턴스를 생성해서 쓸 수 있습니다.


class ExportController extends Controller
{
public function handle()
{
$userStatsExporter = new UserStatsCsvExporter();

return $userStatsExporter->export(12);
}
}

위와 같이 의존성을 코드 내부에서 인스턴스화 하는 코드는 반적으로 권장되지 않습니다. 첫째 ExportControllerUserStatsCsvExporter에 강하게 결합됩니다. 둘째 단 책임 원칙을 위반 합니다. ExportController의 역할은 UserStatsCsvExporter 인스턴스를 생성하는게 아닙니다. 그럼 UserStatsCsvExporter의 인스턴스를 생성하는 책임은 누가 가져야하느냐? 이 책임을 갖는 게 “서비스 컨테이너”입니다.


1. 오토 리졸빙 Auto-Resolving


다음과 같이 메서드 시그니처를 통해 의존성을 주입할 수 있습니다.


public function handle(UserStatsCsvExporter $userStatsExporter)
{
return $userStatsExporter->export(12);
}

벨은 타입-힌팅된 UserStatsCsvExporter를 자동으로 인스턴스화해서 주입해줍니다. 물론 의존성의 의존성도 자동으로 처리해줍니다.


class UserStatsCsvExporter implements UserStatsExporterContract
{

/** @var Translator */
private $translator;

public function __construct(Translator $translator)
{
$this->translator = $translator;
}

public function export(int $userId)
{
// Load user statistics...
// Export file...
}
}

2. 컨테이너 바인딩 Bind To The Container


오토 리졸빙은 의존성이 클래스인 경우에만 작동합니다. PHP 리플렉션 API로 문제를 해결하는 것이기 때문입니다. 다음과 같이 클래스를 인스턴스화하는데 문자열이 필요하면 벨이 어떤 값을 넣어줘야하는지 알 수 없습니다.


class Translator
{
/** @var string */
private $language;

public function __construct(string $language)
{
$this->language = $language;
}

public function translate(string $word)
{
// Translate word...
}
}

이런 경우 다음과 같이 서비스 프로바이더에서 직접 해결해줍니다.


class UserStatsExporterProvider extends ServiceProvider
{
public function register()
{
$this->app->bind(UserStatsCsvExporter::class, function() {
return new UserStatsCsvExporter(new Translator(config('app.locale')));
});
}
}

3. 인터페이스 바인딩 Bind To Interfaces


컨트롤러에 구체 클래스를 주입하는 것보다는 인터페이스를 주입하는 것이 더 권장됩니다. 그래야 특정 구현에 묶이지 않고 나중에 언제든 다른 구현물로 바꿔서 쓸 수 있기 때문입니다.


예제의 경우 UserStatsCsvExporterUserStatsExporterContract 인터페이스를 구현하고 있으므로, 컨트롤러에 UserStatsCsvExporter 대신 UserStatsExporterContract를 주입할 수 있습니다.


public function handle(UserStatsExporterContract $userStatsExporter)
{
return $userStatsExporter->export(12);
}

다만 이렇게 하면 벨이 UserStatsExporterContract의 구현체 중 어떤 것을 넣어줘야하는지 모릅니다. 그래서 이 경우도 서비스 프로바이더에서 직접 해결해줍니다.


public function register()
{
$this->app->bind(UserStatsExporterContract::class, function() {
return new UserStatsXmlExporter(new Translator(config('app.locale')));
});
}

UserStatsCsvExporter대신 UserStatsExporterContract를 바인드하고 있는걸 눈여겨 보세요.


4. 인스턴스 공유


상태를 저장할 필요가 있거나, 더 좋은 성능이 필요하면 singleton 메서드로 하나의 인스턴스를 공유하게 할 수 있습니다. singleton 메서드를 사용하면 매번 새로운 객체를 만드는 대신 기존 객체를 사용합니다.


public function register()
{
$this->app->singleton(UserStatsExporterContract::class, function() {
return new UserStatsXmlExporter(new Translator(config('app.locale')));
});
}

마치며


그냥 기능 설명만 봤을 때는 ‘그래서 이게 왜 필요하지?’는 의문이 있었는데, 이렇게 ‘왜’ ‘언제’ 쓰는지를 기준으로 정리하니 정말 좋네요. 모쪼록 다소 접근하기 힘든 주제인 서비스 컨테이너를 이해하는데 도움이 되었길 바랍니다.





이현석

바쁜 팀장님 대신 알려주는 신입 PHP 개발자 안내서를 쓰고, 클린 아키텍처 인 PHP를 번역했습니다. 2020년에 출간될 Laravel Up & Running 2nd Edition을 번역하고 있습니다.