Devin.KR

PHP · 심화

설계와 보안으로 깊어지는 PHP

의존성 주입 - 생성자 주입과 작은 컨테이너

new 를 안에서 하지 않기, 인터페이스에 의존, 생성자 주입, 클로저 기반 간단한 컨테이너, 서비스 로케이터와의 차이

개발자KR · 원고 갱신

이 장에서 배우는 것

클래스가 일하려면 다른 객체가 필요하다. 수강 신청 서비스는 강좌를 찾을 저장소, 현재 시각, 알림 수단을 쓴다. 이 객체들을 서비스 안에서 직접 만들면 서비스는 특정 구현에 묶이고, 시각이나 알림 같은 바깥 세계가 끼어드는 순간 확인하기 어려운 코드가 된다. 이 장에서는 필요한 객체를 밖에서 받는 방식인 의존성 주입(dependency injection, DI)을 다루고, 객체를 조립하는 작은 컨테이너를 클로저로 직접 만든다.

  • 클래스 안에서 new 를 호출하는 코드가 어떤 문제를 만드는지 설명한다.
  • 구체 클래스 대신 인터페이스에 의존하도록 타입을 바꾼다.
  • 생성자 주입(constructor injection)으로 의존 대상을 드러낸다.
  • 클로저로 등록하고 꺼내 쓰는 컨테이너를 약 20줄로 구현한다.
  • 서비스 로케이터(service locator)와 DI 가 어디서 갈라지는지 구별한다.

문제 상황

학원 시스템에 수강 신청 기능을 붙인다고 하자. 처음에는 한 클래스에 모든 것을 넣는 편이 빠르다. 아래는 실행하지 않는 스케치다.

final class EnrollmentService
{
    public function enroll(int $courseId, string $student): string
    {
        $courses = new PdoCourseRepository(new PDO('sqlite:/var/data/academy.db'));
        $course  = $courses->find($courseId);
        if ($course === null) {
            return 'no-course';
        }

        $now = date('Y-m-d H:i:s');
        $courses->addEnrollment($courseId, $student, $now);

        (new SmsNotifier('https://sms.example.test'))->send($student, '접수되었다.');
        return 'ok';
    }
}

동작은 하지만 실무에서 곧 문제가 드러난다.

  • 정원이 찼을 때의 동작을 확인하려면 실제 파일 데이터베이스가 있어야 한다.
  • 신청 시각이 date() 로 고정되어 있어서 "3월 2일 오전 9시에 접수했을 때"를 재현할 수 없다.
  • 확인하려고 실행할 때마다 문자 메시지 서버로 요청이 나간다.
  • 저장소를 다른 구현으로 바꾸려면 서비스 코드를 열어서 고쳐야 한다.
  • 생성자 시그니처만 봐서는 이 클래스가 무엇을 필요로 하는지 알 수 없다. 필요한 것이 메서드 본문 속에 숨어 있다.

원인은 하나다. 서비스가 자기가 쓸 객체를 직접 고르고 만든다. 서비스의 본업은 "정원을 확인하고 신청을 받는 규칙"인데, 부품을 구하는 일까지 떠안은 것이다.

new 를 밖으로 내보내기

해법은 역할을 나누는 것이다. 서비스는 "저장소가 있다, 시계가 있다, 알림 수단이 있다"는 사실만 전제하고, 실제로 어떤 구현을 쓸지는 서비스를 만드는 쪽이 정한다. 아래 그림은 두 구조를 나란히 놓은 것이다.

서비스가 구체 클래스를 직접 만들면 구현에 묶이고, 인터페이스만 알면 구현을 밖에서 바꿔 끼울 수 있다

인터페이스에 의존한다

서비스가 알아야 하는 것은 "무엇을 할 수 있는가"이지 "어떻게 하는가"가 아니다. 그래서 필요한 능력마다 인터페이스를 정의한다. 기본서에서 다룬 인터페이스 문법 그대로다.

interface Clock
{
    public function now(): string;
}

서비스는 Clock 만 안다. 운영에서는 실제 시각을 돌려주는 구현을, 확인할 때는 늘 같은 시각을 돌려주는 구현을 끼우면 된다. 이 장의 예제에서는 항상 같은 값을 주는 FixedClock 을 쓴다. 덕분에 출력이 실행할 때마다 같다.

생성자 주입

필요한 객체를 받는 가장 단순한 방법은 생성자 매개변수다. 객체는 만들어지는 순간 일할 준비가 끝나고, 생성자 시그니처가 곧 의존 목록이 된다.

final class EnrollmentService
{
    public function __construct(
        private readonly CourseRepository $courses,
        private readonly Clock $clock,
        private readonly Notifier $notifier,
    ) {
    }
}

이 선언만 읽어도 서비스가 저장소, 시계, 알림 수단 세 가지를 쓴다는 것이 보인다. 하나라도 빠지면 객체를 만들 수 없으므로 "나중에 세터로 넣어야 하는데 깜빡한" 반쯤 만들어진 객체도 생기지 않는다. 생성자 매개변수에 readonly 를 붙이면 만든 뒤에 의존 대상이 바뀌는 일도 막는다.

세터나 메서드 매개변수로 넘기는 방식도 있지만, 객체가 평생 필요로 하는 협력자는 생성자로 받는 편이 안전하다. 이 장에서는 생성자 주입만 쓴다.

작은 컨테이너 만들기

생성자 주입을 쓰면 객체를 만드는 쪽의 코드가 길어진다. 서비스 하나를 만들려면 저장소, 시계, 알림 수단을 먼저 만들어야 하고, 저장소가 다시 데이터베이스 연결을 요구할 수도 있다. 이 조립 코드를 한곳에 모아 이름으로 꺼내 쓰게 한 것이 컨테이너(container)다.

컨테이너의 핵심은 두 가지다. 이름(보통 인터페이스 이름)과 "만드는 방법"을 짝지어 등록해 두고, 요청이 오면 그 방법을 실행해 객체를 돌려준다. PHP 에서 "만드는 방법"은 클로저로 표현하기 좋다.

final class Container
{
    private array $definitions = [];
    private array $instances = [];

    public function bind(string $id, Closure $factory): void
    {
        $this->definitions[$id] = [$factory, false];
    }

    public function singleton(string $id, Closure $factory): void
    {
        $this->definitions[$id] = [$factory, true];
    }

    public function get(string $id): mixed
    {
        // 이미 만든 공유 객체가 있으면 그대로 돌려준다.
        // 등록 정보가 없으면 예외, 있으면 공장 클로저를 실행한다.
    }
}

bind 는 꺼낼 때마다 새로 만들고, singleton 은 처음 한 번만 만들어 보관한다. 클로저는 컨테이너 자신을 인자로 받으므로, 공장 안에서 다른 항목을 get 으로 꺼내 이어 붙일 수 있다. 이 연결 덕분에 의존 관계가 깊어도 등록 코드는 항목마다 한 줄씩 늘어날 뿐이다.

중요한 점은 객체가 필요한 시점에 처음 만들어진다는 것이다. 등록만 해 두고 쓰지 않은 항목은 만들어지지 않는다. 컨테이너가 어떻게 의존을 풀어 나가는지는 아래 그림과 같다.

컨테이너는 서비스를 요청받으면 공장 클로저를 실행하고, 클로저가 필요한 의존을 다시 꺼내 조립해 돌려준다

서비스 로케이터와의 차이

컨테이너를 만들면 한 가지 유혹이 생긴다. 서비스에 컨테이너 자체를 넘기고, 필요할 때마다 안에서 $container->get(...) 을 부르는 방식이다. 이것이 서비스 로케이터 패턴이다. 겉보기에는 DI 와 비슷하지만 구조가 정반대다.

생성자 주입과 서비스 로케이터는 의존을 어디서 드러내는지가 다르다
구분생성자 주입서비스 로케이터
의존 목록이 보이는 곳생성자 매개변수메서드 본문 곳곳
서비스가 컨테이너를 아는가모른다안다
확인용 객체 준비가짜 구현을 직접 넘긴다컨테이너를 만들고 항목을 등록한다
빠진 의존이 드러나는 때객체를 만들 때해당 줄이 실행될 때

DI 에서 컨테이너는 조립을 맡은 바깥쪽 코드(예제에서는 main.php)에서만 쓰인다. 서비스는 컨테이너가 있는지도 모른다. 로케이터 방식에서는 서비스가 컨테이너에 기대므로, 컨테이너가 없는 곳에서 서비스를 재사용할 수 없고 필요한 의존도 생성자에서 보이지 않는다. 이 장의 규칙은 간단하다. get 은 조립하는 곳에서만 부른다.

완성 코드

아래는 한 파일로 모은 완성 코드다. 실제 프로젝트에서는 앞 장에서 만든 구조대로 클래스마다 파일을 나누고 자동 로딩을 쓰면 된다. 여기서는 실행이 간단하도록 한 파일에 두었다. 외부 패키지는 쓰지 않는다.

main.php

<?php

declare(strict_types=1);

final class Course
{
    public function __construct(
        public readonly int $id,
        public readonly string $title,
        public readonly int $capacity,
    ) {
    }
}

interface CourseRepository
{
    public function find(int $id): ?Course;

    public function countEnrolled(int $courseId): int;

    public function addEnrollment(int $courseId, string $student, string $enrolledAt): void;
}

interface Clock
{
    public function now(): string;
}

interface Notifier
{
    public function send(string $to, string $message): void;
}

final class ArrayCourseRepository implements CourseRepository
{
    /** @var array<int, Course> */
    private array $courses = [];

    /** @var array<int, list<array{student: string, at: string}>> */
    private array $enrollments = [];

    /** @param list<Course> $courses */
    public function __construct(array $courses)
    {
        foreach ($courses as $course) {
            $this->courses[$course->id] = $course;
        }
    }

    public function find(int $id): ?Course
    {
        return $this->courses[$id] ?? null;
    }

    public function countEnrolled(int $courseId): int
    {
        return count($this->enrollments[$courseId] ?? []);
    }

    public function addEnrollment(int $courseId, string $student, string $enrolledAt): void
    {
        $this->enrollments[$courseId][] = ['student' => $student, 'at' => $enrolledAt];
    }

    /** @return list<array{student: string, at: string}> */
    public function enrollments(int $courseId): array
    {
        return $this->enrollments[$courseId] ?? [];
    }
}

final class FixedClock implements Clock
{
    public function __construct(private readonly string $value)
    {
    }

    public function now(): string
    {
        return $this->value;
    }
}

final class MemoryNotifier implements Notifier
{
    /** @var list<string> */
    private array $messages = [];

    public function send(string $to, string $message): void
    {
        $this->messages[] = "{$to}: {$message}";
    }

    /** @return list<string> */
    public function messages(): array
    {
        return $this->messages;
    }
}

final class EnrollmentService
{
    public function __construct(
        private readonly CourseRepository $courses,
        private readonly Clock $clock,
        private readonly Notifier $notifier,
    ) {
    }

    public function enroll(int $courseId, string $student): string
    {
        $course = $this->courses->find($courseId);
        if ($course === null) {
            return 'no-course';
        }
        if ($this->courses->countEnrolled($courseId) >= $course->capacity) {
            return 'full';
        }

        $this->courses->addEnrollment($courseId, $student, $this->clock->now());
        $this->notifier->send($student, "{$course->title} 수강 신청이 접수되었다.");
        return 'ok';
    }
}

final class Container
{
    /** @var array<string, array{Closure, bool}> */
    private array $definitions = [];

    /** @var array<string, mixed> */
    private array $instances = [];

    public function bind(string $id, Closure $factory): void
    {
        $this->definitions[$id] = [$factory, false];
    }

    public function singleton(string $id, Closure $factory): void
    {
        $this->definitions[$id] = [$factory, true];
    }

    public function get(string $id): mixed
    {
        if (array_key_exists($id, $this->instances)) {
            return $this->instances[$id];
        }
        if (!isset($this->definitions[$id])) {
            throw new LogicException("등록되지 않은 서비스: {$id}");
        }

        [$factory, $shared] = $this->definitions[$id];
        $instance = $factory($this);
        if ($shared) {
            $this->instances[$id] = $instance;
        }
        return $instance;
    }
}

// 조립: 어떤 구현을 쓸지는 여기서만 정한다.
$container = new Container();
$container->singleton(
    ArrayCourseRepository::class,
    fn () => new ArrayCourseRepository([
        new Course(1, '파이썬 기초', 2),
        new Course(2, '웹 디자인', 1),
    ]),
);
$container->singleton(CourseRepository::class, fn (Container $c) => $c->get(ArrayCourseRepository::class));
$container->singleton(Clock::class, fn () => new FixedClock('2026-03-02 09:00:00'));
$container->singleton(MemoryNotifier::class, fn () => new MemoryNotifier());
$container->singleton(Notifier::class, fn (Container $c) => $c->get(MemoryNotifier::class));
$container->bind(
    EnrollmentService::class,
    fn (Container $c) => new EnrollmentService(
        $c->get(CourseRepository::class),
        $c->get(Clock::class),
        $c->get(Notifier::class),
    ),
);

$service = $container->get(EnrollmentService::class);

echo "== 신청 처리 ==\n";
$requests = [[1, '김민준'], [1, '이서연'], [1, '박지후'], [2, '최하윤'], [9, '정도윤']];
foreach ($requests as [$courseId, $student]) {
    echo "{$student} -> 강좌 {$courseId}: ", $service->enroll($courseId, $student), "\n";
}

echo "== 강좌 1 신청 기록 ==\n";
foreach ($container->get(ArrayCourseRepository::class)->enrollments(1) as $row) {
    echo "{$row['student']} @ {$row['at']}\n";
}

echo "== 알림 ==\n";
foreach ($container->get(MemoryNotifier::class)->messages() as $message) {
    echo $message, "\n";
}

echo "== 인스턴스 ==\n";
echo '저장소 공유: ',
    $container->get(CourseRepository::class) === $container->get(CourseRepository::class) ? '예' : '아니오',
    "\n";
echo '서비스 공유: ',
    $container->get(EnrollmentService::class) === $service ? '예' : '아니오',
    "\n";

echo "== 컨테이너 없이 조립 ==\n";
$spy = new MemoryNotifier();
$alone = new EnrollmentService(
    new ArrayCourseRepository([new Course(5, '드로잉', 1)]),
    new FixedClock('2026-01-01 00:00:00'),
    $spy,
);
echo $alone->enroll(5, '강서준'), ' / ', $alone->enroll(5, '윤지아'), "\n";
echo '알림 ', count($spy->messages()), "건\n";

echo "== 미등록 ==\n";
try {
    $container->get('없는서비스');
} catch (LogicException $e) {
    echo $e->getMessage(), "\n";
}

줄별 해설

  • 세 인터페이스: CourseRepository, Clock, Notifier 는 서비스가 필요로 하는 능력을 이름 붙인 것이다. 서비스가 아는 것은 이 셋뿐이다.
  • ArrayCourseRepository: 배열 두 개로 강좌와 신청 기록을 들고 있는 구현이다. enrollments() 는 인터페이스에 없는 확인용 메서드다. 서비스는 이 메서드를 쓰지 않고, main.php 가 결과를 출력할 때만 쓴다.
  • FixedClock: 생성자로 받은 문자열을 그대로 돌려준다. 같은 입력이면 같은 출력이 나오므로 결과를 예측할 수 있다.
  • MemoryNotifier: 메시지를 보내는 대신 배열에 쌓는다. 확인할 때 "무엇을 보냈는가"를 나중에 읽을 수 있다.
  • EnrollmentService: 생성자에서 세 의존을 받고, enroll 안에서는 new 를 한 번도 쓰지 않는다. 강좌가 없으면 'no-course', 정원이 찼으면 'full', 접수하면 'ok' 를 돌려준다. 예외로 알리는 방법은 다음 장에서 다룬다.
  • Container::get: 공유 객체가 이미 있으면 그것을 돌려준다. 값이 null 이어도 구분되도록 isset 대신 array_key_exists 로 확인한다. 등록되지 않은 이름은 LogicException 으로 알린다. 등록되어 있으면 공장 클로저에 컨테이너 자신을 넘겨 실행하고, singleton 으로 등록된 항목만 보관한다.
  • 별칭 등록: CourseRepository::class 를 등록하는 클로저는 ArrayCourseRepository::class 를 꺼내 돌려줄 뿐이다. 인터페이스 이름과 구체 이름이 같은 인스턴스를 가리키므로, 서비스가 쓰는 저장소와 main.php 가 들여다보는 저장소가 동일하다. Notifier 도 같은 방식이다.
  • EnrollmentService 등록: bind 로 등록했으므로 꺼낼 때마다 새 서비스가 만들어진다. 서비스는 상태가 없으니 새로 만들어도 문제없고, 상태를 쥔 저장소와 알림 수단은 singleton 이라 공유된다.
  • 신청 처리 반복문: foreach 의 [$courseId, $student] 는 배열 구조 분해다. 강좌 1의 정원은 2명이므로 세 번째 신청은 full 이 된다.
  • 인스턴스 비교: 저장소는 singleton 이라 두 번 꺼내도 같은 객체이고, 서비스는 bind 라 앞에서 꺼낸 $service 와 다른 객체다. 출력 문장의 삼항 연산자는 비교식 전체를 조건으로 삼는다.
  • 컨테이너 없이 조립: 컨테이너는 편의일 뿐이다. 서비스는 생성자만 맞으면 어디서든 만들 수 있다. 정원이 1명인 강좌와 다른 시각, 따로 만든 알림 수단을 직접 넘겨서 확인했다.
  • 미등록 항목: 등록하지 않은 이름을 요청하면 LogicException 이 던져진다. 설정 실수는 프로그램 논리의 오류이므로 이 예외가 어울린다.

실행 결과

$ php main.php
== 신청 처리 ==
김민준 -> 강좌 1: ok
이서연 -> 강좌 1: ok
박지후 -> 강좌 1: full
최하윤 -> 강좌 2: ok
정도윤 -> 강좌 9: no-course
== 강좌 1 신청 기록 ==
김민준 @ 2026-03-02 09:00:00
이서연 @ 2026-03-02 09:00:00
== 알림 ==
김민준: 파이썬 기초 수강 신청이 접수되었다.
이서연: 파이썬 기초 수강 신청이 접수되었다.
최하윤: 웹 디자인 수강 신청이 접수되었다.
== 인스턴스 ==
저장소 공유: 예
서비스 공유: 아니오
== 컨테이너 없이 조립 ==
ok / full
알림 1건
== 미등록 ==
등록되지 않은 서비스: 없는서비스

실무에서 자주 틀리는 것

생성자 안에서 몰래 new 하기

생성자를 쓰고 있어도 안에서 new 하면 주입이 아니다. 시그니처에는 의존이 나타나지 않고, 바꿔 끼울 방법도 없다.

// 틀린 코드
final class EnrollmentService
{
    private Clock $clock;

    public function __construct()
    {
        $this->clock = new SystemClock();
    }
}
// 고친 코드
final class EnrollmentService
{
    public function __construct(private readonly Clock $clock)
    {
    }
}

기본값이 필요하면 호출하는 쪽에서 new SystemClock() 을 넘기면 된다. "어떤 구현을 쓸지"의 결정은 조립하는 곳에 둔다.

컨테이너를 서비스에 넘기기

컨테이너를 생성자로 받으면 의존이 다시 본문 속으로 숨는다. 서비스 로케이터가 된 것이다.

// 틀린 코드
final class EnrollmentService
{
    public function __construct(private readonly Container $container)
    {
    }

    public function enroll(int $courseId, string $student): string
    {
        $courses = $this->container->get(CourseRepository::class);
        // ...
    }
}
// 고친 코드
final class EnrollmentService
{
    public function __construct(private readonly CourseRepository $courses)
    {
    }
}

이 서비스를 확인하려면 컨테이너를 만들고 항목을 등록해야 하고, 어떤 항목이 필요한지는 본문을 끝까지 읽어야 안다. 고친 코드는 필요한 것을 타입으로 선언한다.

구체 클래스를 타입으로 쓰기

주입은 받지만 타입이 구체 클래스라면 절반만 한 것이다.

// 틀린 코드
public function __construct(private readonly ArrayCourseRepository $courses)
{
}
// 고친 코드
public function __construct(private readonly CourseRepository $courses)
{
}

구체 타입으로 받으면 다른 저장소 구현을 넘기는 순간 타입 오류가 난다. 서비스가 쓰는 메서드만 인터페이스에 담고, 그 인터페이스 타입으로 받는다. 반대로 구현 클래스에만 있는 메서드(예제의 enrollments())를 서비스가 필요로 하게 되었다면, 그 메서드를 인터페이스에 올릴지 다시 생각해야 한다는 신호다.

bind 와 singleton 혼동하기

상태를 쥔 객체를 bind 로 등록하면 꺼낼 때마다 새로 만들어져 이전에 쌓은 데이터가 사라진다.

// 틀린 코드
$container->bind(
    ArrayCourseRepository::class,
    fn () => new ArrayCourseRepository([new Course(1, '파이썬 기초', 2)]),
);
// 서비스가 쓰는 저장소와 결과를 읽는 저장소가 서로 다른 객체가 된다.
// 고친 코드
$container->singleton(
    ArrayCourseRepository::class,
    fn () => new ArrayCourseRepository([new Course(1, '파이썬 기초', 2)]),
);

기준은 단순하다. 상태를 공유해야 하는 객체(저장소, 알림 기록, 연결)는 singleton, 상태가 없거나 요청마다 따로여야 하는 객체는 bind 를 쓴다.

한눈에 보기

이 장의 용어와 예제 코드의 대응
용어뜻예제에서
의존클래스가 일하려고 쓰는 다른 객체저장소, 시계, 알림 수단
인터페이스 의존구현이 아닌 규약 타입으로 선언CourseRepository 등
생성자 주입의존을 생성자 매개변수로 받음EnrollmentService
컨테이너이름과 만드는 방법을 등록해 조립Container
서비스 로케이터서비스가 컨테이너에서 직접 꺼냄쓰지 않는다
컨테이너 등록 방식 두 가지의 선택 기준
메서드꺼낼 때어울리는 대상예제에서
bind매번 새로 만든다상태 없는 서비스EnrollmentService
singleton처음 한 번만 만든다상태를 공유하는 객체저장소, 알림, 시계

연습 문제

  1. 다음 클래스에서 숨은 의존을 모두 찾고, 생성자 주입으로 바꾼 시그니처를 써라.
    final class AttendanceService
    {
        public function mark(int $courseId, string $student): void
        {
            $log = new FileLogger('/tmp/attendance.log');
            $at  = date('H:i');
            $log->write("{$student} {$at}");
        }
    }
  2. Container 에 등록 여부를 알려 주는 has(string $id): bool 메서드를 추가하라.
  3. 알림을 메모리에 쌓는 대신 한 줄씩 화면에 출력하는 EchoNotifier 를 만들어 교체하려 한다. EnrollmentService 를 고치지 않고 바꾸려면 어느 부분만 수정하면 되는가.
  4. 다음 코드는 DI 인가 서비스 로케이터인가. 판단 근거를 한 문장으로 써라.
    final class ReportService
    {
        public function __construct(private readonly Container $c)
        {
        }
    
        public function run(): void
        {
            $this->c->get(Notifier::class)->send('관리자', '보고서 완료');
        }
    }

정답과 해설

  1. 숨은 의존은 두 가지다. FileLogger 로 구체 클래스를 직접 만든 것과, date() 로 현재 시각을 직접 읽은 것이다. 먼저 Logger, Clock 인터페이스를 정의한 뒤 다음과 같이 바꾼다.
    final class AttendanceService
    {
        public function __construct(
            private readonly Logger $log,
            private readonly Clock $clock,
        ) {
        }
    }
    mark 안에서는 $this->clock->now() 와 $this->log->write() 를 쓴다. 시각 형식은 Clock 구현이 정하면 된다.
  2. 등록 정보에 키가 있는지만 확인하면 된다.
    public function has(string $id): bool
    {
        return isset($this->definitions[$id]);
    }
    get 안의 isset($this->definitions[$id]) 검사도 같은 판단이므로 has 를 부르도록 정리할 수 있다. 다만 has 를 서비스 안에서 쓰기 시작하면 로케이터로 흐르기 쉬우니 조립 쪽 코드에서만 쓴다.
  3. 조립하는 부분만 바꾼다. Notifier 를 구현한 EchoNotifier 클래스를 추가하고, 컨테이너에서 Notifier::class 를 등록하는 줄이 EchoNotifier 를 만들도록 고친다. 서비스는 Notifier 인터페이스만 알기 때문에 한 줄도 바뀌지 않는다. 이 장의 예제에서는 MemoryNotifier 별칭 한 줄을 바꾸는 것으로 충분하다. 단, 알림 기록을 읽는 출력 코드가 MemoryNotifier 를 직접 꺼내 쓰고 있다면 그 부분은 별도로 손봐야 한다.
  4. 서비스 로케이터다. 서비스가 컨테이너를 생성자로 받고, 필요한 Notifier 를 본문에서 직접 꺼내므로 이 클래스의 의존이 생성자 시그니처에 드러나지 않는다. DI 로 고치려면 Notifier 를 생성자 매개변수로 받으면 된다.
오탈자·오류 제보 비공개로 접수되어 원고 수정에 반영됩니다

이메일 등 개인정보는 받지 않습니다. 답변이 필요한 질문은 아래 댓글을 이용해 주세요.

READER FEEDBACK

질문·의견

내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.

댓글 0

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

댓글을 남기려면 로그인이 필요합니다.