> ## 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 Pennant

> Laravel Pennant를 사용한 기능 플래그(Feature Flags) 관리 방법을 설명합니다. 기능 정의부터 확인, 스코프 설정, 테스트까지 폭넓게 다룹니다.

## Laravel Pennant란

[Laravel Pennant](https://github.com/laravel/pennant)는 단순하고 가벼운 기능 플래그(Feature Flag) 패키지입니다. 기능 플래그를 사용하면 새로운 기능을 단계적으로 롤아웃하거나, A/B 테스트를 실시하거나, 트렁크 기반 개발을 보완할 수 있습니다.

### 기능 플래그란

```mermaid theme={null}
flowchart TD
    A["리퀘스트"] --> B{"Feature::active('new-api')"}
    B -->|true| C["새로운 API 처리"]
    B -->|false| D["기존 API 처리"]
    C --> E["레스폰스"]
    D --> E
```

기능 플래그를 사용하면 코드의 배포와 릴리스를 분리할 수 있습니다. 코드는 프로덕션 환경에 배포해두면서, 기능의 ON/OFF를 설정으로 제어할 수 있습니다.

***

## 설치

<Steps>
  <Step title="패키지 설치">
    Composer를 사용해 Pennant를 설치합니다.

    ```bash theme={null}
    composer require laravel/pennant
    ```
  </Step>

  <Step title="설정 파일과 마이그레이션 공개">
    `vendor:publish` Artisan 명령으로 파일을 공개합니다.

    ```bash theme={null}
    php artisan vendor:publish --provider="Laravel\Pennant\PennantServiceProvider"
    ```

    이를 통해 `config/pennant.php`와 `database/migrations`에 마이그레이션 파일이 생성됩니다.
  </Step>

  <Step title="마이그레이션 실행">
    Pennant가 기능 플래그 값을 저장하는 `features` 테이블을 생성합니다.

    ```bash theme={null}
    php artisan migrate
    ```
  </Step>
</Steps>

***

## 설정

`config/pennant.php`에서 사용할 스토리지 드라이버를 설정할 수 있습니다. Pennant는 두 종류의 드라이버를 지원합니다.

| 드라이버       | 설명                      |
| ---------- | ----------------------- |
| `database` | 관계형 DB에 값을 영구 저장 (기본값)  |
| `array`    | 인메모리에 저장 (테스트나 일시적 사용용) |

```php theme={null}
// config/pennant.php
'default' => env('PENNANT_STORE', 'database'),
```

***

## 기능 정의

### 클로저 기반 정의

기능은 `Feature` 파사드의 `define` 메서드로 정의합니다. 보통 서비스 프로바이더의 `boot` 메서드에서 정의합니다. 클로저에는 "스코프"(보통 인증된 사용자)가 전달됩니다.

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

namespace App\Providers;

use App\Models\User;
use Illuminate\Support\Lottery;
use Illuminate\Support\ServiceProvider;
use Laravel\Pennant\Feature;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Feature::define('new-api', fn (User $user) => match (true) {
            $user->isInternalTeamMember() => true,
            $user->isHighTrafficCustomer() => false,
            default => Lottery::odds(1 / 100),
        });
    }
}
```

이 기능의 로직은 다음과 같습니다.

* 사내 팀 멤버는 반드시 ON
* 트래픽이 많은 고객은 OFF
* 그 외에는 1% 확률로 ON

기능이 처음 체크될 때 클로저의 결과가 스토리지 드라이버에 저장됩니다. 다음번부터는 저장된 값이 사용됩니다.

<Info>
  정의가 Lottery만 반환하는 경우 클로저를 생략할 수 있습니다.

  ```php theme={null}
  Feature::define('site-redesign', Lottery::odds(1, 1000));
  ```
</Info>

### 클래스 기반 정의

Pennant는 클래스 기반의 기능 정의도 지원합니다. 클래스 기반인 경우 서비스 프로바이더에 대한 등록은 불필요합니다.

```bash theme={null}
php artisan pennant:feature NewApi
```

생성된 클래스는 `app/Features` 디렉터리에 배치됩니다. `resolve` 메서드를 구현하기만 하면 됩니다.

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

namespace App\Features;

use App\Models\User;
use Illuminate\Support\Lottery;

class NewApi
{
    /**
     * 기능의 초기값을 해결한다
     */
    public function resolve(User $user): mixed
    {
        return match (true) {
            $user->isInternalTeamMember() => true,
            $user->isHighTrafficCustomer() => false,
            default => Lottery::odds(1 / 100),
        };
    }
}
```

#### 저장명 커스터마이즈

기본적으로 완전 수식 클래스명이 저장됩니다. `Name` 애트리뷰트로 이름을 커스터마이즈할 수 있습니다.

```php theme={null}
use Laravel\Pennant\Attributes\Name;

#[Name('new-api')]
class NewApi
{
    // ...
}
```

#### 기능 체크 가로채기 (`before` 메서드)

클래스 기반 기능에는 `before` 메서드를 정의할 수 있습니다. 이 메서드는 스토리지에서 값을 가져오기 전에 인메모리에서 실행되며, `null` 이외의 값을 반환하면 그 값이 사용됩니다.

```php theme={null}
class NewApi
{
    public function before(User $user): mixed
    {
        if (Config::get('features.new-api.disabled')) {
            return $user->isInternalTeamMember();
        }
    }

    public function resolve(User $user): mixed
    {
        // ...
    }
}
```

<Tip>
  `before` 메서드는 버그 발생 시 기능을 긴급 비활성화하거나, 특정 일시에 롤아웃을 스케줄링할 때 유용합니다.
</Tip>

***

## 기능 확인

### `Feature::active()` / `Feature::inactive()`

`active` 메서드로 기능이 활성화되어 있는지 확인할 수 있습니다. 기본적으로 현재 인증된 사용자에 대해 체크가 수행됩니다.

```php theme={null}
use Laravel\Pennant\Feature;

if (Feature::active('new-api')) {
    // 새로운 API를 사용하는 처리
}
```

클래스 기반 기능인 경우 클래스명을 전달합니다.

```php theme={null}
use App\Features\NewApi;

if (Feature::active(NewApi::class)) {
    // ...
}
```

그 밖에 편리한 메서드도 준비되어 있습니다.

```php theme={null}
// 모든 기능이 활성화되어 있는지
Feature::allAreActive(['new-api', 'site-redesign']);

// 어느 하나의 기능이 활성화되어 있는지
Feature::someAreActive(['new-api', 'site-redesign']);

// 기능이 비활성화되어 있는지
Feature::inactive('new-api');

// 모든 기능이 비활성화되어 있는지
Feature::allAreInactive(['new-api', 'site-redesign']);

// 어느 하나의 기능이 비활성화되어 있는지
Feature::someAreInactive(['new-api', 'site-redesign']);
```

### 조건부 실행 (`when` / `unless`)

`when` 메서드를 사용하면 기능이 활성화되어 있는 경우에만 클로저를 실행할 수 있습니다.

```php theme={null}
return Feature::when(NewApi::class,
    fn () => $this->resolveNewApiResponse($request),
    fn () => $this->resolveLegacyApiResponse($request),
);
```

`unless`는 `when`의 반대로, 기능이 비활성화되어 있는 경우에 첫 번째 클로저를 실행합니다.

```php theme={null}
return Feature::unless(NewApi::class,
    fn () => $this->resolveLegacyApiResponse($request),
    fn () => $this->resolveNewApiResponse($request),
);
```

### `HasFeatures` 트레이트

`HasFeatures` 트레이트를 `User` 모델에 추가하면, 모델에서 직접 기능을 체크할 수 있습니다.

```php theme={null}
use Laravel\Pennant\Concerns\HasFeatures;

class User extends Authenticatable
{
    use HasFeatures;
}
```

```php theme={null}
if ($user->features()->active('new-api')) {
    // ...
}

// 값 가져오기
$value = $user->features()->value('purchase-button');

// 조건부 실행
$user->features()->when('new-api',
    fn () => /* ... */,
    fn () => /* ... */,
);
```

### Blade 디렉티브

Blade 템플릿에서는 `@feature` 디렉티브를 사용할 수 있습니다.

```blade theme={null}
@feature('site-redesign')
    {{-- 'site-redesign'이 활성화된 경우 --}}
@else
    {{-- 'site-redesign'이 비활성화된 경우 --}}
@endfeature

@featureany(['site-redesign', 'beta'])
    {{-- 둘 중 하나가 활성화된 경우 --}}
@endfeatureany
```

### 미들웨어

`EnsureFeaturesAreActive` 미들웨어를 사용하면 라우트 접근에 기능이 필요함을 지정할 수 있습니다. 기능이 비활성화되어 있는 경우 `400 Bad Request`가 반환됩니다.

```php theme={null}
use Laravel\Pennant\Middleware\EnsureFeaturesAreActive;

Route::get('/api/servers', function () {
    // ...
})->middleware(EnsureFeaturesAreActive::using('new-api', 'servers-api'));
```

응답을 커스터마이즈하려면 `whenInactive` 메서드를 사용합니다.

```php theme={null}
EnsureFeaturesAreActive::whenInactive(
    function (Request $request, array $features) {
        return new Response(status: 403);
    }
);
```

### 인메모리 캐시

Pennant는 1리퀘스트 내에서 기능의 결과를 인메모리에 캐시합니다. 같은 기능 플래그를 여러 번 체크해도 추가 DB 쿼리는 발생하지 않습니다.

캐시를 수동으로 클리어하려면 `flushCache` 메서드를 사용합니다.

```php theme={null}
Feature::flushCache();
```

***

## 스코프

### 스코프 지정

기본적으로 인증된 사용자가 스코프가 되지만, `for` 메서드로 임의의 스코프를 지정할 수 있습니다.

```php theme={null}
// 특정 사용자에 대해 체크
Feature::for($user)->active('new-api');

// 팀에 대해 체크
Feature::for($user->team)->active('billing-v2');
```

팀별로 기능을 관리하는 예입니다.

```php theme={null}
Feature::define('billing-v2', function (Team $team) {
    if ($team->created_at->isAfter(new Carbon('1st Jan, 2023'))) {
        return true;
    }

    if ($team->created_at->isAfter(new Carbon('1st Jan, 2019'))) {
        return Lottery::odds(1 / 100);
    }

    return Lottery::odds(1 / 1000);
});
```

### 기본 스코프 커스터마이즈

`Feature::resolveScopeUsing`으로 기본 스코프를 커스터마이즈할 수 있습니다.

```php theme={null}
Feature::resolveScopeUsing(fn ($driver) => Auth::user()?->team);
```

설정 후에는 `for`를 생략하면 기본 스코프가 사용됩니다.

```php theme={null}
Feature::active('billing-v2');
// 위는 아래와 동등
Feature::for($user->team)->active('billing-v2');
```

### Nullable Scope

스코프가 `null`인 경우(미인증 라우트, Artisan 명령 등), 기능 정의가 null에 대응하지 않으면 자동으로 `false`가 반환됩니다. null을 다루는 경우 nullable 타입으로 정의해 주세요.

```php theme={null}
Feature::define('new-api', fn (User|null $user) => match (true) {
    $user === null => true,
    $user->isInternalTeamMember() => true,
    $user->isHighTrafficCustomer() => false,
    default => Lottery::odds(1 / 100),
});
```

***

## 리치 기능 값

기능은 boolean 이외의 값도 반환할 수 있습니다. 예를 들어 A/B 테스트에서 버튼의 색상을 제어하는 경우입니다.

```php theme={null}
Feature::define('purchase-button', fn (User $user) => Arr::random([
    'blue-sapphire',
    'seafoam-green',
    'tart-orange',
]));
```

값을 가져오려면 `value` 메서드를 사용합니다.

```php theme={null}
$color = Feature::value('purchase-button');
```

Blade에서는 값을 사용한 조건 분기도 가능합니다.

```blade theme={null}
@feature('purchase-button', 'blue-sapphire')
    {{-- blue-sapphire가 활성 --}}
@elsefeature('purchase-button', 'seafoam-green')
    {{-- seafoam-green이 활성 --}}
@elsefeature('purchase-button', 'tart-orange')
    {{-- tart-orange가 활성 --}}
@endfeature
```

<Info>
  리치 값을 사용하는 경우, `false` 이외의 모든 값이 활성으로 간주됩니다.
</Info>

`when` 메서드에 리치 값이 전달되는 경우, 첫 번째 클로저에 값이 전달됩니다.

```php theme={null}
Feature::when('purchase-button',
    fn ($color) => /* $color에 값이 들어감 */,
    fn () => /* 비활성 시 */,
);
```

***

## 여러 기능 가져오기

`values` 메서드로 여러 기능의 값을 한 번에 가져올 수 있습니다.

```php theme={null}
Feature::values(['billing-v2', 'purchase-button']);

// [
//     'billing-v2' => false,
//     'purchase-button' => 'blue-sapphire',
// ]
```

`all` 메서드로 정의된 모든 기능의 값을 가져올 수 있습니다.

```php theme={null}
Feature::all();
```

클래스 기반 기능을 `all`의 결과에 포함시키려면 서비스 프로바이더에서 `discover`를 호출합니다.

```php theme={null}
Feature::discover();
```

이를 통해 `app/Features` 디렉터리의 모든 기능 클래스가 등록됩니다.

***

## Eager Loading

루프 안에서 기능 체크를 수행하는 경우 성능 문제가 발생할 수 있습니다. `load` 메서드를 사용해 미리 값을 가져와 두면 해결할 수 있습니다.

```php theme={null}
// NG: 루프마다 DB 쿼리가 발생
foreach ($users as $user) {
    if (Feature::for($user)->active('notifications-beta')) {
        $user->notify(new RegistrationSuccess);
    }
}

// OK: 미리 일괄 가져오기
Feature::for($users)->load(['notifications-beta']);

foreach ($users as $user) {
    if (Feature::for($user)->active('notifications-beta')) {
        $user->notify(new RegistrationSuccess);
    }
}
```

미취득 값만 가져오려면 `loadMissing`을 사용합니다.

```php theme={null}
Feature::for($users)->loadMissing([
    'new-api',
    'purchase-button',
    'notifications-beta',
]);
```

***

## 값 업데이트

### 수동 업데이트

`activate` / `deactivate` 메서드로 기능의 ON/OFF를 전환할 수 있습니다.

```php theme={null}
// 기본 스코프에서 활성화
Feature::activate('new-api');

// 특정 스코프에서 비활성화
Feature::for($user->team)->deactivate('billing-v2');

// 리치 값 설정
Feature::activate('purchase-button', 'seafoam-green');
```

저장된 값을 잊게 하려면 `forget` 메서드를 사용합니다. 다음 체크 시에 정의로부터 재평가됩니다.

```php theme={null}
Feature::forget('purchase-button');
```

### 일괄 업데이트

`activateForEveryone` / `deactivateForEveryone`으로 스토리지 내 모든 스코프에 일괄 적용할 수 있습니다.

```php theme={null}
Feature::activateForEveryone('new-api');
Feature::activateForEveryone('purchase-button', 'seafoam-green');
Feature::deactivateForEveryone('new-api');
```

### 기능 퍼지

기능을 애플리케이션에서 삭제한 경우나 정의를 변경한 경우, 스토리지에서 값을 삭제(퍼지)할 수 있습니다.

```php theme={null}
// 단일 기능 퍼지
Feature::purge('new-api');

// 여러 기능 퍼지
Feature::purge(['new-api', 'purchase-button']);

// 모든 기능 퍼지
Feature::purge();
```

Artisan 명령으로도 퍼지할 수 있습니다. 배포 파이프라인에 통합하면 편리합니다.

```bash theme={null}
php artisan pennant:purge new-api

# 여러 개 지정
php artisan pennant:purge new-api purchase-button

# 지정한 기능 이외를 모두 퍼지
php artisan pennant:purge --except=new-api --except=purchase-button

# 서비스 프로바이더에 등록된 기능 이외를 퍼지
php artisan pennant:purge --except-registered
```

***

## 테스트

### 기능 재정의

테스트에서는 `Feature::define`으로 기능을 재정의함으로써 반환 값을 제어할 수 있습니다.

```php tab=Pest theme={null}
use Laravel\Pennant\Feature;

test('it can control feature values', function () {
    Feature::define('purchase-button', 'seafoam-green');

    expect(Feature::value('purchase-button'))->toBe('seafoam-green');
});
```

```php tab=PHPUnit theme={null}
use Laravel\Pennant\Feature;

public function test_it_can_control_feature_values(): void
{
    Feature::define('purchase-button', 'seafoam-green');

    $this->assertSame('seafoam-green', Feature::value('purchase-button'));
}
```

클래스 기반 기능도 마찬가지로 다룰 수 있습니다.

```php tab=Pest theme={null}
test('it can control feature values', function () {
    Feature::define(NewApi::class, true);

    expect(Feature::value(NewApi::class))->toBeTrue();
});
```

```php tab=PHPUnit theme={null}
use App\Features\NewApi;

public function test_it_can_control_feature_values(): void
{
    Feature::define(NewApi::class, true);

    $this->assertTrue(Feature::value(NewApi::class));
}
```

### 테스트용 스토어 설정

테스트 중에 사용할 스토어를 `phpunit.xml`의 환경 변수로 지정할 수 있습니다.

```xml theme={null}
<?xml version="1.0" encoding="UTF-8"?>
<phpunit colors="true">
    <php>
        <env name="PENNANT_STORE" value="array"/>
    </php>
</phpunit>
```

***

## 커스텀 드라이버

기존 드라이버가 요건에 맞지 않는 경우, 커스텀 드라이버를 작성할 수 있습니다. `Laravel\Pennant\Contracts\Driver` 인터페이스를 구현합니다.

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

namespace App\Extensions;

use Laravel\Pennant\Contracts\Driver;

class RedisFeatureDriver implements Driver
{
    public function define(string $feature, callable $resolver): void {}
    public function defined(): array {}
    public function getAll(array $features): array {}
    public function get(string $feature, mixed $scope): mixed {}
    public function set(string $feature, mixed $scope, mixed $value): void {}
    public function setForAllScopes(string $feature, mixed $value): void {}
    public function delete(string $feature, mixed $scope): void {}
    public function purge(array|null $features): void {}
}
```

서비스 프로바이더의 `boot` 메서드에서 `extend`를 호출해 등록합니다.

```php theme={null}
Feature::extend('redis', function (Application $app) {
    return new RedisFeatureDriver($app->make('redis'), $app->make('events'), []);
});
```

등록 후에는 `config/pennant.php`에서 드라이버를 지정할 수 있습니다.

```php theme={null}
'stores' => [
    'redis' => [
        'driver' => 'redis',
        'connection' => null,
    ],
],
```

***

## 정리

| 하고 싶은 일    | 방법                                           |
| ---------- | -------------------------------------------- |
| 기능 설치      | `composer require laravel/pennant`           |
| 기능 정의      | `Feature::define('name', fn ($user) => ...)` |
| 기능 확인      | `Feature::active('name')`                    |
| Blade에서 확인 | `@feature('name') ... @endfeature`           |
| 값 업데이트     | `Feature::activate('name')` / `deactivate`   |
| 전체 스코프에 적용 | `Feature::activateForEveryone('name')`       |
| 테스트에서 제어   | `Feature::define('name', true)`로 재정의         |
| 스토리지에서 퍼지  | `Feature::purge('name')`                     |

## 다음 단계

<Columns cols={2}>
  <Card title="디버그와 에러 핸들링" icon="circle-x" href="/ko/error-handling">
    애플리케이션의 예외 처리와 리포트의 구조를 배웁니다.
  </Card>

  <Card title="Laravel Pulse" icon="chart-line" href="/ko/pulse">
    애플리케이션의 성능 모니터링 대시보드를 도입합니다.
  </Card>
</Columns>


## Related topics

- [Laravel Pennant 실전 유스케이스](/ko/blog/laravel-pennant.md)
- [Laravel Boost](/ko/boost.md)
- [Laravel Telescope](/ko/telescope.md)
- [Laravel Bluesky](/ko/packages/laravel-bluesky/index.md)
- [Laravel이란](/ko/introduction.md)
