> ## 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의 custom·via와 LogManager::extend()를 구분해 사용하고, 채널 보관, forgetChannel(), 폴백을 고려해 로그 기능을 테스트합니다.

패키지 고유의 로그 출력을 제공하고 싶을 때는 **로거를 만드는 구조**와 **사용하는 애플리케이션이 선택하는 채널 설정**을 분리합니다. 드라이버를 등록하는 것만으로 기존 채널이 다시 만들어지는 것은 아니며, 설정을 변경한 후에도 이전 Handler가 사용되는 경우가 있습니다.

이 페이지는 Laravel Framework `v13.39.0`의 구현을 확인한 내용입니다. 기본 조작은 [로깅](https://kawax.biz/ko/logging.md)을, 프로바이더 준비는 [패키지 개발](https://kawax.biz/ko/advanced/package-development.md)을 참조하세요.

## 필요한 확장 포인트 선택하기

기존 드라이버로 구현할 수 있다면 독자적인 드라이버를 늘릴 필요는 없습니다.

| 하고 싶은 것 | 선택지 | 확장하는 곳 |
| - | - | - |
| 기존 채널의 포매터 등을 조정한다 | `tap` | 생성된 `Illuminate\Log\Logger` |
| Monolog의 Handler를 설정으로 선택한다 | `driver: monolog` | `handler`, `handler_with` 등의 설정 |
| 패키지의 팩토리로 로거를 구성한다 | `driver: custom`과 `via` | `__invoke(array $config)`를 가진 클래스 |
| 패키지 고유의 드라이버 이름을 공개한다 | `LogManager::extend()` | 애플리케이션과 설정 배열을 받는 클로저 |

`via` 클래스와 `tap` 클래스는 컨테이너에서 해석되므로 생성자 주입도 사용할 수 있습니다. `tap`은 채널 생성 시의 조정이며, 로그를 1건 기록할 때마다 실행되는 훅이 아닙니다.

> [!NOTE]
>
> `logging.channels.audit`의 `audit`은 **채널 이름**입니다. 그 안의 `driver`는 **생성 방법의 이름**입니다. `Log::channel('audit')`로 선택하는 것은 채널 이름이므로, 나중에 드라이버를 변경해도 호출하는 쪽의 이름을 유지할 수 있습니다.

## 먼저 custom과 via로 팩토리를 공개하기

가상의 `Acme\Audit` 패키지에서 파일 출력용 팩토리를 제공합니다. PSR-4로 로드할 수 있는 `src/Logging/CreateAuditLogger.php`에 배치합니다.

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

namespace Acme\Audit\Logging;

use Monolog\Handler\StreamHandler;
use Monolog\Logger;

final class CreateAuditLogger
{
    public function __invoke(array $config): Logger
    {
        return new Logger(
            $config['name'] ?? 'acme-audit',
            [new StreamHandler($config['path'], $config['level'] ?? 'info')],
        );
    }
}
```

사용하는 애플리케이션에서는 `config/logging.php`의 기존 `channels` 배열에 다음 요소를 추가합니다. 기존 `default`나 `stack` 설정은 그대로 둡니다.

```php theme={null}
'audit' => [
    'driver' => 'custom',
    'via' => \Acme\Audit\Logging\CreateAuditLogger::class,
    'path' => storage_path('logs/audit.log'),
    'level' => 'info',
],
```

`LogManager::createCustomDriver()`는 `via` 클래스를 컨테이너에서 가져와 채널의 설정 배열을 유일한 인수로 호출합니다. 이 방식만 사용한다면 드라이버 등록용 프로바이더는 필요 없습니다.

```php theme={null}
use Illuminate\Support\Facades\Log;

Log::channel('audit')->info('주문을 확정했습니다.', ['order_id' => 42]);
```

이 팩토리에서는 `level`을 StreamHandler에 명시적으로 전달합니다. **`custom` 팩토리가 반환하는 로거에 대해 LogManager가 설정의 `level`을 일률적으로 적용하는 것은 아닙니다**. 독자적인 설정 항목을 추가한 경우에도 팩토리 쪽에서 반영합니다.

> [!TIP]
>
> 단순히 파일의 출력 위치만 바꾸는 것이라면 `single` 드라이버로 충분합니다. 이 예는 패키지 고유의 Handler나 초기화를 포함하기 위한 최소 구성입니다. `via`는 설정 캐시에 저장할 수 있는 클래스 이름으로 하고, 설정 배열에 클로저나 로거 인스턴스를 저장하지 마세요.

## 독자적인 드라이버 이름을 제공할 때는 extend를 사용하기

사용하는 쪽에 `driver: acme-audit`라는 설정을 공개하고 싶다면, 같은 팩토리를 사용하는 드라이버를 등록합니다. `src/AuditServiceProvider.php`의 `boot()`에서 Laravel의 `log` 서비스를 확장합니다.

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

namespace Acme\Audit;

use Acme\Audit\Logging\CreateAuditLogger;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Log\LogManager;
use Illuminate\Support\ServiceProvider;
use Monolog\Logger as Monolog;

class AuditServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        $this->callAfterResolving('log', static function (LogManager $manager): void {
            $manager->extend('acme-audit', static function (Application $app, array $config): Monolog {
                return $app->make(CreateAuditLogger::class)($config);
            });
        });
    }
}
```

프로바이더는 [패키지 자동 검색](https://kawax.biz/ko/advanced/package-discovery.md) 등으로 로드합니다. 사용하는 쪽의 `channels.audit`은 앞의 설정에서 다음 설정으로 교체합니다. `via`는 더 이상 필요하지 않습니다.

```php theme={null}
'audit' => [
    'driver' => 'acme-audit',
    'path' => storage_path('logs/audit.log'),
    'level' => 'info',
],
```

* Laravel의 `LogServiceProvider`는 `log`를 singleton으로 등록합니다. `callAfterResolving()`은 아직 해석되지 않았다면 기다리고, 이미 해석되었다면 그 인스턴스에도 등록을 반영합니다. 자세한 내용은 [서비스 해석 후 훅](https://kawax.biz/ko/advanced/package-service-resolution.md)을 참조하세요.
* `extend()`는 생성용 클로저를 등록할 뿐입니다. 채널 생성이나 로그 기록은 이 콜백을 등록하는 시점에는 수행하지 않습니다.
* `callCustomCreator()`는 클로저에 `$app`과 채널 설정 배열을 전달합니다. `via` 팩토리에 전달되는 인수와는 다릅니다.
* 같은 드라이버 이름을 다시 등록하면 생성용 클로저를 덮어씁니다. `single` 등의 내장 이름이나 다른 패키지와 충돌하는 이름은 사용하지 마세요.

> [!WARNING]
>
> 확인한 구현에서는 `extend()`에 전달한 익명 클로저가 LogManager의 스코프로 다시 바인딩됩니다. 비static 클로저의 `$this`는 서비스 프로바이더가 아닙니다. 예와 같이 static 클로저에서 인수 `$app`을 사용하고, 프로바이더의 `$this`에 의존하지 않는 구현으로 합니다.

패키지는 드라이버를 제공하고, 어느 채널이나 stack에서 사용할지는 사용하는 쪽이 선택합니다. 프로바이더에서 `logging.default`나 기존 `logging.channels` 전체를 덮어쓰지 않는 설계로 하세요.

## 채널은 이름별로 보관된다

`LogManager::get()`은 해석한 채널을 `$channels`에 저장합니다. 같은 이름의 채널을 가져오면 설정 배열을 다시 읽지 않고 보관된 객체를 반환합니다.

```mermaid theme={null}
flowchart TD
    A["Log::channel('audit')"] --> B{"audit은 해석 완료?"}
    B -->|Yes| C["보관된 Logger를 반환"]
    B -->|No| D["channels.audit의 driver를 선택"]
    D --> E["via 또는 extend의 팩토리"]
    E --> F["Illuminate Logger로 래핑<br>tap과 공유 컨텍스트를 적용"]
    F --> G["audit 이름으로 보관"]
```

이는 파일에 저장하는 설정 캐시와는 별개인, **현재 LogManager 안의 객체 보관**입니다. `config()` 변경이나 `extend()` 재등록만으로는 이미 생성된 Handler가 교체되지 않습니다.

```php theme={null}
use Illuminate\Support\Facades\Log;

$before = Log::channel('audit');

config(['logging.channels.audit.level' => 'error']);

$same = Log::channel('audit');

Log::forgetChannel('audit');

$after = Log::channel('audit');
```

이 예의 `$same`은 `$before`와 같은 객체입니다. `forgetChannel('audit')`가 Manager의 보관을 해제하면, 다음 가져오기에서 새로운 설정으로 `$after`가 생성됩니다.

### forgetChannel은 기존 참조를 갱신하지 않는다

`forgetChannel()`이 수행하는 것은 Manager 안의 엔트리 삭제입니다.

* `$before`를 보관하고 있는 서비스는 그 이후에도 이전 로거를 사용할 수 있습니다. 기존 참조를 새로운 객체로 교체하는 조작이 아닙니다.
* `Log::channel('stack')` 등으로 이미 생성된 stack은 자식 채널의 Handler와 Processor를 모은 독립된 Monolog를 가집니다. 자식만 `forgetChannel()`해도 기존 stack의 구성은 바뀌지 않습니다. 필요하다면 그 stack의 채널 이름에 대해서도 보관을 해제하고 다시 가져옵니다.
* `forgetChannel()` 자체는 Handler의 `close()`를 호출하지 않습니다. 연결이나 버퍼의 종료 처리까지 보장하는 API가 아닙니다.

일반적인 배포에서는 [설정 캐시 갱신](https://kawax.biz/ko/advanced/package-config-merging.md)과 사용 중인 장기 실행 워커의 재시작을 운영 절차에 포함합니다. 요청마다 설정을 덮어쓰고 다시 해석하는 구조에 의존하지 말고, [서비스의 라이프사이클](https://kawax.biz/ko/advanced/package-service-lifetimes.md)도 고려하세요.

### stack에 포함하려면 Monolog 호환 조작도 필요

독자적인 드라이버는 `Psr\Log\LoggerInterface` 구현을 반환할 수 있지만, Laravel의 `createStackDriver()`는 자식 채널에 대해 `getHandlers()`와 `getProcessors()`를 호출합니다. 이들은 PSR 인터페이스에 포함되지 않습니다.

임의의 PSR 로거를 반환한다고 해서 stack에서도 동작한다고는 할 수 없으므로, stack에 포함하는 것도 제공하는 패키지에서는 예와 같이 Monolog를 반환하거나 그 사용 범위를 명시하고 테스트합니다.

## 목뿐만 아니라 실제 채널 해석을 검증하기

`Log::shouldReceive('channel')`만으로는 프로바이더 등록, 팩토리 선택, 설정 반영을 확인할 수 없습니다. 다음은 [패키지 테스트 환경](https://kawax.biz/ko/advanced/package-testing.md)에서 실제 Manager와 팩토리를 거치는 예입니다.

테스트용 출력 위치에는 `php://memory`를 지정합니다. 여기서는 채널 구축과 Handler 설정을 검증하며, 로그 기록 자체는 수행하지 않습니다.

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

namespace Acme\Audit\Tests;

use Acme\Audit\AuditServiceProvider;
use Acme\Audit\Logging\CreateAuditLogger;
use Illuminate\Log\Logger;
use Illuminate\Support\Facades\Log;
use Monolog\Handler\StreamHandler;
use Monolog\Level;
use Monolog\Logger as Monolog;
use Orchestra\Testbench\TestCase;

class AuditLoggingTest extends TestCase
{
    protected function getPackageProviders($app): array
    {
        return [AuditServiceProvider::class];
    }

    protected function defineEnvironment($app): void
    {
        $app['config']->set('logging.channels.audit', [
            'driver' => 'acme-audit',
            'path' => 'php://memory',
            'level' => 'warning',
        ]);

        $app['config']->set('logging.channels.emergency.path', 'php://memory');
    }

    public function test_the_package_driver_creates_the_configured_logger(): void
    {
        $this->assertAuditChannel(Log::channel('audit'), Level::Warning);
    }

    public function test_configuration_changes_require_a_new_channel(): void
    {
        $before = Log::channel('audit');

        config(['logging.channels.audit.level' => 'error']);

        $this->assertSame($before, Log::channel('audit'));
        $this->assertAuditChannel($before, Level::Warning);

        Log::forgetChannel('audit');

        $after = Log::channel('audit');

        $this->assertNotSame($before, $after);
        $this->assertAuditChannel($after, Level::Error);
        $this->assertAuditChannel($before, Level::Warning);
    }

    public function test_the_same_factory_can_be_used_with_via(): void
    {
        config([
            'logging.channels.audit.driver' => 'custom',
            'logging.channels.audit.via' => CreateAuditLogger::class,
        ]);

        Log::forgetChannel('audit');

        $this->assertAuditChannel(Log::channel('audit'), Level::Warning);
    }

    private function assertAuditChannel(Logger $channel, Level $level): void
    {
        $logger = $channel->getLogger();

        $this->assertInstanceOf(Monolog::class, $logger);
        $this->assertSame('acme-audit', $logger->getName());
        $this->assertCount(1, $logger->getHandlers());

        $handler = $logger->getHandlers()[0];

        $this->assertInstanceOf(StreamHandler::class, $handler);
        $this->assertSame($level, $handler->getLevel());
    }
}
```

> [!WARNING]
>
> `LogManager::get()`은 채널 생성 중에 발생한 예외를 포착하고 emergency logger를 반환하는 경우가 있습니다. "예외가 발생하지 않았다", "LoggerInterface였다"는 확인만으로는 목적한 채널이 구축되었다는 증거가 되지 않습니다. 위의 테스트에서는 Monolog의 이름, Handler, 로그 레벨도 확인해 폴백을 성공으로 오인하지 않도록 하고 있습니다. 생성 후의 기록 오류까지 같은 구조로 반드시 포착되는 것은 아닙니다.

## 장기 유지보수에서 추가하는 검증

| 변경이나 사용 방법 | 확인할 것 |
| - | - |
| `tap` 추가 | 실제 팩토리가 반환하는 로거에 대해 기대하는 조정이 적용된다 |
| stack에 포함 | 자식 채널의 Handler와 Processor가 포함되고, 재해석 시의 구성도 올바르다 |
| 출력 위치나 포맷 변경 | 실제 기록 내용, 권한, 로테이션 등을 별도의 검증 환경에서 확인한다 |
| 등록 시점 변경 | `log`가 이미 해석된 상태여도 드라이버가 등록되고 채널을 생성할 수 있다 |
| Laravel 업데이트 | 콜백의 인수와 스코프, 보관 해제, emergency logger로의 폴백을 계속 확인한다 |

## 참조한 1차 정보

* [Laravel 공식 문서: Logging](https://github.com/laravel/docs/blob/13.x/logging.md)
* [Framework v13.39.0: LogManager의 생성·확장·보관 해제](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Log/LogManager.php)
* [Framework v13.39.0: Logger의 래핑과 메서드 전달](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Log/Logger.php)
* [Framework v13.39.0: LogServiceProvider의 singleton 등록](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Log/LogServiceProvider.php)
* [Framework v13.39.0: ServiceProvider::callAfterResolving](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Support/ServiceProvider.php)
* [Framework v13.39.0: RebindsCallbacksToSelf](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Support/RebindsCallbacksToSelf.php)
* [Laravel 공식 테스트: LogManagerTest](https://github.com/laravel/framework/blob/v13.39.0/tests/Integration/Log/LogManagerTest.php)


## Related topics

- [고급 주제](https://kawax.biz/ko/advanced/index.md)
- [패키지의 Storage 드라이버 등록과 재해석](https://kawax.biz/ko/advanced/package-filesystem-drivers.md)
- [패키지의 Blade 확장과 컴파일 캐시](https://kawax.biz/ko/advanced/package-blade-extensions.md)
- [로깅](https://kawax.biz/ko/logging.md)
- [패키지의 커스텀 알림 채널과 전송 검증](https://kawax.biz/ko/advanced/package-notification-channels.md)


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