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

# 패키지의 Artisan 명령어 등록과 의존성 해결

> Laravel 13의 commands()에서 Artisan 실행까지의 흐름을 읽어 보고, 명령어의 생성과 실행을 분리하는 설계, 의존성 주입, 지연 프로바이더와 장기 유지보수 시 주의할 점을 해설합니다.

패키지에 진단·동기화·설치용 Artisan 명령어를 추가하려면 명령어 등록, 인스턴스 생성, 처리 실행을 나누어 생각해야 합니다. 실행 시에만 필요한 서비스를 생성자에서 해결하면 관계없는 Artisan 명령어의 실행까지 영향을 받을 수 있습니다.

이 페이지에서는 Laravel Framework `v13.35.0`의 구현을 확인하고, [Laravel 패키지 개발](/ko/advanced/package-development)에 있는 `commands()` 등록 예제를 깊이 살펴봅니다. 파일 생성 구현은 [GeneratorCommand](/ko/advanced/generator-command)를, 최적화 작업에 참여하는 방법은 [optimize 통합](/ko/advanced/package-optimization)을 참조하세요.

## 프로바이더에서 명시적으로 등록하기

패키지 내의 명령어는 서비스 프로바이더의 `boot()`에서 클래스 이름을 등록합니다. 애플리케이션의 `app/Console/Commands`에 대한 자동 감지와 패키지가 제공하는 클래스의 등록은 서로 다른 진입점입니다.

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

namespace Acme\Courier;

use Acme\Courier\Console\StatusCommand;
use Illuminate\Support\ServiceProvider;

class CourierServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        if ($this->app->runningInConsole()) {
            $this->commands([
                StatusCommand::class,
            ]);
        }
    }
}
```

이 예제는 프로바이더가 [패키지 자동 감지](/ko/advanced/package-discovery) 또는 애플리케이션 측의 명시적 등록으로 로드된다는 것을 전제로 합니다. 명령어 클래스를 두는 것만으로 패키지의 프로바이더까지 자동으로 등록되는 것은 아닙니다.

`runningInConsole()`은 콘솔 환경인지를 판정할 뿐, 특정 명령어를 실행 중인지를 판정하는 것이 아닙니다. HTTP 요청 내에서 `Artisan::call()`하는 용도도 제공한다면 이 조건 때문에 등록되지 않을 가능성을 고려하고, 무조건 등록을 포함하여 사용 계약을 정합니다. 프로그램에서 실행하는 방법은 [Artisan](/ko/artisan)을 참조하세요.

## commands()는 그 자리에서 명령어를 실행하지 않는다

`ServiceProvider::commands()`는 전달받은 클래스 이름을 사용하는 클로저를 `Illuminate\Console\Application::starting()`에 등록합니다. 이 시점에서는 명령어의 `handle()`을 호출하지 않습니다.

일반적인 Console Kernel에서는 나중에 Artisan Application이 생성되면 등록된 시작 콜백이 실행됩니다. 그때 `resolveCommands()`가 각 클래스를 `resolve()`에 전달하여 명령어 등록을 진행합니다.

```mermaid theme={null}
flowchart TD
    A["프로바이더의 boot()"] --> B["commands()로 시작 콜백 등록"]
    B --> C["Console Kernel이 Artisan Application 생성"]
    C --> D["resolveCommands()로 클래스 등록"]
    D --> E["일반 클래스는 컨테이너로 인스턴스화"]
    E --> F["선택된 명령어 실행"]
    F --> G["handle()의 의존성을 해결하여 호출"]
```

여기서 중요한 점은 **명령어의 인스턴스 생성이 그 명령어의 실행과 같은 시점이라고는 할 수 없다**는 것입니다. 뒤에서 설명할 전용 지연 등록 경로를 사용하지 않는 일반 클래스에서는 `resolve()`가 컨테이너의 `make()`를 호출하므로, 등록 시점에 생성자와 그 의존성이 해결됩니다.

따라서 `php artisan list`나 다른 명령어를 실행하기만 해도 패키지 명령어의 생성자가 동작할 수 있습니다. `commands()`를 `boot()`에 두는 것 자체가 실행 시점까지 인스턴스 생성을 늦춘다는 보장은 되지 않습니다.

## 실행 시의 의존성은 handle()에 주입하기

예로 패키지 설정의 활성·비활성 여부만 표시하는 명령어를 만듭니다. Laravel 13의 공식 Artisan 문서와 마찬가지로 이름과 설명을 `Signature`·`Description` 어트리뷰트로 정의합니다.

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

namespace Acme\Courier\Console;

use Illuminate\Console\Attributes\Description;
use Illuminate\Console\Attributes\Signature;
use Illuminate\Console\Command;
use Illuminate\Contracts\Config\Repository;

#[Signature('courier:status')]
#[Description('Display whether the Courier integration is enabled')]
class StatusCommand extends Command
{
    public function handle(Repository $config): int
    {
        $enabled = (bool) $config->get('courier.enabled', false);

        $this->line($enabled ? 'Courier: enabled' : 'Courier: disabled');

        return self::SUCCESS;
    }
}
```

이 예제에서는 생성자를 추가하지 않고, 설정 Repository를 `handle()`의 인수로 받습니다. 명령어 실행 경로에서 `Command::execute()`가 컨테이너의 `call()`을 사용하므로, 타입을 지정한 의존성이 주입됩니다.

외부 API 클라이언트나 동기화 서비스를 사용하는 경우에도, 실행 시에만 필요하다면 같은 위치에 주입할 수 있습니다. 다만 서비스 자체의 생성이나 다른 프로바이더의 `boot()`에 부작용이 있다면, 그것까지 자동으로 막아 주지는 않습니다.

| 위치 | 일반 명령어에서 평가되는 시점 | 적합한 처리 |
| - | - | - |
| 프로바이더의 `boot()` | 프로바이더 부팅 시 | 명령어 클래스 등록 |
| 명령어의 생성자 | 인스턴스 생성 시. 실행 직전이라고는 할 수 없음 | 가벼운 초기화 |
| `handle()`의 타입 지정 인수 | 명령어 실행 시 | 실행에 필요한 서비스 해결 |
| `handle()`의 본문 | 명령어 실행 시 | 입력 검증, 비즈니스 처리, 결과 표시 |

<Warning>
  생성자에서는 외부 API 연결, 데이터베이스 갱신, 공개 파일 덮어쓰기 등을 하지 마세요. 패키지 사용자는 설치 직후나 연결 설정이 완료되지 않은 상태에서도 `list`·`help`를 사용할 수 있어야 합니다.
</Warning>

생성자를 직접 정의하는 경우에는 `parent::__construct()`도 호출합니다. Laravel의 부모 생성자가 어트리뷰트, signature, description 등으로부터 명령어를 구성하기 때문입니다.

## Signature와 지연 등록은 별개의 메커니즘

확인한 `Application::resolve()`에는 Symfony의 `AsCommand` 어트리뷰트의 `name`을 읽어 이름과 클래스의 대응을 command map에 저장하는 분기가 있습니다. 이 분기에서는 그 자리에서 컨테이너의 `make()`를 호출하지 않습니다. Console Kernel은 나중에 `setContainerCommandLoader()`를 호출하여 이 map을 사용하는 로더를 설정합니다.

한편, 위 예제에서 사용한 Laravel의 `Signature` 어트리뷰트는 이 `AsCommand` 판정과는 별개입니다. **`Signature`를 붙이는 것만으로 그 클래스가 이 지연 등록 경로에 들어가는 것은 아닙니다.**

이 차이는 내부 구현을 읽을 때의 주의점으로 다루고, 우선은 공식 명령어 정의와 `handle()`에 대한 의존성 주입으로 생성 비용과 부작용을 줄입니다. 지연 로더가 있는 경우에도 목록·도움말 등에서 명령어 정보가 필요해질 수 있으므로, "선택한 명령어의 실행 시 외에는 절대 생성되지 않는다"는 전제로 설계하지 않습니다.

## 명령어 등록까지 지연시키지 않기

서비스 바인딩을 지연 로드하는 것과 Artisan에 명령어를 알리는 것은 별개입니다. 프로바이더가 `DeferrableProvider`를 구현하고 있으면, 대상 서비스가 필요해질 때까지 `boot()` 자체가 실행되지 않을 수 있습니다.

명령어 등록만 그 `boot()`에 두고 Artisan 실행 시점까지 프로바이더가 로드되지 않으면, 명령어의 존재를 알리는 시작 콜백도 등록되지 않습니다. `handle()`로 의존성 주입을 옮긴 서비스가 지연 프로바이더를 로드한다고 해도, Artisan이 명령어를 찾기 전의 문제는 해결되지 않습니다.

명령어 공개를 확실히 하는 구성으로서, 즉시 로드되는 프로바이더에 등록을 두고 무거운 서비스의 바인딩만 별도의 지연 프로바이더로 분리할 수 있습니다. 구체적인 요건은 [지연 서비스 프로바이더](/ko/advanced/deferred-provider)를 참조하세요.

## 업데이트 시 확인할 사항

명령어 이름, 인수, 옵션, 출력, 종료 코드는 사용자의 셸 스크립트나 CI에서 사용되는 공개 인터페이스입니다. 클래스 내부 변경과 같은 감각으로 바꾸지 말고, 호환성과 마이그레이션 절차를 확인합니다.

| 케이스 | 확인할 사항 |
| - | - |
| 깨끗한 애플리케이션에 도입 | 프로바이더가 로드되고 명령어 이름을 찾을 수 있다 |
| `php artisan list` | 패키지의 실행 처리나 외부 연결이 시작되지 않는다 |
| `php artisan help courier:status` | 설명과 입력 사양을 확인할 수 있다 |
| 실제 명령어 실행 | `handle()`의 의존성이 해결되고 의도한 종료 코드가 된다 |
| 설정이 미공개·미설정 | 목록·도움말이 깨지지 않고, 실행 시 부족한 설정을 적절히 처리할 수 있다 |
| 패키지나 Laravel 업데이트 | 기존 명령어 이름·인수·옵션을 계속 사용할 수 있다 |

위의 `StatusCommand`라면 프로바이더를 로드한 테스트 환경에서 다음과 같이 출력과 종료 코드를 검증할 수 있습니다. 테스트 기반은 [Orchestra Testbench로 Laravel 패키지를 테스트하기](/ko/advanced/package-testing)를, assertion의 자세한 내용은 [콘솔 테스트](/ko/console-tests)를 참조하세요.

```php theme={null}
public function test_status_command_displays_enabled_state(): void
{
    config()->set('courier.enabled', true);

    $this->artisan('courier:status')
        ->expectsOutput('Courier: enabled')
        ->assertExitCode(0);
}
```

이와 더불어 무거운 서비스를 사용하는 명령어에서는 목록이나 도움말 실행 시 서비스의 비즈니스 메서드가 호출되지 않는다는 것도 검증합니다. 등록할 수 있다는 것만 확인해서는 관계없는 명령어에 미치는 영향까지 감지할 수 없습니다.

## 참조한 1차 정보

* [Laravel 공식 문서: 패키지의 명령어 등록](https://github.com/laravel/docs/blob/13.x/packages.md#commands)
* [Laravel 공식 문서: 명령어 구조와 의존성 주입](https://github.com/laravel/docs/blob/13.x/artisan.md#command-structure)
* [ServiceProvider: commands()](https://github.com/laravel/framework/blob/v13.35.0/src/Illuminate/Support/ServiceProvider.php)
* [Console Application: starting()·bootstrap()·resolve()](https://github.com/laravel/framework/blob/v13.35.0/src/Illuminate/Console/Application.php)
* [Console Kernel: getArtisan()과 로더 등록](https://github.com/laravel/framework/blob/v13.35.0/src/Illuminate/Foundation/Console/Kernel.php)
* [Command: 생성자와 execute()](https://github.com/laravel/framework/blob/v13.35.0/src/Illuminate/Console/Command.php)
* [공식 테스트: 명령어 해결과 AsCommand에 의한 별칭 등록](https://github.com/laravel/framework/blob/v13.35.0/tests/Console/ConsoleApplicationTest.php)


## Related topics

- [패키지의 라우트 등록과 캐시](/ko/advanced/package-routes.md)
- [Laravel 패키지 개발](/ko/advanced/package-development.md)
- [패키지 캐시와 optimize 통합](/ko/advanced/package-optimization.md)
- [GeneratorCommand — 커스텀 make: 명령어 구현](/ko/advanced/generator-command.md)
- [패키지의 서비스 해석 후 훅](/ko/advanced/package-service-resolution.md)


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