> ## 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의 SessionManager를 분석해 SessionHandlerInterface의 저장 계약, 명시적인 드라이버 선택, TTL과 직렬화, 보관된 Store와 테스트를 설명합니다.

패키지에서 독자적인 세션 저장소를 제공하려면 **저장 처리의 등록**과 **애플리케이션에 의한 선택**을 분리합니다. 설치만으로 로그인 상태의 저장소가 바뀌지 않도록 하고, 사용자가 기존 세션에 미치는 영향을 확인한 뒤 활성화할 수 있게 설계합니다.

이 페이지는 Laravel Framework `v13.39.0`의 공식 소스를 확인한 내용입니다. 기본 조작은 [세션](https://kawax.biz/ko/session.md)을, 프로바이더의 자동 로드는 [패키지 자동 검출](https://kawax.biz/ko/advanced/package-discovery.md)을 참조하세요.

## extend가 받는 것은 Store가 아니라 핸들러

`Session::extend()`의 콜백은 PHP의 `SessionHandlerInterface`를 구현한 **저장용 핸들러**를 반환합니다. `Illuminate\Contracts\Session\Session`을 구현한 Store나 캐시 Repository를 직접 반환하는 API가 아닙니다.

`SessionManager::callCustomCreator()`는 콜백의 반환값을 `buildSession()`에 전달하고, `session.encrypt`에 따라 `Store` 또는 `EncryptedStore`로 감쌉니다. 이때 쿠키 이름과 `session.serialization`도 적용합니다.

```mermaid theme={null}
flowchart TD
    A["boot()에서 독자 드라이버 이름을 등록"] --> B["사용자가 session.driver를 선택"]
    B --> C["첫 driver() 호출 시 생성 콜백을 실행"]
    C --> D["SessionHandlerInterface를 반환"]
    D --> E["SessionManager가 Store 또는<br>EncryptedStore로 감쌈"]
    E --> F["StartSession이 읽기를 시작"]
    F --> G["애플리케이션이 put() 등으로 조작"]
    G --> H["Store가 직렬화하여<br>handler.write()에 전달"]
```

일반적인 HTTP 처리에서는 `StartSession` 미들웨어가 세션의 시작과 저장을 관리합니다. 핸들러를 제공하기 위해 패키지 측에서 `session_start()`나 `session_set_save_handler()`를 호출할 필요는 없습니다.

## 저장 계약을 나누어 생각하기

독자적인 백엔드를 구현하는 경우 다음 역할을 충족해야 합니다.

| 메서드 | 핸들러의 책임 |
| - | - |
| `open($savePath, $sessionName)` | 필요한 초기화. 불필요하면 `true`를 반환 |
| `close()` | 필요한 종료 처리. 불필요하면 `true`를 반환 |
| `read($sessionId)` | 저장된 문자열을 반환. 저장되지 않았거나 만료되었으면 빈 문자열을 반환 |
| `write($sessionId, $data)` | 전달된 문자열을 저장하고 성공 여부를 반환 |
| `destroy($sessionId)` | 지정한 ID의 데이터를 삭제 |
| `gc($lifetime)` | 초 단위 유효 기간을 기준으로 만료된 데이터를 삭제. 저장소가 자동으로 만료시킨다면 삭제 불필요 |

`Store`가 직렬화와 복원을 담당하므로 핸들러에서 세션 배열을 다시 `serialize()`하거나 `json_encode()`하지 않습니다. 암호화를 활성화한 경우에도 `EncryptedStore`가 처리한 문자열을 그대로 저장합니다.

> [!WARNING]
>
> 데이터가 저장되지 않은 경우와 백엔드 장애는 구분합니다. 확인한 `Store::readFromHandler()`는 읽기 결과가 비어 있거나 `false`이면 빈 세션으로 취급합니다. 또한 `Store::save()`는 핸들러의 `write()` 반환값을 검사하지 않습니다.
>
> 장애를 반드시 예외로 처리하려는 패키지에서는 저장소의 예외를 삼키지 말고, `false`를 반환하는 백엔드도 핸들러 측에서 감지해 예외로 변환하는 등 방침을 명시하고 테스트합니다. 실패 시 Laravel이 자동으로 재시도하거나 저장 실패를 알려 준다고 보장할 수는 없습니다.

## boot에서는 생성 처리만 등록하기

다음은 내장 `CacheBasedSessionHandler`를 재사용하는 최소 예시입니다. 새로운 스토리지 프로토콜을 구현하는 것이 아니라, 이름이 지정된 캐시 스토어를 사용하는 독자 드라이버의 등록 방법을 보여 줍니다. `Acme\SessionBridge\`가 Composer의 PSR-4로 `src/`에 대응하는 구성입니다.

`src/SessionBridgeServiceProvider.php`에 프로바이더를 준비합니다.

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

namespace Acme\SessionBridge;

use Illuminate\Contracts\Container\Container;
use Illuminate\Session\CacheBasedSessionHandler;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\ServiceProvider;
use InvalidArgumentException;
use SessionHandlerInterface;

final class SessionBridgeServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Session::extend('acme-cache', function (Container $app): SessionHandlerInterface {
            $config = $app->make('config');
            $store = $config->get('session.store');
            $minutes = (int) $config->get('session.lifetime', 120);

            if (! is_string($store) || $store === '' || $minutes <= 0) {
                throw new InvalidArgumentException(
                    'acme-cache requires a named cache store and a positive session lifetime.'
                );
            }

            return new CacheBasedSessionHandler(
                $app->make('cache')->store($store),
                $minutes,
            );
        });
    }
}
```

`Session::extend()`는 SessionManager를 가져와 생성 처리를 등록하지만, 이 시점에는 콜백 안의 캐시 스토어를 해석하지 않습니다. 저장소는 해당 드라이버가 처음 선택될 때 생성됩니다. 프로바이더의 `boot()`에서 연결 확인이나 데이터 저장을 실행하지 않는 구성으로 만듭니다.

독자적인 `SessionHandlerInterface` 구현을 사용하는 경우에는 콜백이 반환하는 핸들러를 교체합니다. `new Store(...)`까지 수행하면 SessionManager가 기대하는 반환값과 맞지 않게 됩니다.

* 프로바이더는 일반적인 비지연 프로바이더로 로드합니다.
* 세션이 처음 사용되기 전에 등록을 완료합니다. 다른 프로바이더가 먼저 독자 드라이버를 해석하는 경우에는 로드 순서도 확인합니다.
* 드라이버 이름에는 패키지 고유의 이름을 사용합니다. 같은 이름의 `extend()`는 생성 처리를 덮어쓰며, 내장 드라이버 이름을 등록하면 내장 구현보다 우선합니다.

## 사용하는 애플리케이션이 명시적으로 선택하기

사용자는 기존 `config/session.php`의 해당 키를 변경합니다. 다른 설정은 그대로 둡니다.

```php theme={null}
'driver' => 'acme-cache',
'store' => 'acme-sessions',
```

저장소로는 `config/cache.php`의 기존 `stores` 배열에 이름이 지정된 스토어를 추가합니다. 다음은 Redis 연결 `default`를 사용하는 예입니다. [Redis 연결 설정과 필요한 클라이언트](https://kawax.biz/ko/redis.md)는 애플리케이션 측에서 준비합니다.

```php theme={null}
'acme-sessions' => [
    'driver' => 'redis',
    'connection' => 'default',
    'prefix' => 'acme-sessions:',
],
```

이 예의 연결과 프리픽스는 **캐시 스토어 설정**에서 결정됩니다. 내장 Redis 세션 드라이버의 `createRedisDriver()`를 거치지 않으므로, `session.connection`이나 `session.prefix`를 추가해도 이 콜백이 자동으로 반영하지는 않습니다.

이 콜백은 Repository나 저장소 설정을 변경하지 않고 이름이 지정된 스토어를 그대로 사용합니다. 테스트용 `array` 스토어도 같은 애플리케이션 안에서는 같은 저장 영역을 공유합니다.

패키지에서 `session.driver`, 쿠키 이름, 애플리케이션 전체의 캐시 프리픽스를 무조건 덮어쓰지 않도록 합니다. 다른 용도와 저장 영역을 공유하지 않는 설계나, 여러 서버에서 같은 저장소를 사용할 수 있는 구성도 사용자와 함께 확인합니다.

> [!TIP]
>
> 일반적인 Redis나 데이터베이스 저장만이 목적이라면 먼저 내장 세션 드라이버를 사용하세요. 독자 드라이버는 새로운 저장소나 추가적인 저장 규약이 필요한 경우를 위한 확장 지점입니다.

## TTL 단위와 저장 형식 유지하기

`CacheBasedSessionHandler`의 생성자는 보존 기간을 **분** 단위로 받습니다. `write()`는 매번 `$minutes * 60`을 캐시 Repository의 `put()`에 전달하므로, 위의 예에서는 쓰기가 일어날 때마다 TTL이 갱신됩니다. `gc()`는 `0`을 반환하고 만료 처리는 캐시 측에 맡깁니다.

반면 `StartSession`이 핸들러의 `gc($lifetime)`에 전달하는 기간은 **초** 단위입니다. `session.lifetime` 값을 그대로 초로 저장하는 구현이 되지 않도록 주의합니다.

또한 다음 변경은 설정 이름만 바꾸는 것으로 끝나지 않습니다.

| 변경 | 기존 세션에 미치는 영향 |
| - | - |
| 저장소, 연결, 키 프리픽스 | 기존 데이터가 새 저장소에서 보이지 않게 됨 |
| `session.serialization` | 기존 형식의 데이터를 새 형식으로 복원할 수 없게 됨 |
| `session.encrypt` | 평문과 암호문의 읽기와 쓰기가 일치하지 않게 됨 |
| `session.cookie` | 브라우저가 보내는 기존 세션 ID 쿠키와의 대응이 바뀜 |

패키지 업데이트에서 이러한 항목을 변경하는 경우에는 재인증 안내, 마이그레이션 처리 여부, 이전 버전과 새 버전이 혼재하는 배포를 검토합니다. [패키지 버전 호환성 관리](https://kawax.biz/ko/advanced/package-versioning.md)와 마찬가지로 저장 형식과 설정 키도 유지보수할 공개 사양에 포함합니다.

## 생성된 Store는 설정 변경만으로 교체되지 않음

SessionManager의 부모 클래스인 `Manager`는 생성한 드라이버를 이름별로 보관합니다. 또한 `SessionServiceProvider`는 `session.store`를 singleton으로 등록합니다.

| 조작 | 일어나는 일 | 일어나지 않는 일 |
| - | - | - |
| 설정값만 변경 | 이후의 생성 처리가 새 값을 읽을 수 있음 | 생성된 핸들러의 TTL이나 연결 변경 |
| 같은 이름으로 `extend()`를 재등록 | 생성 콜백 교체 | 같은 이름으로 보관된 Store의 폐기 |
| `forgetDrivers()` | Manager 내부에 보관된 드라이버를 모두 삭제 | 해석된 `session.store`나 외부에서 보관하는 Store 참조의 교체 |

`forgetDrivers()`는 세션 데이터를 삭제하는 API도 아닙니다. 실행 중인 요청에서 Store를 다시 만들면 이미 읽어 들인 데이터나 다른 참조와의 불일치를 초래합니다.

이 예의 CacheManager도 이름이 지정된 스토어를 보관합니다. 그 연결 설정을 변경한 경우에는 SessionManager만 다시 해석하는 것으로는 충분하지 않습니다.

설정 캐시를 사용하는 배포에서는 캐시를 다시 빌드하고, 장기 실행 워커도 새 코드와 설정을 읽어 들이도록 재시작합니다. 애플리케이션 재시작과 저장된 데이터의 마이그레이션은 별개의 작업입니다.

## 세션 저장과 요청 락은 별개

커스텀 핸들러를 구현한 것만으로는 같은 세션에 대한 병렬 요청이 직렬화되지 않습니다. `StartSession`의 블로킹은 `session.block_store`로 선택하는 캐시 스토어의 락을 이용합니다. 세션 본체의 저장소와 같다고는 할 수 없습니다.

`Route::post(...)->block()` 등으로 제어하는 경우에는 락을 지원하는 캐시 스토어, 여러 서버 간 공유, 대기 시간과 보유 시간을 별도로 검증합니다. 패키지가 애플리케이션 전체의 블로킹 설정을 임의로 활성화하지 않도록 합니다.

## 저장과 재로드까지 테스트하기

[Orchestra Testbench](https://kawax.biz/ko/advanced/package-testing.md)를 사용하는 패키지에서는 프로바이더를 로드하고 독자 드라이버를 실제로 선택한 테스트를 준비합니다. 다음은 `tests/SessionDriverTest.php`의 예입니다. 테스트용 `array` 캐시는 외부 연결을 사용하지 않습니다.

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

namespace Acme\SessionBridge\Tests;

use Acme\SessionBridge\SessionBridgeServiceProvider;
use Illuminate\Session\CacheBasedSessionHandler;
use Illuminate\Session\Store;
use Orchestra\Testbench\TestCase;

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

    protected function defineEnvironment($app): void
    {
        $app['config']->set('session.driver', 'acme-cache');
        $app['config']->set('session.store', 'session-test');
        $app['config']->set('session.lifetime', 2);
        $app['config']->set('session.cookie', 'acme_session');
        $app['config']->set('session.serialization', 'json');
        $app['config']->set('session.encrypt', false);
        $app['config']->set('cache.stores.session-test', ['driver' => 'array']);
    }

    public function test_it_wraps_the_registered_handler(): void
    {
        $store = $this->app->make('session')->driver();

        $this->assertInstanceOf(Store::class, $store);
        $this->assertInstanceOf(CacheBasedSessionHandler::class, $store->getHandler());
        $this->assertSame($store, $this->app->make('session.store'));
    }

    public function test_it_saves_and_reloads_the_payload(): void
    {
        $manager = $this->app->make('session');
        $first = $manager->driver();
        $first->start();
        $first->put('cart.quantity', 3);
        $first->save();

        $id = $first->getId();
        $payload = $first->getHandler()->read($id);
        $this->assertSame(3, json_decode($payload, true)['cart']['quantity']);

        $manager->forgetDrivers();
        $second = $manager->driver();
        $second->setId($id);
        $second->start();

        $this->assertNotSame($first, $second);
        $this->assertSame(3, $second->get('cart.quantity'));

        $second->invalidate();
        $this->assertSame('', $second->getHandler()->read($id));
    }

    public function test_forgetting_drivers_does_not_replace_the_store_binding(): void
    {
        $old = $this->app->make('session.store');
        $manager = $this->app->make('session');

        $this->app['config']->set('session.lifetime', 5);
        $manager->forgetDrivers();

        $this->assertNotSame($old, $manager->driver());
        $this->assertSame($old, $this->app->make('session.store'));
    }
}
```

이 테스트는 같은 애플리케이션 안에서 Store를 다시 생성하는 **격리 테스트**입니다. HTTP 미들웨어나 쿠키 왕복, 다른 프로세스로의 영속화, 실제 Redis의 만료나 락을 검증하는 것은 아닙니다. `array`를 프로덕션의 공유 저장소로 사용하지 않도록 합니다.

독자 핸들러를 공개하기 전에는 다음 항목도 추가합니다.

* 저장되지 않았거나 만료된 ID가 빈 문자열을 반환하는지
* 저장한 문자열을 변경 없이 복원할 수 있고, 삭제 후에는 다시 가져올 수 없는지
* TTL의 단위와 갱신, 실제 백엔드에서의 만료
* 읽기, 쓰기, 삭제 실패 시 정해 둔 반환값이나 예외가 되는지
* `session.encrypt`를 활성화한 왕복과 지원하는 직렬화 형식
* 프로바이더를 로드하는 것만으로는 저장소에 접근하지 않고, 내장 드라이버의 선택도 빼앗지 않는지
* 필요하다면 실제 `web` 미들웨어와 쿠키를 거치는 HTTP 테스트, 그리고 병렬 요청의 락

## 관련 페이지

> [card] [세션](https://kawax.biz/ko/session.md)
>
> 세션 읽기와 쓰기, 플래시 데이터, ID 재생성을 확인합니다.

> [card] [Manager의 내부 구현](https://kawax.biz/ko/advanced/manager.md)
>
> 커스텀 드라이버의 생성과 보관, 기본 드라이버로의 위임을 확인합니다.

> [card] [패키지의 커스텀 캐시 스토어](https://kawax.biz/ko/advanced/package-cache-drivers.md)
>
> 저장소가 되는 캐시 Store와 Repository의 역할 분담, TTL 계약을 확인합니다.

> [card] [패키지 서비스의 라이프사이클](https://kawax.biz/ko/advanced/package-service-lifetimes.md)
>
> 장기 실행 워커에서 보관되는 의존성과 상태의 분리를 확인합니다.

## 1차 자료

* [공식 HTTP Session 문서: 커스텀 드라이버와 블로킹](https://github.com/laravel/docs/blob/625d1dfb61018e6bc50cc7ab67fca89ae018dfa2/session.md)
* [공식 패키지 개발 문서](https://github.com/laravel/docs/blob/625d1dfb61018e6bc50cc7ab67fca89ae018dfa2/packages.md)
* [SessionManager: 커스텀 생성 처리와 Store 구성](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Session/SessionManager.php)
* [Store: 읽기, 직렬화, 저장, ID 재생성](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Session/Store.php)
* [CacheBasedSessionHandler: TTL 변환과 저장 계약](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Session/CacheBasedSessionHandler.php)
* [CacheManager: 이름이 지정된 스토어와 Redis 설정](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Cache/CacheManager.php)
* [Manager: extend, driver, forgetDrivers](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Support/Manager.php)
* [SessionServiceProvider: Manager와 session.store의 singleton 등록](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Session/SessionServiceProvider.php)
* [StartSession: 저장 처리, GC, 캐시를 이용한 락](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Session/Middleware/StartSession.php)
* [공식 CacheBasedSessionHandlerTest: 미저장, TTL, 삭제 검증](https://github.com/laravel/framework/blob/v13.39.0/tests/Session/CacheBasedSessionHandlerTest.php)


## Related topics

- [코어 패키지와 커스텀 드라이버 - Feedable](https://kawax.biz/ko/packages/feedable/core.md)
- [패키지의 커스텀 캐시 스토어와 계약](https://kawax.biz/ko/advanced/package-cache-drivers.md)
- [패키지의 Storage 드라이버 등록과 재해석](https://kawax.biz/ko/advanced/package-filesystem-drivers.md)
- [패키지의 커스텀 알림 채널과 전송 검증](https://kawax.biz/ko/advanced/package-notification-channels.md)
- [Illuminate\Support\Manager — 드라이버 시스템의 해부](https://kawax.biz/ko/advanced/manager.md)


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