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

# 패키지의 서비스 데코레이션과 extend

> Laravel 13의 Container::extend()를 읽고, 계약을 유지하는 데코레이터, 해석 전후의 등록, singleton의 유지 범위, 이중 데코레이션과 업데이트 시의 테스트를 설명합니다.

이용 애플리케이션이 선택한 서비스 구현을 그대로 둔 채, 패키지에서 처리를 추가하고 싶을 때가 있습니다. 컨테이너의 `extend()`를 사용하면 원래의 생성 처리를 재정의하지 않고, 해석된 서비스를 데코레이터로 감쌀 수 있습니다.

이 페이지는 Laravel 공식 문서의 기본 브랜치 `13.x`와, 2026년 11월 9일에 확인한 Framework 최신 릴리스 `v13.40.0`의 공식 구현·테스트를 참조합니다. [서비스 해석 후 훅](https://kawax.biz/ko/advanced/package-service-resolution.md)과의 차이에 더해, **등록하는 시점에 인스턴스가 저장되어 있는지 여부**에 따라 달라지는 유지 범위를 다룹니다.

## 설정 추가와 객체 교체를 구분하기

| API | 목적 | 콜백의 반환값 |
| - | - | - |
| `bind()`·`singleton()` | 서비스의 생성 방법과 공유 여부를 등록한다 | 생성할 서비스 |
| `afterResolving()` | 해석한 서비스에 설정 등을 적용한다 | 서비스 교체에는 사용되지 않는다 |
| `extend()` | 생성 처리를 유지하고 서비스를 데코레이트·변경한다 | 변경 후의 서비스를 반드시 반환한다 |

`extend()`의 콜백은 대상 서비스와 컨테이너를 받습니다. 같은 객체를 설정해서 반환해도 되고, 새 데코레이터를 반환해도 됩니다. `return`을 잊으면 이후의 해석 처리에 `null`이 전달됩니다.

> [!NOTE]
>
> 여기서 다루는 것은 `Illuminate\Container\Container::extend()`입니다. Cache나 Mail 등의 Manager가 가진, 드라이버 이름과 생성 처리를 등록하는 `extend()`와는 다른 API입니다.

## 같은 계약을 구현하는 데코레이터 만들기

### 원래 구현을 교체하지 않고 감싸기

설명을 위해 메시지를 포맷하는 다음 계약을 `src/MessageFormatter.php`에 둡니다. 아래 클래스들도 각각 같은 이름의 PHP 파일을 `src/`에 만든다고 가정합니다.

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

namespace Acme\Messages;

interface MessageFormatter
{
    public function format(string $message): string;
}
```

데코레이트하기 전의 구현은 입력을 그대로 반환합니다.

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

namespace Acme\Messages;

final class PlainMessageFormatter implements MessageFormatter
{
    public function format(string $message): string
    {
        return $message;
    }
}
```

데코레이터는 같은 인터페이스를 구현하고, 전달받은 서비스에 처리를 위임합니다. 이 예에서는 위임 대상의 반환값에 패키지 고유의 접두사를 붙입니다.

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

namespace Acme\Messages;

final class PrefixedMessageFormatter implements MessageFormatter
{
    public function __construct(
        private readonly MessageFormatter $inner,
    ) {}

    public function format(string $message): string
    {
        return '[Acme] '.$this->inner->format($message);
    }
}
```

이용 측이 구체 클래스 `PlainMessageFormatter`를 타입으로 지정하고 있으면, 같은 인터페이스를 구현했을 뿐인 데코레이터로는 대체할 수 없습니다. 공개하는 의존 대상을 `MessageFormatter`로 통일하고, 인수·반환값·예외 등의 계약을 유지합니다. 동적인 메서드 전달만으로는 PHP 인터페이스 구현을 대신할 수 없습니다.

### 프로바이더에서 데코레이션 등록하기

데코레이션을 제공하는 `src/PrefixServiceProvider.php`는 생성 방법을 덮어쓰지 않고, 컨테이너에 확장 등록만 합니다. 프로바이더 로딩은 [패키지 자동 감지의 내부 구조](https://kawax.biz/ko/advanced/package-discovery.md)를 참고하세요.

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

namespace Acme\Messages;

use Illuminate\Support\ServiceProvider;

final class PrefixServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->extend(
            MessageFormatter::class,
            static fn (MessageFormatter $formatter): MessageFormatter =>
                new PrefixedMessageFormatter($formatter),
        );
    }
}
```

콜백의 두 번째 인수로 컨테이너도 전달되지만, 이 예에서는 필요하지 않으므로 선언하지 않았습니다. 대상 서비스는 인수 `$formatter`를 사용합니다. 같은 키를 콜백 안에서 다시 `make()`하면 해석이 순환하는 원인이 됩니다.

이용 애플리케이션의 `AppServiceProvider::register()` 등에서 원래 구현을 바인딩합니다.

```php theme={null}
use Acme\Messages\MessageFormatter;
use Acme\Messages\PlainMessageFormatter;

$this->app->bind(MessageFormatter::class, PlainMessageFormatter::class);
```

`extend()`는 대상의 `bind()`보다 먼저 등록할 수도 있습니다. 대상이 아직 해석되지 않았고 저장된 인스턴스도 없다면, 등록만으로 원래 서비스를 생성하지 않습니다. 공식 테스트에서도 지연 생성과 바인딩 전 등록이 검증되어 있습니다.

그 후 `app(MessageFormatter::class)->format('발송했습니다.')`는 `[Acme] 발송했습니다.`를 반환합니다. 원래 구현을 다른 `MessageFormatter` 구현으로 변경해도, 그 구현을 감싸는 구조입니다.

> [!WARNING]
>
> 첫 해석보다 먼저 등록하는 것을 기본으로 하세요. 다른 프로바이더가 먼저 서비스를 사용할 수 있으므로, 단순히 `boot()`로 옮기는 것만으로는 등록 전 해석을 막을 수 없습니다. 또한 확장 등록만 담당하는 프로바이더를 안이하게 지연 프로바이더로 만들면, 등록 자체의 시작 조건이 없어질 수 있습니다.

## 적용 대상과 데코레이션 순서 이해하기

일반적인 해석에서는 컨테이너가 원래 서비스를 생성하고, 등록 순서대로 extender의 반환값으로 교체한 뒤, 공유 인스턴스로 저장하고 해석 후 훅을 호출합니다.

```mermaid theme={null}
flowchart LR
    A["원래 생성 처리"] --> B["서비스"]
    B --> C["extender A<br>Wrapper A로 감싸기"]
    C --> D["extender B<br>Wrapper B로 감싸기"]
    D --> E["공유라면 저장"]
    E --> F["해석 후 훅·이용 측"]
```

A, B 순서로 등록하면 결과는 `B(A(원래 서비스))`입니다. 이용 측의 메서드 호출은 바깥쪽 B부터 시작하므로, 등록 순서와 처리 시작 순서를 혼동하지 마세요. 여러 패키지의 데코레이션 순서에 의존하는 경우에는 이용 애플리케이션에서 순서를 명시하고 조합을 테스트합니다.

extender는 컨테이너가 alias를 정규화한 **해석 키**에 대해 적용됩니다. `MessageFormatter::class`에 대한 등록이, `PlainMessageFormatter::class`를 직접 해석한 경우에 타입이 일치한다는 이유만으로 적용되지는 않습니다. 인터페이스를 통한 이용으로 통일합니다.

## 저장된 인스턴스에 대한 데코레이션은 유지 방식이 다르다

`v13.40.0`의 `Container::extend()`는 대상 인스턴스가 컨테이너 안에 저장되어 있는지 여부로 분기합니다.

| 등록 시의 상태 | `extend()`의 동작 | 이후 새로 생성할 때의 유지 |
| - | - | - |
| 아직 해석하지 않았고 저장된 인스턴스도 없다 | extender 배열에 추가한다 | 이후의 일반 생성 시에 적용한다 |
| 일반 `bind()`를 과거에 해석했지만 저장된 인스턴스는 없다 | extender를 추가하고 rebound 알림을 시도한다 | 이후의 일반 생성 시에 적용한다 |
| singleton이나 `instance()`의 객체가 저장되어 있다 | 그 자리에서 데코레이트하고, 저장값을 반환값으로 교체한 뒤 rebound 알림을 보낸다 | **이번 extender는 배열에 추가되지 않는다** |

마지막 행이 중요합니다. 해석된 singleton을 데코레이트한 뒤 `forgetInstance()`하거나 같은 키를 다시 바인딩해도, **그 자리에서만 적용된 데코레이션**은 다음 생성 시에 다시 적용되지 않습니다. 첫 해석 전에 extender 배열에 등록한 경우와는 다릅니다.

`scoped()`도 저장된 인스턴스를 사용하므로, 스코프 도중에 늦게 등록한 데코레이션을 다음 스코프에도 남는 등록이라고 생각하지 마세요. [패키지 서비스의 라이프사이클 설계](https://kawax.biz/ko/advanced/package-service-lifetimes.md)와 함께 확인합니다.

또한 나중에 `instance()`로 객체를 직접 저장하면, 일반 생성 시에 extender를 적용하는 루프를 거치지 않습니다. 테스트에서의 교체도 포함해, "등록되어 있으니 전달한 실체도 자동으로 데코레이트된다"고 할 수는 없습니다.

### 이미 배포된 참조는 자동으로 교체되지 않는다

이 예처럼 새 래퍼를 반환하는 경우, 컨테이너의 저장값이 바뀌어도 다른 서비스의 프로퍼티에 이미 저장된 원래 객체는 바뀌지 않습니다. `rebinding()` 알림은 등록된 콜백에 새 서비스를 전달하는 구조이며, 모든 의존 대상을 찾아 다시 쓰는 것이 아닙니다.

또한 `rebinding()`에 의한 알림 콜백 등록도 항상 지연으로 끝나는 것은 아닙니다. 대상이 바인딩되어 있으면 등록 시에도 `make()`한 결과를 반환합니다. 과거에 해석한 일반 바인딩에 `extend()`하는 경로에서도, rebound 콜백이 있으면 알림을 위해 `make()`될 수 있습니다. 초기화나 부작용의 타이밍을 일률적으로 "다음 이용 시"라고 단정하지 마세요.

## 이중 데코레이션과 상태 이월 피하기

`extend()`는 같은 클로저나 같은 데코레이터의 등록을 자동으로 중복 제거하지 않습니다. 이 프로바이더의 `register()`를 같은 컨테이너에 두 번 적용하면, 접두사도 두 번 붙을 수 있습니다.

* 데코레이션 등록은 프로바이더의 정해진 시작 경로에 모으고, 요청이나 잡마다 반복하지 않는다.
* 중복 방지를 위해 모든 컨테이너가 공유하는 단일 정적 플래그를 사용하지 않는다. 테스트나 새 애플리케이션에서 필요한 등록이 빠진다.
* singleton의 데코레이션도 같은 공유 기간 동안 유지된다. 현재 사용자나 요청 등을 래퍼에 고정하지 않는다.
* `forgetExtenders()`는 등록 배열을 지울 뿐이며, 생성된 래퍼를 원래대로 되돌리는 작업이 아니다.

이 예에서는 출력 문자열을 변경하고 있습니다. 접두사·위임 횟수·예외 처리도 패키지의 공개 동작이 되므로, 단순히 "구현의 내부 변경"으로 취급하지 말고 호환성을 확인합니다.

## 같은 컨테이너로 등록 시기 테스트하기

다음 PHPUnit 예시는 위 클래스들을 패키지의 autoload로 불러올 수 있다는 전제입니다. 프로바이더의 `register()`를 Container에 직접 적용하고, HTTP나 외부 서비스를 사용하지 않고 데코레이션의 유지 범위를 확인합니다. 자동 감지나 애플리케이션 전체의 시작은 별도로 [Orchestra Testbench로 Laravel 패키지를 테스트하기](https://kawax.biz/ko/advanced/package-testing.md)에서 검증하세요.

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

namespace Acme\Messages\Tests;

use Acme\Messages\MessageFormatter;
use Acme\Messages\PlainMessageFormatter;
use Acme\Messages\PrefixedMessageFormatter;
use Acme\Messages\PrefixServiceProvider;
use Illuminate\Container\Container;
use PHPUnit\Framework\TestCase;

final class ServiceDecorationTest extends TestCase
{
    public function test_extension_is_lazy_and_can_precede_binding(): void
    {
        $container = new Container;
        $builds = 0;

        (new PrefixServiceProvider($container))->register();
        $container->bind(MessageFormatter::class, function () use (&$builds) {
            $builds++;

            return new PlainMessageFormatter;
        });

        $this->assertSame(0, $builds);

        $first = $container->make(MessageFormatter::class);
        $second = $container->make(MessageFormatter::class);

        $this->assertSame('[Acme] ready', $first->format('ready'));
        $this->assertSame('[Acme] ready', $second->format('ready'));
        $this->assertNotSame($first, $second);
        $this->assertSame(2, $builds);
    }

    public function test_early_extension_survives_singleton_recreation(): void
    {
        $container = new Container;
        $container->singleton(MessageFormatter::class, PlainMessageFormatter::class);
        (new PrefixServiceProvider($container))->register();

        $first = $container->make(MessageFormatter::class);

        $this->assertInstanceOf(PrefixedMessageFormatter::class, $first);
        $this->assertSame($first, $container->make(MessageFormatter::class));

        $container->forgetInstance(MessageFormatter::class);
        $second = $container->make(MessageFormatter::class);

        $this->assertNotSame($first, $second);
        $this->assertSame('[Acme] ready', $second->format('ready'));
    }

    public function test_late_singleton_extension_is_not_retained_as_a_callback(): void
    {
        $container = new Container;
        $container->singleton(MessageFormatter::class, PlainMessageFormatter::class);
        $original = $container->make(MessageFormatter::class);

        (new PrefixServiceProvider($container))->register();
        $decorated = $container->make(MessageFormatter::class);

        $this->assertInstanceOf(PrefixedMessageFormatter::class, $decorated);
        $this->assertNotSame($original, $decorated);
        $this->assertSame('ready', $original->format('ready'));
        $this->assertSame('[Acme] ready', $decorated->format('ready'));

        $container->forgetInstance(MessageFormatter::class);
        $recreated = $container->make(MessageFormatter::class);

        $this->assertInstanceOf(PlainMessageFormatter::class, $recreated);
        $this->assertSame('ready', $recreated->format('ready'));
    }
}
```

마지막 테스트는 늦은 등록을 권장하는 것이 아니라, 확인한 버전의 경계를 명시하는 것입니다. `forgetInstance()`만을 데코레이션의 해제·재등록 API로 설계하지 마세요.

릴리스 전에는 여러 extender의 순서, 중복 등록, `instance()`에 의한 교체, 이용 측이 이미 보유한 참조도 확인합니다. 지원 Laravel 버전을 늘릴 때는 해석 키와 타입 계약에 더해, `extend()`의 분기와 공식 테스트를 다시 읽습니다.

## 관련 페이지

> [card] [패키지의 서비스 해석 후 훅](https://kawax.biz/ko/advanced/package-service-resolution.md)
>
> 객체를 교체하지 않고 설정하는 callAfterResolving()과 적용 횟수의 주의점을 확인합니다.

> [card] [패키지 서비스의 라이프사이클 설계](https://kawax.biz/ko/advanced/package-service-lifetimes.md)
>
> bind·singleton·scoped와, 생성된 서비스가 보유하는 의존 관계를 확인합니다.

## 참조한 1차 정보

* [Laravel 공식 문서: Extending bindings·Rebinding](https://github.com/laravel/docs/blob/13.x/container.md#extending-bindings)
* [Laravel 공식 문서: Service Providers](https://github.com/laravel/docs/blob/13.x/providers.md)
* [Laravel 공식 문서: 패키지 개발과 자동 감지](https://github.com/laravel/docs/blob/13.x/packages.md)
* [Container: extend()·resolve()·rebound()·instance()·forgetExtenders()](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Container/Container.php)
* [공식 테스트: 지연 생성·바인딩 전 등록·alias·여러 extender](https://github.com/laravel/framework/blob/v13.40.0/tests/Container/ContainerExtendTest.php)
* [ServiceProvider: 생성자와 register()](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Support/ServiceProvider.php)


## Related topics

- [패키지의 서비스 해석 후 훅](https://kawax.biz/ko/advanced/package-service-resolution.md)
- [고급 주제](https://kawax.biz/ko/advanced/index.md)
- [패키지의 데이터베이스 연결 확장](https://kawax.biz/ko/advanced/package-database-connections.md)
- [패키지 마이그레이션의 배포와 업데이트](https://kawax.biz/ko/advanced/package-migrations.md)
- [Laravel 패키지 개발](https://kawax.biz/ko/advanced/package-development.md)


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