> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# 패키지의 컨테이너 태그와 플러그인 집약

> Laravel 13의 컨테이너 태그로 여러 패키지의 구현을 집약하고, tagged()의 지연 해석, giveTagged()와의 차이, 중복 등록과 라이프사이클을 검증합니다.

여러 패키지나 이를 사용하는 애플리케이션에서 같은 집약 서비스에 기능을 추가할 수 있게 하고 싶다면 컨테이너의 `tag()`와 `tagged()`를 사용할 수 있습니다. 집약하는 쪽이 모든 구현 클래스를 알 필요는 없습니다.

이 페이지는 Laravel 13을 대상으로 하며, Framework `v13.38.0`의 구현과 공식 테스트를 확인한 내용입니다. 바인딩의 기본은 [서비스 컨테이너](https://kawax.biz/ko/service-container.md)를, 프로바이더 배포 방법은 [패키지 개발](https://kawax.biz/ko/advanced/package-development.md)을 참조하세요.

## 태그는 서비스의 등록 키를 묶는다

`tag()`는 클래스 이름 등의 컨테이너 등록 키를 태그 목록에 추가합니다. 태그를 붙이는 것만으로는 해당 서비스가 생성되지 않습니다.

`tagged()`는 해당 태그에 등록된 서비스를 열거하기 위한 `iterable`을 반환합니다. 특정 인터페이스를 구현한 클래스를 자동으로 검색하는 기능이 아니라, 각 프로바이더가 명시적으로 참여하는 구조입니다. `vendor:publish --tag`의 공개 리소스용 태그와도 다릅니다.

```mermaid theme={null}
flowchart TD
    A["각 프로바이더의 register()"] --> B["구현을 bind()하고<br>같은 태그에 추가"]
    B --> C["집약 서비스를 해석"]
    C --> D["tagged()의 iterable을 보관"]
    D --> E["foreach로 열거"]
    E --> F["등록 키마다 make()"]
    F --> G["구현의 collect()를 호출"]
```

## 확장 가능한 리포트 패키지 만들기

### 구현이 공유하는 컨트랙트 준비하기

예로 여러 리포트를 모으는 `acme/reports` 패키지를 만듭니다. `Acme\Reports\`가 `src/`에 대응하는 Composer PSR-4 설정을 전제로 합니다.

`src/Contracts/Report.php`에 플러그인이 구현할 컨트랙트를 정의합니다.

```php theme={null}
<?php

namespace Acme\Reports\Contracts;

interface Report
{
    public function collect(): array;
}
```

최소한의 구현으로 `src/RuntimeReport.php`와 `src/MemoryReport.php`를 준비합니다.

```php theme={null}
<?php

namespace Acme\Reports;

use Acme\Reports\Contracts\Report;

class RuntimeReport implements Report
{
    public function collect(): array
    {
        return ['php_version' => PHP_VERSION];
    }
}
```

```php theme={null}
<?php

namespace Acme\Reports;

use Acme\Reports\Contracts\Report;

class MemoryReport implements Report
{
    public function collect(): array
    {
        return ['allocated_bytes' => memory_get_usage(true)];
    }
}
```

### 집약 서비스는 iterable을 받는다

`src/ReportCollector.php`는 각 구현을 열거할 때 수집 처리를 호출합니다. 공개할 태그 이름을 상수로 두면 사용하는 쪽도 같은 이름을 참조할 수 있습니다.

```php theme={null}
<?php

namespace Acme\Reports;

use Acme\Reports\Contracts\Report;
use UnexpectedValueException;

class ReportCollector
{
    public const TAG = 'acme.reports.collectors';

    /** @param iterable<Report> $reports */
    public function __construct(private iterable $reports)
    {
    }

    public function collect(): array
    {
        $results = [];

        foreach ($this->reports as $report) {
            if (! $report instanceof Report) {
                throw new UnexpectedValueException('Tagged services must implement Report.');
            }

            $results[] = $report->collect();
        }

        return $results;
    }
}
```

PHP의 `iterable` 타입만으로는 요소의 타입을 검증할 수 없으며, `tag()` 자체도 `Report`의 구현인지 확인하지 않습니다. 이 예에서는 집약하는 쪽에서도 컨트랙트를 확인합니다.

### register()에서는 바인딩과 태그만 등록한다

`src/ReportsServiceProvider.php`에서 기본 구현과 집약 서비스를 등록합니다. 집약 서비스의 팩토리는 가져온 `iterable`을 그대로 전달합니다.

```php theme={null}
<?php

namespace Acme\Reports;

use Illuminate\Contracts\Container\Container;
use Illuminate\Support\ServiceProvider;

class ReportsServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->bind(RuntimeReport::class);
        $this->app->bind(MemoryReport::class);

        $this->app->tag([
            RuntimeReport::class,
            MemoryReport::class,
        ], ReportCollector::TAG);

        $this->app->bind(ReportCollector::class, function (Container $app): ReportCollector {
            return new ReportCollector($app->tagged(ReportCollector::TAG));
        });
    }
}
```

다른 패키지나 사용하는 애플리케이션은 `Report`를 구현한 `DailyReport` 등을 준비하고, 자신의 프로바이더의 `register()` 안에서 같은 태그에 추가합니다.

```php theme={null}
use Acme\Reports\ReportCollector;
use App\Reports\DailyReport;

$this->app->bind(DailyReport::class);
$this->app->tag(DailyReport::class, ReportCollector::TAG);
```

모든 프로바이더가 등록된 후 `app(ReportCollector::class)->collect()`를 호출하면 추가한 구현도 집약됩니다. 참여하는 구현이 늘어나도 `ReportCollector`의 생성 처리를 변경할 필요가 없습니다.

## tagged()와 giveTagged()는 생성 시점이 다르다

확인한 구현에서는 등록된 태그의 `tagged()`가 `RewindableGenerator`를 반환합니다. 각 서비스의 `make()`는 열거할 때 호출되며, 도중에 열거를 멈추면 나머지 서비스는 생성되지 않습니다.

또한 `RewindableGenerator`는 열거할 때마다 새로운 Generator를 만듭니다. 서비스의 결과를 캐시하는 기능이 아니므로, 일반 `bind()`라면 다시 열거할 때 인스턴스도 다시 생성됩니다. `singleton()`이나 `scoped()` 서비스는 각각의 공유 규칙을 따릅니다.

반면 컨텍스트 바인딩의 `giveTagged()`는 해석 시점에 `tagged()`의 결과를 `iterator_to_array()`로 배열로 변환합니다.

| 주입 방법 | 개별 서비스가 해석되는 시점 | 같은 집약 객체에서 다시 열거 |
| - | - | - |
| `tagged()`의 결과를 그대로 전달 | 각 요소를 열거할 때 | 다시 `make()`한다 |
| `#[Tag(...)]`를 붙인 `iterable` 인수 | 각 요소를 열거할 때 | 다시 `make()`한다 |
| `giveTagged()`로 주입 | 집약 서비스의 의존성을 해석할 때 | 보관한 배열의 요소를 재사용한다 |

배열로 한 번에 의존성을 해석하고 싶다면 앞의 팩토리를 사용한 `bind()`를 다음 등록으로 바꿀 수 있습니다.

```php theme={null}
use Acme\Reports\ReportCollector;

$this->app->bind(ReportCollector::class);

$this->app->when(ReportCollector::class)
    ->needs('$reports')
    ->giveTagged(ReportCollector::TAG);
```

`$reports`의 타입이 `iterable`이라도 이 방법으로 보관되는 값은 배열입니다. 인수의 타입만으로는 지연 해석을 보장할 수 없습니다. 또한 `giveTagged()`가 수행하는 것은 인스턴스 해석이며, 각 구현의 `collect()` 호출이 아닙니다.

`Tag` 어트리뷰트의 `resolve()`는 `tagged()`를 그대로 반환합니다. 어트리뷰트를 사용할 때는 [컨텍스트 어트리뷰트](https://kawax.biz/ko/service-container.md#컨텍스트-어트리뷰트)처럼 생성자 인수에 지정하고, 클래스 생성을 컨테이너에 맡깁니다. 직접 만든 팩토리 안의 `new`에 어트리뷰트가 자동으로 적용되지는 않습니다.

## 플러그인 등록 계약을 깨뜨리지 않기

### 중복 등록과 순서를 명시하기

`tag()`는 목록에 추가하는 처리이므로, 같은 등록 키를 다시 추가해도 중복을 제거하지 않습니다. 같은 구현이 두 번 열거되면, singleton이라 같은 객체가 반환되더라도 `collect()`는 두 번 호출됩니다.

태그 등록은 각 구현을 담당하는 프로바이더의 한 곳에 모읍니다. `bindIf()`는 바인딩 덮어쓰기를 피하기 위한 API이며, 그 이후의 `tag()` 중복을 막아 주지는 않습니다.

열거 순서는 태그 목록에 추가된 순서입니다. 태그에는 우선순위 정렬 처리가 없으므로, 서로 다른 패키지 간의 처리 순서를 프로바이더 로드 순서에만 의존하지 마세요. 순서가 공개 사양으로 필요하다면 우선순위나 식별자를 컨트랙트에 포함하고, 집약하는 쪽에서 명시적으로 정렬하도록 설계합니다.

### 태그 추가를 담당하는 프로바이더를 먼저 등록하기

등록되지 않은 태그의 `tagged()`는 예외가 아니라 빈 배열을 반환합니다. 먼저 받아 둔 빈 배열이나 `giveTagged()`로 이미 생성된 배열에 나중에 등록한 태그가 반영되지는 않습니다.

등록된 태그의 지연 열거에 대해서도 실행 도중의 목록 변경을 전제로 하지 마세요. 확인한 `tagged()`는 개수를 가져올 때 전달하는 한편, 열거할 때는 태그 목록을 참조합니다. 모든 프로바이더의 등록을 마친 후에 사용하도록 설계하면 개수와 열거 대상의 불일치도 피할 수 있습니다.

> [!WARNING]
>
> 태그 열거는 아직 로드되지 않은 프로바이더를 태그 이름으로 찾는 처리가 아닙니다. 태그 추가를 담당하는 프로바이더를 `DeferrableProvider`로 만들면, 해당 서비스가 다른 경로로 해석될 때까지 태그 등록이 실행되지 않아 목록에서 빠질 수 있습니다. 항상 참여시키고 싶은 구현의 태그 등록에는 일반 프로바이더를 사용하고, 등록 시점에는 구현을 생성하지 않도록 설계합니다.

지연 프로바이더의 시작 조건은 [DeferrableProvider](https://kawax.biz/ko/advanced/deferred-provider.md)에서, 설치 시의 로드는 [패키지 자동 검출](https://kawax.biz/ko/advanced/package-discovery.md)에서 확인할 수 있습니다.

### 태그와 서비스의 라이프사이클을 분리하기

태그를 붙여도 서비스가 자동으로 singleton이나 scoped가 되지는 않습니다. 이 예는 일반 `bind()`를 사용하며, 집약 객체에 수집 결과를 저장하지 않습니다.

특히 `giveTagged()`로 해석한 배열을 singleton 집약 서비스에 보관하면, 배열 안의 scoped 서비스도 계속 보관될 수 있습니다. 스코프 초기화는 이미 다른 객체에 저장된 참조를 교체하지 않습니다. Octane이나 큐 워커에서 사용하는 설계는 [패키지 서비스의 라이프사이클](https://kawax.biz/ko/advanced/package-service-lifetimes.md)도 참조하세요.

## 지연 해석과 배열화 테스트하기

패키지의 Composer 오토로드와 PHPUnit 환경에서 `tests/ReportCollectorTest.php`에 다음 테스트를 추가할 수 있습니다. 컨테이너 단독으로 프로바이더 등록을 호출하고 생성 횟수를 검증합니다. 애플리케이션 통합이나 자동 검출까지 포함하는 테스트는 [Orchestra Testbench](https://kawax.biz/ko/advanced/package-testing.md)로 보완합니다.

```php theme={null}
<?php

namespace Acme\Reports\Tests;

use Acme\Reports\ReportCollector;
use Acme\Reports\ReportsServiceProvider;
use Acme\Reports\RuntimeReport;
use Illuminate\Container\Container;
use PHPUnit\Framework\TestCase;

class ReportCollectorTest extends TestCase
{
    public function test_tagged_reports_are_lazy_and_can_be_resolved_again(): void
    {
        $created = 0;
        $app = $this->makeContainer($created);
        $collector = $app->make(ReportCollector::class);

        $this->assertSame(0, $created);
        $this->assertCount(2, $collector->collect());
        $this->assertSame(1, $created);
        $this->assertCount(2, $collector->collect());
        $this->assertSame(2, $created);
    }

    public function test_give_tagged_resolves_an_array_once(): void
    {
        $created = 0;
        $app = $this->makeContainer($created);
        $app->bind(ReportCollector::class);
        $app->when(ReportCollector::class)
            ->needs('$reports')
            ->giveTagged(ReportCollector::TAG);

        $collector = $app->make(ReportCollector::class);

        $this->assertSame(1, $created);
        $this->assertCount(2, $collector->collect());
        $this->assertCount(2, $collector->collect());
        $this->assertSame(1, $created);
    }

    private function makeContainer(int &$created): Container
    {
        $app = new Container;
        (new ReportsServiceProvider($app))->register();
        $app->bind(RuntimeReport::class, function () use (&$created): RuntimeReport {
            $created++;

            return new RuntimeReport;
        });

        return $app;
    }
}
```

또한 패키지를 업데이트할 때는 다음 조합을 확인합니다.

| 케이스 | 확인할 내용 |
| - | - |
| 추가 구현을 같은 태그에 등록 | 집약 대상에 포함되고 컨트랙트 타입 검사를 통과한다 |
| 같은 구현을 중복 등록 | 수집 처리가 중복되는 것을 감지할 수 있다 |
| 태그 미등록·잘못된 태그 이름 | 빈 목록을 허용할지, 패키지 쪽에서 오류로 처리할지가 명확하다 |
| 다른 요청·잡에서 재사용 | scoped 서비스나 수집 결과를 넘겨받지 않는다 |
| 태그 이름·컨트랙트 변경 | 사용하는 쪽의 프로바이더가 참조하는 공개 API로서 호환성을 평가한다 |

## 참고한 1차 정보

* [Laravel 공식 문서: Tagging](https://github.com/laravel/docs/blob/13.x/container.md#tagging)
* [Laravel 공식 문서: Binding primitives・Variadic tag dependencies](https://github.com/laravel/docs/blob/13.x/container.md#binding-primitives)
* [Laravel 공식 문서: 서비스 프로바이더](https://github.com/laravel/docs/blob/13.x/providers.md)
* [Laravel 공식 문서: 패키지 개발](https://github.com/laravel/docs/blob/13.x/packages.md)
* [Container: tag()・tagged()](https://github.com/laravel/framework/blob/v13.38.0/src/Illuminate/Container/Container.php)
* [ContextualBindingBuilder: giveTagged()](https://github.com/laravel/framework/blob/v13.38.0/src/Illuminate/Container/ContextualBindingBuilder.php)
* [RewindableGenerator: 다시 열거와 개수](https://github.com/laravel/framework/blob/v13.38.0/src/Illuminate/Container/RewindableGenerator.php)
* [Tag 어트리뷰트: resolve()](https://github.com/laravel/framework/blob/v13.38.0/src/Illuminate/Container/Attributes/Tag.php)
* [공식 테스트: ContainerTaggingTest](https://github.com/laravel/framework/blob/v13.38.0/tests/Container/ContainerTaggingTest.php)
* [공식 테스트: ContextualBindingTest](https://github.com/laravel/framework/blob/v13.38.0/tests/Container/ContextualBindingTest.php)


## Related topics

- [서비스 컨테이너](https://kawax.biz/ko/service-container.md)
- [고급 주제](https://kawax.biz/ko/advanced/index.md)
- [패키지의 정적 해석 (PHPStan / Larastan)](https://kawax.biz/ko/advanced/package-static-analysis.md)
- [패키지의 서비스 해석 후 훅](https://kawax.biz/ko/advanced/package-service-resolution.md)
- [패키지의 CHANGELOG와 릴리스 관리](https://kawax.biz/ko/advanced/package-changelog.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.