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

# 패키지의 Storage 드라이버 등록과 재해석

> Laravel 13의 FilesystemManager를 분석하여, Storage::extend() 등록, 디스크 인스턴스 보관, 설정 변경 후의 재해석과 fake에 의존하지 않는 검증을 해설합니다.

패키지에서 스토리지 기능을 제공하려면 **드라이버 생성 방법을 등록하는 것**과 **사용자의 디스크를 생성하는 것**을 분리합니다. 프로바이더 부팅만으로 외부 연결이나 파일 조작을 시작하지 않도록 설계하면, 해당 기능을 사용하지 않는 환경의 부팅도 망가뜨리기 어려워집니다.

이 페이지는 Laravel Framework `v13.37.0`의 구현을 확인한 내용입니다. 기본 조작은 [파일 스토리지](/ko/filesystem)를, 프로바이더 로딩은 [패키지 자동 검색](/ko/advanced/package-discovery)을 참조하세요.

## 드라이버 이름과 디스크 이름을 구분한다

`Storage::extend()`의 첫 번째 인수는 **드라이버 이름**입니다. 반면 `Storage::disk()`의 인수는 `filesystems.disks`에 정의하는 **디스크 이름**입니다. 같은 드라이버를 사용하는 디스크를 여러 개 정의할 수 있습니다.

| 요소 | 이 페이지의 예 | 역할 |
| - | - | - |
| 드라이버 이름 | `acme-archive-local` | 디스크를 생성하는 콜백의 식별자 |
| 디스크 이름 | `reports`, `exports` | 설정과 생성된 인스턴스의 식별자 |
| 디스크 설정 | `root`, `throw` 등 | 해당 디스크 생성 시 콜백에 전달되는 값 |

`FilesystemManager::resolve()`는 설정의 `driver`에 대응하는 커스텀 생성 처리를 찾고, 있으면 내장 드라이버보다 먼저 호출합니다. `local`이나 `s3`를 확장 이름으로 사용하면 기존 생성 처리를 대체하므로, 패키지 고유의 이름을 선택합니다.

## 프로바이더에서는 생성 방법만 등록한다

다음은 등록 경로를 이해하기 위해 Laravel 표준 로컬 드라이버 생성 처리를 재사용하는 최소 예제입니다. 로컬 저장만이 목적이라면 일반 `local`로 충분합니다. 독자적인 백엔드를 제공하는 경우에는 공식 문서의 예처럼 Flysystem 어댑터를 구축하고 Laravel의 `FilesystemAdapter`로 감싸는 처리로 바꿉니다.

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

namespace Acme\Archive;

use Illuminate\Contracts\Foundation\Application;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\ServiceProvider;
use InvalidArgumentException;

class ArchiveServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Storage::extend('acme-archive-local', static function (
            Application $app,
            array $config,
        ): FilesystemAdapter {
            if (! is_string($config['root'] ?? null) || $config['root'] === '') {
                throw new InvalidArgumentException('Archive disk requires a root.');
            }

            return $app['filesystem']->createLocalDriver($config);
        });
    }
}
```

이 프로바이더는 [일반적인 방법으로 패키지에 등록](/ko/advanced/package-discovery)합니다. HTTP, Artisan, 큐 워커에서 사용할 수 있도록 `runningInConsole()`로 등록을 제한하지 않습니다. 또한 확장 등록만 담당하는 프로바이더는 [지연 프로바이더](/ko/advanced/deferred-provider)로 만들지 않습니다.

콜백에는 애플리케이션과 **해석 대상 디스크의 설정 전체**가 전달됩니다. 설정 검증이나 클라이언트 생성은 이 안에서 수행하고, `boot()`에서는 파일 조작을 하지 않습니다. 예제의 `createLocalDriver()`는 생성 시 디렉터리를 만들 수 있으므로, 등록 시점과 생성 시점을 분리하는 것이 중요합니다.

<Tip>
  `FilesystemManager::extend()`는 일반 클로저의 `$this`를 Manager에 바인딩합니다. 프로바이더의 `$this`를 사용할 수 있다고 전제하지 말고, 이 예제처럼 `static` 클로저와 인수인 `$app`을 사용하면 의존 관계가 명확해집니다.
</Tip>

사용자는 `config/filesystems.php`의 `disks` 배열에 설정을 추가합니다.

```php theme={null}
'reports' => [
    'driver' => 'acme-archive-local',
    'root' => storage_path('app/private/reports'),
    'throw' => true,
],
'exports' => [
    'driver' => 'acme-archive-local',
    'root' => storage_path('app/private/exports'),
    'throw' => true,
],
```

사용하는 쪽에서는 드라이버 이름이 아니라 디스크 이름을 지정합니다.

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

Storage::disk('reports')->put('summary.txt', 'Monthly summary');
```

<Info>
  이 예제는 로컬 저장의 생성 경로만 보여 줍니다. `serve`에 의한 파일 제공이나 임시 URL 제공은 다루지 않습니다. 독자적인 어댑터에서 URL 생성, 가시성, 읽기 전용 등을 지원하는 경우에도 각각의 구현과 필요한 의존 패키지를 확인하고, 설정 키를 전달하기만 하면 활성화된다고 판단하지 마세요.
</Info>

## 이름 있는 디스크는 생성 후 보관된다

`disk('reports')`는 매번 콜백을 호출하지 않습니다. Manager의 `get()`이 내부 `$disks` 배열에 있는 생성된 인스턴스를 우선합니다. 아직 생성되지 않은 경우에만 `resolve()`를 거치며, 생성 결과는 디스크 이름을 키로 보관됩니다.

```mermaid theme={null}
flowchart TD
    A["프로바이더의 boot()"] --> B["extend()로 생성 처리를 등록"]
    B --> C["Storage::disk('reports')"]
    C --> D{"reports는 생성됨?"}
    D -->|예| E["보관 중인 인스턴스를 반환"]
    D -->|아니요| F["driver를 읽고 생성 처리에 설정을 전달"]
    F --> G["생성 결과를 reports로 보관"]
    G --> E
```

따라서 같은 프로세스에서 다음을 수행해도 생성된 디스크는 자동으로 갱신되지 않습니다.

* `config()`로 `filesystems.disks.reports`를 변경한다.
* 같은 드라이버 이름에 대해 `Storage::extend()`를 다시 호출한다.

`FilesystemServiceProvider`는 Manager 자체도 singleton으로 등록합니다. 장시간 동작하는 프로세스에서는 요청이나 잡마다 디스크가 반드시 다시 만들어진다고 가정하지 마세요.

## 설정 변경 후에는 필요한 디스크만 재해석한다

같은 프로세스에서 설정을 변경하여 검증하는 경우에는 설정 갱신과 보관된 디스크 제거를 분리합니다.

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

$before = Storage::disk('reports');

config(['filesystems.disks.reports.root' => storage_path('app/private/reports-v2')]);
Storage::forgetDisk('reports');

$after = Storage::disk('reports');
```

`forgetDisk()`는 지정한 이름을 Manager의 보관 배열에서 제거합니다. 다음 `disk()` 호출에서 현재 설정을 다시 읽습니다. 확인한 버전의 `purge()`도 지정한 이름을 제거하는 구현이며, 독자적인 백엔드의 연결 종료 메서드를 호출하는 것은 아닙니다.

<Warning>
  `$before`처럼 사용하는 쪽에서 보관하고 있는 참조까지 바뀌는 것은 아닙니다. singleton 서비스가 오래된 디스크를 보관하고 있는 경우도 마찬가지입니다. `forgetDisk()`를 모든 참조나 다른 프로세스를 일괄 갱신하는 수단으로 사용하지 마세요.
</Warning>

프로덕션의 설정 변경이나 패키지 업데이트에서는 일반적인 배포 절차에 따라 설정 캐시를 다시 생성하고, 장시간 동작하는 워커도 재시작합니다. 설정 캐시와 이 프로세스 내의 디스크 보관 배열은 별개입니다.

### 일시적인 설정에는 build를 사용한다

이름 있는 공유 설정을 변경하고 싶지 않다면 [온디맨드 디스크](/ko/filesystem)를 사용할 수 있습니다.

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

$disk = Storage::build([
    'driver' => 'acme-archive-local',
    'root' => storage_path('app/private/one-off-export'),
    'throw' => true,
]);
```

`build()`는 지정한 설정으로 직접 `resolve()`를 호출하며, 결과를 이름 있는 디스크의 보관 배열에 추가하지 않습니다. 커스텀 드라이버 등록은 필요하지만, 호출할 때마다 생성 처리로 진행합니다. 동적인 저장 위치를 사용하는 경우에도 저장 위치나 연결 대상을 사용자 입력으로 무제한 선택할 수 있도록 설계하지 않습니다.

## fake와 별도로 생성 경로를 테스트한다

`Storage::fake('reports')`는 Manager의 `set()`으로 해당 디스크를 테스트용 로컬 디스크로 대체합니다. 애플리케이션의 파일 조작 테스트에는 편리하지만, **패키지의 커스텀 생성 처리가 올바르게 등록되고 실행되는지는 검증하지 않습니다**.

[패키지의 테스트 환경](/ko/advanced/package-testing)에서 `ArchiveServiceProvider`를 로드한 뒤 다음을 나누어 검증합니다.

| 테스트 | 확인할 내용 |
| - | - |
| 일반 부팅 | 디스크를 사용하지 않으면 생성 처리 내의 설정 검증이나 파일 조작이 시작되지 않음 |
| 이름 있는 디스크 | `reports`와 `exports`에 각각의 설정이 전달되고, 서로 다른 인스턴스가 됨 |
| 같은 이름의 재취득 | `disk('reports')`를 두 번 호출하면 같은 인스턴스가 됨 |
| 설정 변경 | 설정을 바꾸기만 하면 같은 인스턴스, `forgetDisk()` 후에는 다른 인스턴스가 됨 |
| 온디맨드 | 같은 설정으로 `build()`를 두 번 호출해도 다른 인스턴스가 됨 |
| 잘못된 설정 | 이 예제에서는 `root`가 없는 디스크를 해석할 때 예외가 발생함 |
| 실제 읽기/쓰기 | 테스트 전용 디렉터리에서 저장, 조회, 삭제를 검증하고 종료 시 정리함 |
| 설정 캐시 있음 | 새로 부팅한 검증용 애플리케이션에서도 드라이버가 등록되고 해석할 수 있음 |

독자적인 외부 백엔드에서는 네트워크를 사용하지 않는 생성 테스트와, 명시적으로 준비한 검증 환경에서의 연결 테스트를 분리합니다. fake의 성공을 실제 서비스와의 호환성 증거로 삼지 않습니다.

## 장기 유지보수 체크리스트

* 드라이버 이름, 설정 키, 반환값의 API, 예외나 `throw`의 처리를 공개 사양으로 문서화한다.
* 표준 드라이버나 다른 패키지와 충돌하지 않는 확장 이름을 사용한다.
* 프로바이더 부팅만으로 디스크를 해석하거나 사용하지 않는다.
* 설정 변경 시에는 설정 캐시, Manager의 보관 배열, 사용하는 쪽의 참조, 다른 프로세스를 구분한다.
* Laravel의 지원 버전마다 생성, 재해석, 실제 파일 조작을 지속적으로 검증한다.

## 참조한 1차 자료

* [Laravel 공식 문서: 커스텀 파일 시스템](https://github.com/laravel/docs/blob/12080c72699b/filesystem.md#custom-filesystems)
* [FilesystemManager: disk·build·resolve·forgetDisk·purge·extend](https://github.com/laravel/framework/blob/v13.37.0/src/Illuminate/Filesystem/FilesystemManager.php)
* [FilesystemServiceProvider: Manager의 singleton 등록](https://github.com/laravel/framework/blob/v13.37.0/src/Illuminate/Filesystem/FilesystemServiceProvider.php)
* [Storage 파사드: fake에 의한 로컬 디스크로의 대체](https://github.com/laravel/framework/blob/v13.37.0/src/Illuminate/Support/Facades/Storage.php)
* [Laravel 공식 테스트: 디스크의 생성·제거·커스텀 클로저](https://github.com/laravel/framework/blob/v13.37.0/tests/Filesystem/FilesystemManagerTest.php)


## Related topics

- [패키지의 서비스 해석 후 훅](/ko/advanced/package-service-resolution.md)
- [패키지의 라우트 등록과 캐시](/ko/advanced/package-routes.md)
- [코어 패키지와 커스텀 드라이버 - Feedable](/ko/packages/feedable/core.md)
- [패키지의 미들웨어 등록과 적용 범위](/ko/advanced/package-middleware.md)
- [패키지 서비스의 라이프사이클 설계](/ko/advanced/package-service-lifetimes.md)


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