> ## 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의 DB::extend와 Connection::resolverFor를 분석해 연결 이름과 드라이버 이름의 우선순위, 연결 보관, 재생성과 패키지 테스트를 설명합니다.

패키지에서 데이터베이스 연결을 확장하려면 **생성 방법의 등록**과 **사용할 연결의 선택**을 분리합니다. 설치만으로 애플리케이션의 기본 연결이나 모든 MySQL 연결이 대체되지 않도록 설계합니다.

이 페이지는 Laravel Framework `v13.40.0`의 공식 구현을 확인한 내용입니다. 연결 설정의 기본은 [데이터베이스 설정](https://kawax.biz/ko/database.md)을, 프로바이더의 로드는 [패키지 자동 검출](https://kawax.biz/ko/advanced/package-discovery.md)을 참조하세요.

## 확장 지점 선택하기

데이터베이스 생성 처리에는 적용 범위가 서로 다른 확장 지점이 있습니다.

| 확장 지점 | 적용 범위 | 콜백의 인수 |
| - | - | - |
| `DB::extend('연결 이름', $resolver)` | 지정한 이름의 연결 | 설정 배열, 연결 이름 |
| `DB::extend('드라이버 이름', $resolver)` | 해당 드라이버를 사용하는 연결. 단, 연결 이름의 확장이 우선 | 설정 배열, 연결 이름 |
| `Connection::resolverFor('sqlite', $resolver)` | 표준 ConnectionFactory가 생성하는 해당 드라이버의 Connection | PDO 또는 생성용 Closure, DB 이름, 테이블 접두사, 설정 배열 |

`DB::extend()`에 전달하는 콜백의 반환값은 PDO나 Query Builder가 아니라 `Illuminate\Database\Connection`입니다. `DatabaseManager::configure()`가 이 클래스를 요구합니다.

> [!WARNING]
>
> 데이터베이스의 `extend()`는 공통 `Illuminate\Support\Manager`와 같은 호출 규약이 아닙니다. 콜백의 첫 번째 인수는 애플리케이션 컨테이너가 아니라 **연결 설정 배열**입니다. 다른 드라이버 확장의 예시를 그대로 가져다 쓰지 마세요.

## 연결 이름의 확장이 드라이버 이름보다 우선한다

`DatabaseManager::makeConnection()`은 설정을 가져온 뒤 다음 순서로 생성 방법을 선택합니다.

```mermaid theme={null}
flowchart TD
    A["DB::connection으로 연결 이름을 지정"] --> B{"생성된 연결이 있는가?"}
    B -->|있음| C["보관된 Connection을 반환"]
    B -->|없음| D["연결 설정을 가져와 URL 설정을 해석"]
    D --> E{"연결 이름의 확장이 있는가?"}
    E -->|있음| F["연결 이름의 resolver를 실행"]
    E -->|없음| G{"driver의 확장이 있는가?"}
    G -->|있음| H["driver의 resolver를 실행"]
    G -->|없음| I["표준 ConnectionFactory로 생성"]
    F --> J["이벤트·재연결 처리 등을 설정하고 보관"]
    H --> J
    I --> J
```

* `DB::extend('reporting', ...)`는 `database.connections.reporting`에 적용됩니다.
* `DB::extend('mysql', ...)`는 연결 이름 쪽에 확장이 없고 `driver`가 `mysql`인 연결에 적용됩니다.
* 둘 다 같은 확장 레지스트리를 사용합니다. 연결 이름과 드라이버 이름의 충돌도 고려해 패키지 고유의 이름을 사용합니다.
* `extend()`는 설정 배열을 만들지 않습니다. 확장만 등록하고 지정한 이름의 연결 설정이 없으면 예외가 발생합니다.

## 독자 드라이버 이름을 명시적으로 선택하기

다음은 독자 이름 `acme-sqlite`를 표준 SQLite 연결 생성으로 연결하는 최소 예시입니다. **새로운 DB 프로토콜이나 SQL 방언을 구현하는 예시가 아닙니다**. 독자 어댑터를 배포할 때의 등록과 선택의 경계를 보여 줍니다.

`Acme\DatabaseBridge\`가 Composer의 PSR-4로 `src/`에 대응하는 구성을 가정하고, `src/DatabaseBridgeServiceProvider.php`에 배치합니다.

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

namespace Acme\DatabaseBridge;

use Illuminate\Database\Connection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\ServiceProvider;

final class DatabaseBridgeServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        $app = $this->app;

        DB::extend('acme-sqlite', function (array $config, string $name) use ($app): Connection {
            $config['driver'] = 'sqlite';

            return $app->make('db.factory')->make($config, $name);
        });
    }
}
```

`db.factory`는 Laravel이 공유하는 `ConnectionFactory`의 바인딩입니다. 표준 Factory를 직접 호출하므로 DatabaseManager의 확장 선택으로 되돌아가지 않고, SQLite의 지연 PDO 생성과 Connection 생성을 재사용할 수 있습니다.

이 콜백은 처음 연결을 해석할 때 실행됩니다. 프로바이더의 `boot()`는 생성 방법의 등록만 수행하며, 연결 테스트나 마이그레이션을 실행하지 않습니다. 일반적인 비지연 프로바이더로서 대상 연결이 처음 해석되기 전에 로드되도록 합니다.

사용하는 애플리케이션은 `config/database.php`의 기존 `connections` 배열에 연결을 추가합니다. 기본값인 `default`는 변경하지 않습니다.

```php theme={null}
'acme_reports' => [
    'driver' => 'acme-sqlite',
    'database' => database_path('reports.sqlite'),
    'prefix' => '',
    'foreign_key_constraints' => true,
],
```

SQLite 파일을 미리 준비하고 쓰기 권한을 확인한 뒤 사용합니다. 패키지를 로드하기만 해도 파일이 생성되는 설계로 만들지 않습니다.

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

$connection = DB::connection('acme_reports');
$rows = $connection->select('select 1 as value');
```

사용자가 전달하는 것은 연결 이름 `acme_reports`이며, 드라이버 이름 `acme-sqlite`가 아닙니다. 예시에서는 Factory에 전달하기 전에 `driver`를 `sqlite`로 변환하므로, 생성 후의 `getDriverName()`도 `sqlite`입니다.

> [!TIP]
>
> resolver 안에서 `DB::connection($name)`을 다시 호출하면 아직 보관되지 않은 같은 연결의 생성으로 되돌아갑니다. 재귀를 피하려면 Factory나 독자 어댑터로 Connection을 생성합니다.

## Connection 서브클래스로의 대체는 별개의 범위

기존 드라이버의 Connection 클래스 자체를 대체하려면 `Connection::resolverFor()`가 있습니다. 콜백에는 즉시 연결된 PDO가 아니라 **PDO를 지연 생성하는 Closure가 전달되는 경우도 있습니다**.

다만 이 등록은 Connection 클래스의 static 프로퍼티에 보관됩니다. `sqlite`에 등록하면 표준 Factory로 새로 만드는 SQLite 연결 전체에 영향을 주며, 위의 `acme-sqlite` 예시도 대상이 됩니다. 연결 이름 하나만 대체하는 용도로는 다룰 수 없습니다.

* 콜백에서는 대상 DB용 Connection 서브클래스를 반환합니다. 기본 Connection만으로는 DB 고유의 Grammar나 Processor 등을 유지할 수 없습니다.
* Factory는 `createConnector()`와 `createConnection()`을 분리하고 있습니다. `resolverFor()`를 등록하는 것만으로 알 수 없는 드라이버의 PDO 커넥터까지 제공되는 것은 아닙니다.
* `DB::extend()`의 독자 resolver가 표준 Factory를 사용하지 않으면, Factory 내부의 `resolverFor()` 확인도 거치지 않습니다.
* 같은 드라이버에 다시 등록하면 덮어씁니다. 다른 패키지와의 충돌과, 같은 PHP 프로세스 내 테스트로의 이월을 확인합니다.

영향을 제한하고 싶은 패키지는 먼저 고유한 연결 이름·드라이버 이름을 사용한 `DB::extend()`를 검토합니다.

## reconnect와 purge 구분하기

DatabaseManager는 생성된 Connection을 연결 이름별로 보관합니다. 설정이나 resolver를 변경해도 이미 생성된 연결에는 자동으로 반영되지 않습니다.

| 조작 | 보관된 Connection에 미치는 영향 |
| - | - |
| `DB::extend()` | 다음 생성 처리를 등록·덮어쓴다. 기존 객체는 그대로 |
| `DB::disconnect($name)` | PDO를 끊지만 Connection은 보관한다 |
| `DB::reconnect($name)` | 보관되어 있으면 새로 생성한 연결에서 PDO 등을 옮긴다. 원래 Connection을 반환한다 |
| `DB::purge($name)` | 연결을 끊고 Manager의 보관 대상에서 제외한다. 다음 `connection()`에서 재생성 |
| `DB::forgetExtension($key)` | resolver 등록을 삭제한다. 설정이나 생성된 연결은 삭제하지 않는다 |

특히 `reconnect()`는 보관된 Connection의 클래스나 설정 배열을 통째로 대체하는 API가 아닙니다. 확장 클래스나 연결 설정의 변경을 반영하려면, 진행 중인 트랜잭션이 없는 안전한 시점에 `purge()`한 뒤 새 연결을 가져옵니다.

> [!WARNING]
>
> `purge()`는 다른 서비스나 변수가 보관하고 있는 Connection 참조를 새것으로 교체하지 않습니다. 또한 연결 이름의 `::read`·`::write`·`::direct`는 별도의 보관 키가 되므로, 기본 이름을 purge하는 것만으로 이들까지 일괄 삭제된다고 생각하지 마세요.
>
> 테넌트 전환 등을 목적으로 실행 중인 공유 연결 설정을 함부로 바꾸지 마세요. 장기 실행 워커에서는 설정 캐시의 갱신뿐 아니라 프로세스 재시작과 보관된 서비스에 미치는 영향도 확인합니다.

## 패키지의 등록과 생성 테스트하기

[Orchestra Testbench](https://kawax.biz/ko/advanced/package-testing.md)에서는 프로바이더를 로드하고 독자 드라이버를 실제로 선택합니다. 다음은 `tests/DatabaseBridgeTest.php`의 예시입니다. 실행 환경에는 PDO와 SQLite 드라이버가 필요합니다.

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

namespace Acme\DatabaseBridge\Tests;

use Acme\DatabaseBridge\DatabaseBridgeServiceProvider;
use Illuminate\Database\SQLiteConnection;
use Orchestra\Testbench\TestCase;

final class DatabaseBridgeTest extends TestCase
{
    protected function getPackageProviders($app): array
    {
        return [DatabaseBridgeServiceProvider::class];
    }

    protected function defineEnvironment($app): void
    {
        $app['config']->set('database.default', 'testing');
        $app['config']->set('database.connections.acme_reports', [
            'driver' => 'acme-sqlite',
            'database' => ':memory:',
            'prefix' => '',
        ]);
    }

    public function test_registration_does_not_create_connections(): void
    {
        $manager = $this->app->make('db');

        $this->assertSame('testing', $manager->getDefaultConnection());
        $this->assertArrayNotHasKey('acme_reports', $manager->getConnections());
    }

    public function test_connection_is_reused_and_purge_creates_a_new_one(): void
    {
        $manager = $this->app->make('db');
        $first = $manager->connection('acme_reports');

        $this->assertInstanceOf(SQLiteConnection::class, $first);
        $this->assertSame('acme_reports', $first->getName());
        $this->assertSame('sqlite', $first->getDriverName());
        $this->assertSame(1, (int) $first->selectOne('select 1 as value')->value);
        $this->assertSame($first, $manager->connection('acme_reports'));
        $this->assertSame($first, $manager->reconnect('acme_reports'));

        $manager->purge('acme_reports');

        $second = $manager->connection('acme_reports');

        $this->assertNotSame($first, $second);
        $this->assertSame(1, (int) $second->selectOne('select 1 as value')->value);
    }
}
```

`:memory:`는 연결이 끊기면 내용이 사라지므로, 이 테스트는 테이블이나 데이터의 유지를 기대하지 않습니다. 실제 패키지에서는 다음도 추가합니다.

* 연결 이름의 resolver가 드라이버 이름의 resolver보다 우선하는지.
* 같은 이름의 재등록과 `forgetExtension()`이 생성된 Connection을 대체하지 않는지.
* 필수 설정이 빠졌을 때의 예외와 연결 실패 시의 동작.
* 지원 DB에서의 마이그레이션, 트랜잭션, read/write 분리 등 제공을 약속하는 기능.
* 장기 실행 워커나 여러 패키지와 함께 사용할 때 등록이나 연결 상태가 섞이지 않는지.

## 유지보수 시 확인 항목

* 독자 이름으로 옵트인하며, 기본 연결이나 내장 드라이버를 몰래 덮어쓰고 있지 않은가.
* config 캐시에 Closure나 연결 객체를 넣지 않고, 생성 처리를 프로바이더에 두고 있는가.
* 연결 대상과 인증 정보를 신뢰할 수 있는 애플리케이션 설정으로 관리하고 있는가.
* `extend()`의 인수, Factory의 생성 순서, `reconnect()`의 전달 대상을 지원 대상 Laravel에서 다시 확인했는가.
* 설정 변경 후의 재생성 절차와 워커 재시작을 사용자에게 안내하고 있는가.

## 관련 페이지

* [패키지 개발](https://kawax.biz/ko/advanced/package-development.md)
* [패키지 서비스의 라이프사이클 설계](https://kawax.biz/ko/advanced/package-service-lifetimes.md)
* [패키지 설정의 병합과 캐시](https://kawax.biz/ko/advanced/package-config-merging.md)
* [패키지 마이그레이션의 공개와 갱신](https://kawax.biz/ko/advanced/package-migrations.md)

## 참조한 1차 정보

* [Laravel 공식: 데이터베이스와 여러 연결](https://github.com/laravel/docs/blob/13.x/database.md#using-multiple-database-connections)
* [Laravel 공식: 패키지의 서비스 프로바이더](https://github.com/laravel/docs/blob/13.x/packages.md#service-providers)
* [DatabaseManager: 생성 순서, 확장 등록, 연결의 보관과 재연결](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Database/DatabaseManager.php)
* [ConnectionFactory: PDO의 지연 생성과 Connection resolver](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Database/Connectors/ConnectionFactory.php)
* [Connection: resolverFor와 getResolver](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Database/Connection.php)
* [DatabaseServiceProvider: db와 db.factory의 등록](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Database/DatabaseServiceProvider.php)


## Related topics

- [MongoDB](https://kawax.biz/ko/mongodb.md)
- [데이터베이스 마이그레이션](https://kawax.biz/ko/migrations.md)
- [데이터베이스 설정](https://kawax.biz/ko/database.md)
- [패키지의 커스텀 세션 드라이버](https://kawax.biz/ko/advanced/package-session-drivers.md)
- [Laravel Boost](https://kawax.biz/ko/boost.md)


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