> ## 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의 알림 채널을 패키지에서 제공하는 방법과 클래스 이름 기반 해석, 수신처 라우팅, 큐 처리, 실제 전송 테스트를 설명합니다.

패키지에서 알림 전송 방법을 추가하고 싶다면 **채널 선택**과 **수신처 결정**을 분리해서 설계합니다. 이 페이지에서는 운영 이벤트를 로그에 기록하는 작은 채널을 예로 들어, 패키지에서 제공하는 클래스가 해석되는 경로와 업데이트 시 필요한 검증을 설명합니다. 외부 서비스로 전송하도록 바꾸는 경우에도 채널과 알림의 책임 분담은 동일합니다.

이 페이지는 Laravel Framework `v13.38.0`의 구현을 확인한 내용입니다. 알림의 기본 사용법은 [알림](/ko/notifications.md)을, 패키지 테스트 환경은 [패키지 테스트](/ko/advanced/package-testing.md)를 참조하세요.

## 클래스 이름은 전송 방법, 라우트는 수신처

`via()`에서 반환하는 채널 클래스 이름은 어떤 객체의 `send()`를 호출할지 지정합니다. 반면 `routeNotificationFor()`에서 얻는 값은 해당 채널이 사용하는 수신처입니다. 두 값의 이름을 같게 맞출 필요는 없습니다.

| 지정 위치 | 이 페이지의 예 | 역할 |
| - | - | - |
| 알림의 `via()` | `OperationsLogChannel::class` | 채널 해석과 전송 |
| 알림 가능 객체의 라우트 | `operationsLog` → `operations` | 채널 내부에서 사용하는 수신처 |
| 알림의 `viaQueues()` | `OperationsLogChannel::class` → `operations-log` | 큐에 넣을 때 잡이 향하는 곳 |

`NotificationSender`는 `via()`가 선택한 채널 이름을 `ChannelManager::driver()`에 전달합니다. `ChannelManager`는 내장 이름이나 등록된 이름에서 찾지 못했고 그 이름이 실제로 존재하는 클래스라면, 컨테이너로 해당 클래스를 해석합니다. **클래스 이름으로만 사용한다면 `Notification::extend()`나 패키지 서비스 프로바이더는 필요 없습니다**. Composer 오토로드로 클래스를 찾을 수 있고, 생성자의 의존성을 컨테이너가 해석할 수 있도록 하면 됩니다.

```mermaid theme={null}
flowchart LR
    A["via()로 클래스 이름 선택"] --> B["ChannelManager::driver()"]
    B --> C{"이미 해석됨?"}
    C -->|예| D["보관된 채널"]
    C -->|아니요| E["컨테이너로 생성"]
    E --> D
    D --> F["send()에서 수신처를 얻어 전송"]
```

## 채널 본체를 패키지에서 제공하기

예제의 `Acme\OperationsLog` 패키지는 민감한 정보를 포함하지 않는 이벤트 이름을 수신처인 운영 팀 이름과 함께 기록합니다. Laravel 애플리케이션에서는 `Psr\Log\LoggerInterface`가 로그 서비스에 바인딩되어 있으므로 생성자로 주입받을 수 있습니다.

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

namespace Acme\OperationsLog;

use Illuminate\Notifications\Notification;
use InvalidArgumentException;
use Psr\Log\LoggerInterface;

final class OperationsLogChannel
{
    public function __construct(private readonly LoggerInterface $logger)
    {
    }

    public function send(object $notifiable, Notification $notification): void
    {
        $destination = $notifiable->routeNotificationFor('operationsLog', $notification);

        if (! is_string($destination) || $destination === '') {
            throw new InvalidArgumentException('Operations log destination is required.');
        }

        $this->logger->info('Operations notification', [
            'destination' => $destination,
            'event' => $notification->toOperationsLog($notifiable),
        ]);
    }
}
```

전달받는 객체는 `Notifiable` 트레이트를 사용하는 모델이거나 `Notification::route()`가 만드는 온디맨드 수신처라고 가정합니다. 전자에서는 `routeNotificationForOperationsLog()`의 값이, 후자에서는 `route('operationsLog', ...)`로 등록한 값이 반환됩니다. 수신처가 없는데도 전송 완료로 처리되지 않도록 이 예제에서는 예외를 던집니다. 실제 외부 전송으로 바꾸는 경우에도 수신처와 전송 결과를 확인하고, 실패를 성공으로 묻어 버리지 않도록 설계하세요.

> [!WARNING]
>
> 로그에 기록하는 이벤트 이름이나 수신처에 액세스 토큰이나 개인정보를 포함하지 마세요. 이 예제는 채널 해석과 테스트를 보여 주기 위한 로그 전송이며, 외부 서비스로의 전송 구현이 아닙니다.

## 사용하는 쪽에서 알림과 수신처 정의하기

애플리케이션의 알림은 채널 클래스를 반환하고, 채널에 전달할 메시지만 정의합니다. `ShouldQueue`와 `Queueable`을 사용하면 일반적인 `notify()` 호출이 큐를 거치게 됩니다.

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

namespace App\Notifications;

use Acme\OperationsLog\OperationsLogChannel;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification;

class HealthCheckPassed extends Notification implements ShouldQueue
{
    use Queueable;

    public function via(object $notifiable): array
    {
        return [OperationsLogChannel::class];
    }

    public function toOperationsLog(object $notifiable): string
    {
        return 'health-check-passed';
    }

    public function viaQueues(): array
    {
        return [OperationsLogChannel::class => 'operations-log'];
    }
}
```

수신처를 그 자리에서 지정하는 경우, `Notification::route()`의 키는 **채널 클래스 이름이 아니라** 채널 내부에서 호출하는 `routeNotificationFor('operationsLog', ...)`의 키입니다.

```php theme={null}
use App\Notifications\HealthCheckPassed;
use Illuminate\Support\Facades\Notification;

Notification::route('operationsLog', 'operations')
    ->notify(new HealthCheckPassed());
```

애플리케이션의 모델에서 보내는 경우에는 `Notifiable`을 사용하는 모델에 다음 메서드를 준비합니다.

```php theme={null}
use Illuminate\Notifications\Notification;

public function routeNotificationForOperationsLog(Notification $notification): string
{
    return 'operations';
}
```

이 모델에 대해 `$user->notify(new HealthCheckPassed())`를 호출해도 같은 채널이 선택됩니다. 큐 연결을 설정하고 `operations-log` 큐를 처리하는 워커를 실행하세요.

```shell theme={null}
php artisan queue:work --queue=operations-log
```

`OperationsLogChannel` 자체는 채널별로 Manager에 보관되므로, **수신처나 알림 본문을 채널의 프로퍼티에 저장하지 말고** 매번 `send()`의 인수에서 꺼내세요. 오래 실행되는 큐 워커에서도 이전 알림의 정보가 남지 않도록 하기 위해서입니다.

## 짧은 별칭이 필요한 경우

사용자가 `via()`에서 클래스 이름 대신 짧은 이름을 지정하게 하고 싶을 때만, 패키지의 일반 서비스 프로바이더에서 `Notification::extend()`를 사용합니다.

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

namespace Acme\OperationsLog;

use Illuminate\Support\Facades\Notification;
use Illuminate\Support\ServiceProvider;

class OperationsLogServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Notification::extend(
            'acme-operations-log',
            fn ($app) => $app->make(OperationsLogChannel::class),
        );
    }
}
```

프로바이더는 [패키지 자동 감지](/ko/advanced/package-discovery.md) 등을 통해 전송 처리가 시작되기 전에 로드되도록 합니다. 별칭을 사용하는 알림에서는 `via()`를 `['acme-operations-log']`로, `viaQueues()`의 키도 같은 이름으로 변경합니다. **수신처 조회용 `operationsLog`는 별개의 키**이므로 변경할 필요가 없습니다. 내장 채널 이름과 충돌하지 않는 이름을 사용하세요.

Manager는 해석한 채널을 이름별로 보관합니다. 같은 프로세스에서 별칭 생성 처리를 다시 등록해도 이미 생성된 인스턴스는 교체되지 않습니다. 등록 방법을 변경했다면 새로 시작한 애플리케이션이나 워커에서도 검증하세요.

## fake와 별도로 전송 처리 테스트하기

`Notification::fake()`는 알림을 보내려는 **의도**를 검증하기 위한 것입니다. 채널의 `send()`나 라우트 조회가 동작하는지는 확인할 수 없습니다. 전송 테스트에서는 실제 알림 Manager를 사용하고, `LoggerInterface`를 테스트용으로 교체합니다.

다음은 [Testbench의 `TestCase`](/ko/advanced/package-testing)에서 실행하는 예입니다. `notifyNow()`를 사용하면 `ShouldQueue` 알림이라도 워커를 실행하지 않고 채널의 처리를 실행할 수 있습니다.

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

namespace Acme\OperationsLog\Tests;

use Acme\OperationsLog\OperationsLogChannel;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification as BaseNotification;
use Illuminate\Support\Facades\Notification;
use Orchestra\Testbench\TestCase;
use Psr\Log\LoggerInterface;

class OperationsLogChannelTest extends TestCase
{
    public function test_channel_writes_the_routed_event(): void
    {
        $notification = new class extends BaseNotification implements ShouldQueue {
            use Queueable;

            public function via(object $notifiable): array
            {
                return [OperationsLogChannel::class];
            }

            public function toOperationsLog(object $notifiable): string
            {
                return 'health-check-passed';
            }
        };

        $logger = $this->createMock(LoggerInterface::class);
        $logger->expects($this->once())->method('info')->with(
            'Operations notification',
            ['destination' => 'operations', 'event' => 'health-check-passed'],
        );

        $this->app->instance(LoggerInterface::class, $logger);

        Notification::route('operationsLog', 'operations')->notifyNow($notification);
    }
}
```

추가로 수신처가 없을 때 예외가 발생하는지, 별칭을 제공하는 경우 새로 시작한 상태에서 해석되는지도 확인합니다. 큐를 거쳐 사용하는 경우에는 별도로 큐 워커를 실행하는 검증용 애플리케이션에서 `operations-log` 큐에 잡이 도착하고 처리되는지도 테스트합니다.

`NotificationSender`는 `ShouldQueue` 알림을 채널별 잡으로 나누고, `viaQueues()`의 키로 큐를 선택합니다. 채널의 `send()`가 예외를 던지면 `NotificationFailed`를 발생시킨 뒤 예외를 다시 던지고, 정상적으로 반환되면 `NotificationSent`를 발생시킵니다. 큐의 재시도 설정은 애플리케이션 설정을 따릅니다. 채널 내부에서 예외를 묻어 버리면 전송에 실패해도 성공으로 처리된다는 점에 주의하세요.

## 참조한 1차 자료

* [Laravel 13 공식 문서: 커스텀 알림 채널과 큐](https://github.com/laravel/docs/blob/13.x/notifications.md#custom-channels)
* [Laravel 13 공식 문서: 패키지 자동 감지](https://github.com/laravel/docs/blob/13.x/packages.md#package-discovery)
* [ChannelManager: 클래스 이름으로부터의 해석](https://github.com/laravel/framework/blob/v13.38.0/src/Illuminate/Notifications/ChannelManager.php)
* [NotificationSender: 채널별 전송, 큐, 이벤트](https://github.com/laravel/framework/blob/v13.38.0/src/Illuminate/Notifications/NotificationSender.php)
* [RoutesNotifications와 AnonymousNotifiable: 수신처 라우트](https://github.com/laravel/framework/blob/v13.38.0/src/Illuminate/Notifications/RoutesNotifications.php)
* [Manager: 드라이버 보관과 확장](https://github.com/laravel/framework/blob/v13.38.0/src/Illuminate/Support/Manager.php)
* [Application: LoggerInterface 바인딩](https://github.com/laravel/framework/blob/v13.38.0/src/Illuminate/Foundation/Application.php)


## Related topics

- [알림 채널 - Laravel Bluesky](/ko/packages/laravel-bluesky/notification.md)
- [패키지의 서비스 해석 후 훅](/ko/advanced/package-service-resolution.md)
- [패키지의 Blade 확장과 컴파일 캐시](/ko/advanced/package-blade-extensions.md)
- [알림 채널 - LINE SDK for Laravel](/ko/packages/laravel-line-sdk/notification.md)
- [코어 패키지와 커스텀 드라이버 - Feedable](/ko/packages/feedable/core.md)


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