Pennant
업데이트됨번역일: 2026년 9월 30일
이 페이지는 원문이 업데이트되어 번역이 갱신되었습니다.
- 원문 수정
- 2026년 9월 30일
- 번역 갱신
- 2026년 9월 30일
Pennant
소개
Laravel Pennant는 불필요한 것을 걷어낸 심플하고 가벼운 기능 플래그(feature flag) 패키지입니다. 기능 플래그를 사용하면 새로운 애플리케이션 기능을 신중하고 자신 있게 배포할 수 있습니다. A/B 테스트를 위한 새 UI를 부드럽게 굴려보거나, 트렁크 기반 개발(trunk-based development) 전략을 뒷받침하거나, 그 외 여러 상황에서 유용하게 활용할 수 있습니다.
NOTE
트렁크 기반 개발이 익숙하지 않다면, 완성되지 않은 기능이라도 기능 플래그로 감싸서 즉시 main(또는 master) 브랜치에 머지하는 개발 방식이라고 생각하면 됩니다. 실제로 기능을 사용자에게 노출할지는 플래그 값으로 제어하기 때문에, 긴 수명의 기능 브랜치를 유지할 필요가 없어집니다.
설치
먼저 Composer 패키지 매니저를 사용해 Pennant를 프로젝트에 설치합니다.
composer require laravel/pennant다음으로 vendor:publish Artisan 명령어를 사용해 Pennant의 설정 파일과 마이그레이션 파일을 게시(publish)합니다.
php artisan vendor:publish --provider="Laravel\Pennant\PennantServiceProvider"마지막으로 애플리케이션의 데이터베이스 마이그레이션을 실행합니다. 이 과정에서 Pennant가 database 드라이버를 사용할 때 필요한 features 테이블이 생성됩니다.
php artisan migrate설정
패키지 자산을 게시하면 Pennant의 설정 파일이 config/pennant.php에 생성됩니다. 이 설정 파일을 통해 Pennant가 정의된 기능 플래그의 값을 어디에 저장할지 지정할 수 있습니다.
Pennant는 저장 방식으로 array 드라이버를 지원합니다. array 드라이버는 기능 플래그 값을 애플리케이션의 메모리 내 배열에 저장하는 방식으로, 한 요청 동안만 값이 유지됩니다. 또한 Pennant는 database 드라이버를 지원하는데, 이 드라이버는 데이터베이스에 값을 저장하기 때문에 여러 요청에 걸쳐서도 값을 지속적으로 유지합니다. 이것이 기본 저장 방식입니다.
기능 정의하기
기능을 정의할 때는 Feature 파사드가 제공하는 define 메서드를 사용합니다. 기능의 이름과, 기능의 최종 값을 결정하는 데 사용할 클로저를 함께 전달해야 합니다.
일반적으로 기능은 서비스 프로바이더에서 Feature 파사드를 사용해 정의합니다. 클로저는 기능을 확인하는 대상, 즉 "스코프(scope)"를 인자로 전달받습니다. 사용자를 대상으로 스코프를 지정하는 경우가 가장 흔합니다. 아래 예시에서는 사용자들에게 새 API를 점진적으로 배포하는 기능을 정의합니다.
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Lottery;
use Laravel\Pennant\Feature;
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Feature::define('new-api', function (User $user) {
return match (true) {
$user->isInternalTeamMember() => true,
$user->isHighTrafficCustomer() => false,
default => Lottery::odds(1 / 100),
};
});
}
}이 예시에서 확인할 수 있듯 기능에는 다음과 같은 규칙이 적용됩니다.
- 모든 내부 팀원은 새 API를 사용합니다.
- 트래픽이 많은 고객은 새 API를 사용하지 않습니다.
- 나머지 사용자에게는 100분의 1의 확률로 이 기능을 무작위 배포합니다.
특정 사용자가 new-api 기능을 처음 조회하면, 클로저의 결과가 저장소 드라이버에 저장됩니다. 이후 해당 사용자에 대해 다시 기능을 확인할 때는 저장소에 저장되어 있는 값이 그대로 반환되며, 클로저가 다시 호출되지는 않습니다.
간편하게 사용할 수 있도록, 기능 정의에서 스코프 인자가 필요 없을 때는 define 메서드의 클로저를 생략할 수도 있습니다.
Feature::define('site-redesign');이렇게 기능을 정의하면, 아직 해당 기능이 "활성화(active)"되지 않은 사용자에게는 항상 true를 반환합니다.
클래스 기반 기능
Pennant는 클래스 기반으로 기능을 정의하는 방식도 제공합니다. 클로저 기반 정의와 달리 클래스 기반 기능은 서비스 프로바이더에 등록할 필요가 없습니다. 클래스 기반 기능을 만들려면 Artisan의 pennant:feature 명령어를 실행합니다. 기본적으로 이 기능 클래스는 애플리케이션의 app/Features 디렉터리에 생성됩니다.
php artisan pennant:feature NewApi기능 클래스를 작성할 때는 resolve 메서드만 정의하면 됩니다. 이 메서드는 주어진 스코프에 대한 기능의 초기 값을 결정하는 역할을 합니다. 이때도 스코프는 대개 사용자가 됩니다.
<?php
namespace App\Features;
use Illuminate\Support\Lottery;
class NewApi
{
/**
* Resolve the feature's initial value.
*/
public function resolve(User $user): mixed
{
return match (true) {
$user->isInternalTeamMember() => true,
$user->isHighTrafficCustomer() => false,
default => Lottery::odds(1 / 100),
};
}
}NOTE
기능 클래스는 컨테이너를 통해 resolve되므로 필요에 따라 생성자에서 의존성을 주입받을 수 있습니다.
기능의 인스턴스를 확인하려면 Feature 파사드의 using 메서드를 사용합니다.
use Laravel\Pennant\Feature;
Feature::define(NewApi::class);NOTE
기능 클래스는 Serializable을 구현할 필요가 없습니다. Feature::define()에는 클래스명만 전달하면 되며, Pennant가 애플리케이션 서비스 컨테이너를 이용해 해당 클래스를 resolve할 때 처리해 줍니다.
원한다면 기능 클래스에 기본으로 사용될 이름을 명시적으로 지정하지 않고, 클래스명을 그대로 기능 이름으로 사용할 수도 있습니다. Feature::define()에 클래스명을 전달하지 않는다면 Pennant는 클래스의 name 정적 메서드를 자동으로 사용합니다. 이 메서드를 재정의하면 기능 이름을 커스터마이징할 수 있습니다.
<?php
namespace App\Features;
class NewApi
{
/**
* Get the feature's name.
*/
public static function name(): string
{
return 'new-api';
}
// ...
}기능 확인하기
기능이 활성화되어 있는지 확인하려면 Feature 파사드가 제공하는 active 메서드를 사용합니다.
use Laravel\Pennant\Feature;
if (Feature::active('new-api')) {
// ...
}기본적으로 기능은 현재 인증된 사용자를 기준으로 확인됩니다. 그렇기 때문에 앞서 정의한 new-api 기능도, 현재 인증된 사용자가 내부 팀원인지, 트래픽이 많은 고객인지, 아니면 100분의 1의 룰렛에 당첨되었는지에 따라 값이 결정됩니다.
다른 사용자를 대상으로 기능을 확인하고 싶다면 Feature 파사드의 for 메서드를 사용합니다.
Feature::for($user)->active('new-api');Pennant에는 이 밖에도 기능의 상태를 좀 더 상세하게 확인할 수 있는 여러 편의 메서드가 준비되어 있습니다.
// 주어진 기능들이 모두 활성화되었는지 확인합니다...
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']);NOTE
Artisan 명령어나 큐 작업(Job)처럼 HTTP 컨텍스트 바깥에서 Pennant를 사용할 때는, 일반적으로 기능의 스코프를 명시적으로 지정하는 것이 좋습니다. 또는 기본 스코프를 정의해서 인증된 사용자와 비인증 요청 모두를 고려한 스코프를 지정할 수도 있습니다.
클래스 기반 기능 확인하기
클래스 기반 기능이라면 active 메서드에는 클래스명을 전달해야 합니다.
use Laravel\Pennant\Feature;
use App\Features\NewApi;
if (Feature::active(NewApi::class)) {
// ...
}조건부 실행
when 메서드를 사용하면 기능이 활성화된 경우에 실행할 클로저를 직관적으로 지정할 수 있습니다. 또한 두 번째 클로저를 전달하면 기능이 비활성 상태일 때 실행할 코드도 지정할 수 있습니다.
use Laravel\Pennant\Feature;
use App\Features\NewApi;
Feature::when(NewApi::class,
fn () => $this->resolveNewApiResponse(),
fn () => $this->resolveOldApiResponse(),
);when을 사용하면, 앞서 살펴본 것처럼 기능이 비활성 상태일 때 처리할 수동 조건문(if/else)을 직접 작성할 필요가 없어집니다.
when의 반대 역할을 하는 unless 메서드도 존재합니다. 첫 번째 클로저는 기능이 비활성 상태일 때 실행됩니다.
use Laravel\Pennant\Feature;
use App\Features\NewApi;
Feature::unless(NewApi::class,
fn () => $this->resolveOldApiResponse(),
fn () => $this->resolveNewApiResponse(),
);`HasFeatures` 트레이트
Pennant의 HasFeatures 트레이트는 애플리케이션의 User 모델(또는 기능을 가진 다른 모델)에 추가할 수 있으며, 모델에서 직접 기능 관련 메서드에 편리하게 접근할 수 있게 해줍니다.
use Laravel\Pennant\Concerns\HasFeatures;
class User extends Authenticatable
{
use HasFeatures;
// ...
}트레이트를 모델에 추가하면 features 메서드를 호출해 기능을 쉽게 확인할 수 있습니다.
if ($user->features()->active('new-api')) {
// ...
}물론 features 메서드를 통해 그 외 다양한 편의 메서드도 사용할 수 있습니다.
// 값...
$value = $user->features()->value('purchase-button')
$values = $user->features()->values(['new-api', 'purchase-button']);
// 상태...
$user->features()->active('new-api');
$user->features()->allAreActive(['new-api', 'server-api']);
$user->features()->someAreActive(['new-api', 'server-api']);
$user->features()->inactive('new-api');
$user->features()->allAreInactive(['new-api', 'server-api']);
$user->features()->someAreInactive(['new-api', 'server-api']);
// 조건부 실행...
$user->features()->when('new-api',
fn () => /* .. */,
fn () => /* .. */,
);
$user->features()->unless('new-api',
fn () => /* .. */,
fn () => /* .. */,
);Blade 디렉티브
기능 확인을 Blade에서 간편하게 하기 위해, Pennant는 @feature 및 @featureany 디렉티브를 제공합니다.
@feature('site-redesign')
<!-- 'site-redesign' 기능이 활성화됨 -->
@else
<!-- 'site-redesign' 기능이 비활성화됨 -->
@endfeature
@featureany(['site-redesign', 'beta-feature'])
<!-- 'site-redesign' 또는 'beta-feature' 기능 중 하나가 활성화됨 -->
@endfeatureany미들웨어
Pennant는 현재 인증된 사용자가 기능에 접근할 권한이 있는지 확인할 수 있는 미들웨어도 함께 제공합니다. 이 미들웨어를 라우트에 지정한 뒤, 요청 중인 사용자가 기능에 접근 권한이 없을 경우 처리할 기능 이름을 함께 넘겨줄 수 있습니다.
use App\Features\NewApi;
use Illuminate\Support\Facades\Route;
Route::get('/api/servers', function () {
// ...
})->middleware(EnsureFeaturesAreActive::using(NewApi::class));기본적으로 지정한 기능들 중 하나라도 비활성 상태이면, 미들웨어는 사용자를 위한 400 Bad Request HTTP 응답을 자동으로 반환합니다.
미들웨어가 비활성 기능을 만났을 때 처리 방식을 커스터마이징하려면, EnsureFeaturesAreActive 미들웨어가 제공하는 whenInactive 메서드를 사용합니다. 보통 이 메서드는 애플리케이션 서비스 프로바이더의 boot 메서드에서 호출합니다.
use Illuminate\Http\Request;
use Laravel\Pennant\Middleware\EnsureFeaturesAreActive;
EnsureFeaturesAreActive::whenInactive(
function (Request $request, array $features) {
return new Response(status: 403);
}
);기능 확인 가로채기
특정 기능에 대해 저장된 값을 확인하기 전에 실행할 로직을 넣어야 할 때가 있습니다. 예를 들어 새로운 API 기능을 개발하다가, 개발이 마무리되면 기존 API 지원을 걷어내기로 결정했다고 가정해봅니다. 이 시점 이후에는 애플리케이션에 요청이 들어올 때마다 new-api 기능이 항상 true가 되게 하고, 저장되어 있는 값은 서서히 비워버리는 편이 좋습니다.
이럴 때 기능 클래스의 before 메서드를 활용할 수 있습니다. before 메서드는 클래스 기반 기능에서만 사용할 수 있습니다.
<?php
namespace App\Features;
class NewApi
{
/**
* Execute an action before the initial feature resolution.
*/
public static function before(): mixed
{
return true;
}
/**
* Resolve the feature's initial value.
*/
public function resolve(User $user): mixed
{
return match (true) {
$user->isInternalTeamMember() => true,
$user->isHighTrafficCustomer() => false,
default => Lottery::odds(1 / 100),
};
}
}before 메서드에서 null이 아닌 값을 반환하면, 그 요청 동안에는 해당 값이 즉시 사용되며 저장된 값은 조회하지 않습니다. 이렇게 하면 이미 저장되어 있는 모든 값을 손쉽게 지울 수 있습니다.
php artisan pennant:purge new-api인메모리 캐시
기능을 확인할 때 Pennant는 결과를 인메모리 캐시에 저장합니다. database 드라이버를 사용하는 경우, 단일 요청 안에서 동일한 기능 플래그를 다시 확인하더라도 추가적인 쿼리가 발생하지 않는다는 뜻입니다. 이는 요청이 끝날 때까지 기능의 결과가 일관되게 유지된다는 것도 보장해줍니다.
만약 수동으로 인메모리 캐시를 비워야 한다면 Feature 파사드가 제공하는 flushCache 메서드를 사용합니다.
Feature::flushCache();Pennant
소개
Laravel Pennant는 불필요한 기능을 걷어낸 가볍고 간단한 기능 플래그(feature flag) 패키지입니다. 기능 플래그를 사용하면 새로운 애플리케이션 기능을 안심하고 점진적으로 배포할 수 있고, 새로운 UI 디자인을 A/B 테스트할 수 있으며, 트렁크 기반 개발 전략을 보완하는 등 다양한 활용이 가능합니다.
NOTE
기능 플래그란 특정 코드 경로나 기능을 즉시 켜고 끌 수 있게 해주는 스위치라고 생각하면 됩니다. 예를 들어 신규 결제 시스템을 전체 사용자에게 한 번에 배포하는 대신, 먼저 내부 직원이나 일부 베타 사용자에게만 노출한 뒤 문제가 없는지 확인하고 점진적으로 확대할 수 있습니다.
설치
먼저 Composer 패키지 매니저를 이용해 Pennant를 프로젝트에 설치합니다:
composer require laravel/pennant다음으로 vendor:publish Artisan 명령어를 사용해 Pennant의 설정 파일과 마이그레이션 파일을 게시(publish)합니다:
php artisan vendor:publish --provider="Laravel\Pennant\PennantServiceProvider"마지막으로 애플리케이션의 데이터베이스 마이그레이션을 실행합니다. 이 과정에서 Pennant의 database 드라이버가 사용할 features 테이블이 생성됩니다:
php artisan migrateNOTE
database 드라이버 외에도 다른 드라이버를 사용할 계획이라면 마이그레이션 실행을 생략해도 괜찮습니다. 다만 기본 드라이버가 database이므로, 별도의 드라이버를 설정하지 않았다면 위 마이그레이션 단계를 꼭 진행해야 합니다.
Pennant
설정
Pennant의 에셋을 퍼블리시하고 나면 config/pennant.php 설정 파일이 생성됩니다. 이 설정 파일에서는 Pennant가 해석된(resolved) 기능 플래그 값을 저장할 때 사용할 기본 저장 방식을 지정할 수 있습니다.
Pennant는 두 가지 저장 드라이버를 기본으로 제공합니다. array 드라이버는 해석된 기능 플래그 값을 메모리 상의 배열에 저장하며, database 드라이버는 관계형 데이터베이스에 영구적으로 저장합니다. 별도로 설정을 변경하지 않는 한 database 드라이버가 기본 저장 방식으로 사용됩니다.
Feature 정의하기
Feature를 정의하려면 Feature 파사드가 제공하는 define 메서드를 사용하면 됩니다. Feature의 이름과 함께, 해당 Feature의 초기값을 결정하는 클로저를 전달해야 합니다.
일반적으로 Feature는 서비스 프로바이더 안에서 Feature 파사드를 사용해 정의합니다. 클로저는 Feature를 확인할 대상, 즉 "스코프(scope)"를 인자로 전달받습니다. 대부분의 경우 스코프는 현재 인증된 사용자가 됩니다. 아래 예시에서는 애플리케이션의 새로운 API를 사용자들에게 점진적으로 배포하기 위한 Feature를 정의해보겠습니다:
<?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),
});
}
}위 예시에서 우리는 다음과 같은 규칙을 정의했습니다:
- 내부 팀원은 모두 새 API를 사용해야 합니다.
- 트래픽이 많은(high traffic) 고객은 새 API를 사용하지 않아야 합니다.
- 그 외의 경우, 100명 중 1명 꼴로 무작위로 새 API가 활성화됩니다.
특정 사용자에 대해 new-api Feature가 처음 확인될 때, 클로저의 실행 결과가 스토리지 드라이버에 저장됩니다. 이후 동일한 사용자에 대해 다시 Feature를 확인하면, 클로저를 다시 실행하지 않고 저장된 값을 그대로 가져옵니다.
편의를 위해, Feature 정의가 단순히 Lottery 결과만 반환한다면 클로저 자체를 생략할 수도 있습니다:
Feature::define('site-redesign', Lottery::odds(1, 1000));
클래스 기반 Feature
Pennant는 클래스 기반의 Feature 정의도 지원합니다. 클로저 기반 정의와 달리, 클래스 기반 Feature는 서비스 프로바이더에 별도로 등록할 필요가 없습니다. 클래스 기반 Feature를 생성하려면 pennant:feature Artisan 명령어를 실행하면 됩니다. 기본적으로 생성된 Feature 클래스는 애플리케이션의 app/Features 디렉터리에 위치합니다:
php artisan pennant:feature NewApiFeature 클래스를 작성할 때는 resolve 메서드만 정의하면 됩니다. 이 메서드는 주어진 스코프에 대한 Feature의 초기값을 결정할 때 호출됩니다. 이때도 스코프는 보통 현재 인증된 사용자입니다:
<?php
namespace App\Features;
use App\Models\User;
use Illuminate\Support\Lottery;
class NewApi
{
/**
* Feature의 초기값을 결정합니다.
*/
public function resolve(User $user): mixed
{
return match (true) {
$user->isInternalTeamMember() => true,
$user->isHighTrafficCustomer() => false,
default => Lottery::odds(1 / 100),
};
}
}클래스 기반 Feature의 인스턴스를 직접 가져오고 싶다면, Feature 파사드의 instance 메서드를 사용하면 됩니다:
use App\Features\NewApi;
use Laravel\Pennant\Feature;
$instance = Feature::instance(NewApi::class);NOTE
Feature 클래스는 서비스 컨테이너를 통해 resolve되므로, 필요하다면 Feature 클래스의 생성자에 의존성을 주입할 수 있습니다.
저장되는 Feature 이름 커스터마이징하기
기본적으로 Pennant는 Feature 클래스의 전체 네임스페이스가 포함된 클래스명을 그대로 저장합니다. 저장되는 Feature 이름을 애플리케이션의 내부 구조와 분리하고 싶다면, Feature 클래스에 Name 어트리뷰트를 추가하면 됩니다. 이 어트리뷰트에 지정한 값이 클래스명 대신 저장됩니다:
<?php
namespace App\Features;
use Laravel\Pennant\Attributes\Name;
#[Name('new-api')]
class NewApi
{
// ...
}기능 확인하기
기능이 활성화되어 있는지 확인하려면 Feature 파사드의 active 메서드를 사용하면 됩니다. 기본적으로 기능은 현재 인증된 사용자를 기준으로 확인됩니다.
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Laravel\Pennant\Feature;
class PodcastController
{
/**
* 리소스 목록을 표시합니다.
*/
public function index(Request $request): Response
{
return Feature::active('new-api')
? $this->resolveNewApiResponse($request)
: $this->resolveLegacyApiResponse($request);
}
// ...
}기본적으로는 현재 인증된 사용자를 기준으로 기능을 확인하지만, for 메서드를 사용하면 다른 사용자나 스코프를 기준으로도 손쉽게 확인할 수 있습니다.
return Feature::for($user)->active('new-api')
? $this->resolveNewApiResponse($request)
: $this->resolveLegacyApiResponse($request);Pennant는 기능 활성화 여부를 확인할 때 유용하게 쓸 수 있는 몇 가지 편의 메서드도 함께 제공합니다.
// 주어진 기능이 모두 활성화되어 있는지 확인...
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']);NOTE
Artisan 명령어나 큐에 등록된 Job처럼 HTTP 컨텍스트 밖에서 Pennant를 사용할 때는 일반적으로 기능의 스코프를 명시적으로 지정해야 합니다. 또는 인증된 HTTP 컨텍스트와 인증되지 않은 컨텍스트 모두를 처리할 수 있는 기본 스코프를 정의해 두는 방법도 있습니다.
클래스 기반 기능 확인하기
클래스 기반 기능의 경우, 확인 시 클래스 이름을 전달해야 합니다.
<?php
namespace App\Http\Controllers;
use App\Features\NewApi;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Laravel\Pennant\Feature;
class PodcastController
{
/**
* 리소스 목록을 표시합니다.
*/
public function index(Request $request): Response
{
return Feature::active(NewApi::class)
? $this->resolveNewApiResponse($request)
: $this->resolveLegacyApiResponse($request);
}
// ...
}조건부 실행
when 메서드를 사용하면 기능이 활성화되어 있을 때 주어진 클로저를 유연하게 실행할 수 있습니다. 두 번째 클로저를 함께 전달하면 기능이 비활성화되어 있을 때 해당 클로저가 실행됩니다.
<?php
namespace App\Http\Controllers;
use App\Features\NewApi;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Laravel\Pennant\Feature;
class PodcastController
{
/**
* 리소스 목록을 표시합니다.
*/
public function index(Request $request): Response
{
return Feature::when(NewApi::class,
fn () => $this->resolveNewApiResponse($request),
fn () => $this->resolveLegacyApiResponse($request),
);
}
// ...
}unless 메서드는 when 메서드와 반대로 동작하여, 기능이 비활성화되어 있을 때 첫 번째 클로저를 실행합니다.
return Feature::unless(NewApi::class,
fn () => $this->resolveLegacyApiResponse($request),
fn () => $this->resolveNewApiResponse($request),
);`HasFeatures` 트레이트
애플리케이션의 User 모델(또는 기능을 사용하는 다른 모델)에 Pennant의 HasFeatures 트레이트를 추가하면, 모델에서 바로 기능을 편리하게 확인할 수 있습니다.
<?php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Pennant\Concerns\HasFeatures;
class User extends Authenticatable
{
use HasFeatures;
// ...
}트레이트를 모델에 추가한 후에는 features 메서드를 호출해서 간단하게 기능을 확인할 수 있습니다.
if ($user->features()->active('new-api')) {
// ...
}물론 features 메서드는 기능과 상호작용하는 데 유용한 다양한 편의 메서드를 제공합니다.
// 값 조회...
$value = $user->features()->value('purchase-button');
$values = $user->features()->values(['new-api', 'purchase-button']);
// 상태 확인...
$user->features()->active('new-api');
$user->features()->allAreActive(['new-api', 'server-api']);
$user->features()->someAreActive(['new-api', 'server-api']);
$user->features()->inactive('new-api');
$user->features()->allAreInactive(['new-api', 'server-api']);
$user->features()->someAreInactive(['new-api', 'server-api']);
// 조건부 실행...
$user->features()->when('new-api',
fn () => /* ... */,
fn () => /* ... */,
);
$user->features()->unless('new-api',
fn () => /* ... */,
fn () => /* ... */,
);Blade 디렉티브
Blade 템플릿에서도 매끄럽게 기능을 확인할 수 있도록, Pennant는 @feature와 @featureany 디렉티브를 제공합니다.
@feature('site-redesign')
<!-- 'site-redesign'가 활성화된 경우 -->
@else
<!-- 'site-redesign'가 비활성화된 경우 -->
@endfeature
@featureany(['site-redesign', 'beta'])
<!-- 'site-redesign' 또는 'beta' 중 하나라도 활성화된 경우 -->
@endfeatureany미들웨어
Pennant는 라우트가 실행되기도 전에 현재 인증된 사용자가 특정 기능에 접근할 수 있는지 검증하는 미들웨어도 제공합니다. 라우트에 이 미들웨어를 지정하고, 접근에 필요한 기능을 함께 명시할 수 있습니다. 지정한 기능 중 하나라도 현재 인증된 사용자에게 비활성화되어 있다면, 해당 라우트는 400 Bad Request HTTP 응답을 반환합니다. 정적 메서드인 using에는 여러 개의 기능을 전달할 수 있습니다.
use Illuminate\Support\Facades\Route;
use Laravel\Pennant\Middleware\EnsureFeaturesAreActive;
Route::get('/api/servers', function () {
// ...
})->middleware(EnsureFeaturesAreActive::using('new-api', 'servers-api'));응답 커스터마이징
나열된 기능 중 하나가 비활성화되었을 때 미들웨어가 반환하는 응답을 커스터마이징하고 싶다면, EnsureFeaturesAreActive 미들웨어가 제공하는 whenInactive 메서드를 사용하면 됩니다. 일반적으로 이 메서드는 애플리케이션의 서비스 프로바이더 중 하나의 boot 메서드 안에서 호출합니다.
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Laravel\Pennant\Middleware\EnsureFeaturesAreActive;
/**
* 애플리케이션 서비스를 부트스트랩합니다.
*/
public function boot(): void
{
EnsureFeaturesAreActive::whenInactive(
function (Request $request, array $features) {
return new Response(status: 403);
}
);
// ...
}기능 확인 가로채기
저장된 기능 값을 조회하기 전에, 메모리상에서 먼저 어떤 검사를 수행하는 것이 유용할 때가 있습니다. 예를 들어 새로운 API를 기능 플래그 뒤에 두고 개발하는 중이라고 가정해봅시다. 만약 새 API에서 버그를 발견했다면, 저장소에 이미 저장된 기능 값을 잃지 않으면서도 내부 팀원을 제외한 모든 사용자에게 해당 기능을 즉시 비활성화하고 싶을 수 있습니다. 이렇게 임시로 비활성화한 뒤 버그를 수정하고 나서, 원래 기능에 접근 권한이 있던 사용자들에게 다시 기능을 활성화해줄 수 있습니다.
이런 작업은 클래스 기반 기능의 before 메서드로 구현할 수 있습니다. before 메서드가 정의되어 있으면, 저장소에서 값을 조회하기 전에 항상 메모리상에서 먼저 실행됩니다. 이 메서드가 null이 아닌 값을 반환하면, 해당 요청이 처리되는 동안에는 저장된 기능 값 대신 이 반환값이 사용됩니다.
<?php
namespace App\Features;
use App\Models\User;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Lottery;
class NewApi
{
/**
* 저장된 값을 조회하기 전에 항상 메모리상에서 실행되는 검사입니다.
*/
public function before(User $user): mixed
{
if (Config::get('features.new-api.disabled')) {
return $user->isInternalTeamMember();
}
}
/**
* 기능의 초기 값을 결정합니다.
*/
public function resolve(User $user): mixed
{
return match (true) {
$user->isInternalTeamMember() => true,
$user->isHighTrafficCustomer() => false,
default => Lottery::odds(1 / 100),
};
}
}또한 이 기능을 활용하면, 기능 플래그 뒤에 있던 기능을 특정 시점부터 전체 사용자에게 롤아웃하도록 미리 예약해둘 수도 있습니다.
<?php
namespace App\Features;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Config;
class NewApi
{
/**
* 저장된 값을 조회하기 전에 항상 메모리상에서 실행되는 검사입니다.
*/
public function before(User $user): mixed
{
if (Config::get('features.new-api.disabled')) {
return $user->isInternalTeamMember();
}
if (Carbon::parse(Config::get('features.new-api.rollout-date'))->isPast()) {
return true;
}
}
// ...
}인메모리 캐시
기능을 확인할 때마다 Pennant는 그 결과를 메모리상에 캐시해둡니다. database 드라이버를 사용하는 경우, 같은 요청 내에서 동일한 기능 플래그를 여러 번 확인하더라도 추가적인 데이터베이스 쿼리가 발생하지 않는다는 뜻입니다. 이는 또한 한 요청이 처리되는 동안 해당 기능의 결과가 항상 일관되게 유지되도록 보장해줍니다.
인메모리 캐시를 수동으로 비우고 싶다면, Feature 파사드가 제공하는 flushCache 메서드를 사용하면 됩니다.
Feature::flushCache();스코프
스코프 지정하기
지금까지 살펴본 것처럼 기능(feature)은 보통 현재 인증된 사용자를 기준으로 확인합니다. 하지만 항상 이 방식이 적합한 것은 아닙니다. 그래서 Pennant는 Feature 파사드의 for 메서드를 통해 기능을 확인할 대상(스코프)을 원하는 대로 지정할 수 있게 해줍니다:
return Feature::for($user)->active('new-api')
? $this->resolveNewApiResponse($request)
: $this->resolveLegacyApiResponse($request);물론 기능 스코프가 반드시 "사용자"여야 하는 것은 아닙니다. 예를 들어 개별 사용자가 아니라 팀 단위로 새로운 결제 화면을 순차적으로 배포한다고 가정해봅시다. 오래된 팀에게는 더 천천히 롤아웃하고, 신규 팀에게는 더 빠르게 적용하고 싶을 수도 있습니다. 이런 경우 기능 정의 클로저는 다음과 같이 작성할 수 있습니다:
use App\Models\Team;
use Illuminate\Support\Carbon;
use Illuminate\Support\Lottery;
use Laravel\Pennant\Feature;
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);
});위 클로저는 User가 아니라 Team 모델을 인자로 받고 있습니다. 즉, 사용자가 속한 팀을 기준으로 이 기능이 활성화되어 있는지 확인하려면 Feature 파사드의 for 메서드에 팀을 전달하면 됩니다:
if (Feature::for($user->team)->active('billing-v2')) {
return redirect('/billing/v2');
}
// ...전역 스코프
설정된 기본 스코프 리졸버와 무관하게, 전역 스코프를 기준으로 기능을 확인하거나 조작하고 싶다면 globally 메서드를 사용하세요. 이는 애플리케이션 전체에 적용되는 기능 플래그, 예를 들어 일시적인 점검 모드를 켜거나 모든 사용자에게 기능을 롤아웃할 때 유용합니다:
Feature::globally()->active('new-api');
Feature::globally()->activate('new-api');기본 스코프
Pennant가 기능을 확인할 때 사용하는 기본 스코프도 원하는 대로 커스터마이징할 수 있습니다. 예를 들어 애플리케이션의 모든 기능을 현재 인증된 사용자가 아니라 사용자가 속한 팀을 기준으로 확인한다고 가정해봅시다. 매번 Feature::for($user->team)를 호출하는 대신, 팀을 기본 스코프로 지정해두면 편리합니다. 이 설정은 보통 애플리케이션의 서비스 프로바이더에서 수행합니다:
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\ServiceProvider;
use Laravel\Pennant\Feature;
class AppServiceProvider extends ServiceProvider
{
/**
* 애플리케이션 서비스를 부트스트랩합니다.
*/
public function boot(): void
{
Feature::resolveScopeUsing(fn ($driver) => Auth::user()?->team);
// ...
}
}이렇게 설정하면 for 메서드로 스코프를 명시적으로 전달하지 않는 한, 기능 확인 시 현재 인증된 사용자의 팀이 기본 스코프로 사용됩니다:
Feature::active('billing-v2');
// 이제 아래 코드와 동일하게 동작합니다...
Feature::for($user->team)->active('billing-v2');Null 스코프
기능을 확인할 때 전달한 스코프가 null인데, 해당 기능의 정의에서 nullable 타입이나 유니온 타입에 null을 포함하는 방식으로 null을 지원하지 않는다면, Pennant는 자동으로 false를 결과값으로 반환합니다.
따라서 기능에 전달되는 스코프가 null일 가능성이 있고, 그런 경우에도 기능의 값 리졸버가 정상적으로 호출되길 원한다면, 기능 정의에서 이를 명시적으로 처리해줘야 합니다. null 스코프는 Artisan 명령어, 큐 Job, 혹은 인증되지 않은 라우트 안에서 기능을 확인할 때 발생할 수 있습니다. 이런 컨텍스트에서는 보통 인증된 사용자가 없기 때문에 기본 스코프가 null이 됩니다.
기능 스코프를 항상 명시적으로 지정하지 않는다면, 스코프 타입을 "nullable"로 선언하고 기능 정의 로직 안에서 null 스코프 값을 처리해줘야 합니다:
use App\Models\User;use Illuminate\Support\Lottery;use Laravel\Pennant\Feature; Feature::define('new-api', fn (User $user) => match (true) {//Feature::define('new-api', fn (User|null $user) => match (true) {// $user === null => true,// $user->isInternalTeamMember() => true, $user->isHighTrafficCustomer() => false, default => Lottery::odds(1 / 100),});스코프 식별하기
Pennant에 내장된 array와 database 스토리지 드라이버는 모든 PHP 데이터 타입은 물론 Eloquent 모델에 대해서도 스코프 식별자를 올바르게 저장하는 방법을 알고 있습니다. 하지만 서드파티 Pennant 드라이버를 사용하는 경우, 해당 드라이버는 Eloquent 모델이나 애플리케이션의 커스텀 타입에 대한 식별자를 어떻게 저장해야 하는지 알지 못할 수 있습니다.
이런 상황을 위해 Pennant는 애플리케이션에서 Pennant 스코프로 사용되는 객체에 FeatureScopeable 계약(contract)을 구현함으로써, 저장될 스코프 값의 형식을 직접 지정할 수 있게 해줍니다.
예를 들어 하나의 애플리케이션에서 내장 database 드라이버와 서드파티 "Flag Rocket" 드라이버, 이렇게 두 가지 기능 드라이버를 함께 사용한다고 가정해봅시다. "Flag Rocket" 드라이버는 Eloquent 모델을 어떻게 저장해야 할지 모르고, 대신 FlagRocketUser 인스턴스를 요구합니다. 이때 FeatureScopeable 계약에 정의된 toFeatureIdentifier 메서드를 구현하면, 애플리케이션에서 사용하는 각 드라이버에 전달할 저장용 스코프 값을 원하는 대로 커스터마이징할 수 있습니다:
<?php
namespace App\Models;
use FlagRocket\FlagRocketUser;
use Illuminate\Database\Eloquent\Model;
use Laravel\Pennant\Contracts\FeatureScopeable;
class User extends Model implements FeatureScopeable
{
/**
* 주어진 드라이버에 맞게 객체를 기능 스코프 식별자로 변환합니다.
*/
public function toFeatureIdentifier(string $driver): mixed
{
return match($driver) {
'database' => $this,
'flag-rocket' => FlagRocketUser::fromId($this->flag_rocket_id),
};
}
}스코프 직렬화하기
기본적으로 Pennant는 Eloquent 모델과 연관된 기능을 저장할 때 완전한 클래스명(FQCN)을 사용합니다. 만약 이미 Eloquent 모프 맵(morph map)을 사용하고 있다면, Pennant가 저장된 기능 데이터를 애플리케이션 구조와 분리시킬 수 있도록 이 모프 맵을 그대로 활용하게 할 수도 있습니다.
이를 위해서는 서비스 프로바이더에서 Eloquent 모프 맵을 정의한 뒤, Feature 파사드의 useMorphMap 메서드를 호출하면 됩니다:
use Illuminate\Database\Eloquent\Relations\Relation;
use Laravel\Pennant\Feature;
Relation::enforceMorphMap([
'post' => 'App\Models\Post',
'video' => 'App\Models\Video',
]);
Feature::useMorphMap();다채로운 기능 값 (Rich Feature Values)
지금까지는 기능이 "활성" 또는 "비활성"이라는 이진 상태를 갖는 경우를 주로 살펴봤습니다. 하지만 Pennant는 단순한 불리언 값뿐만 아니라 더 다채로운(rich) 값도 저장할 수 있습니다.
예를 들어, 애플리케이션의 "지금 구매하기" 버튼에 세 가지 새로운 색상을 테스트하고 있다고 가정해봅시다. 이 경우 기능 정의에서 true나 false를 반환하는 대신, 문자열을 반환할 수 있습니다:
use Illuminate\Support\Arr;
use Laravel\Pennant\Feature;
Feature::define('purchase-button', fn (User $user) => Arr::random([
'blue-sapphire',
'seafoam-green',
'tart-orange',
]));purchase-button 기능의 값은 value 메서드를 사용해 조회할 수 있습니다:
$color = Feature::value('purchase-button');Pennant에 포함된 Blade 디렉티브를 사용하면 기능의 현재 값에 따라 조건부로 콘텐츠를 렌더링하는 것도 간단합니다:
@feature('purchase-button', 'blue-sapphire')
<!-- 'blue-sapphire'가 활성 상태일 때 -->
@elsefeature('purchase-button', 'seafoam-green')
<!-- 'seafoam-green'이 활성 상태일 때 -->
@elsefeature('purchase-button', 'tart-orange')
<!-- 'tart-orange'가 활성 상태일 때 -->
@endfeatureNOTE
다채로운 값을 사용할 때 한 가지 알아둘 점이 있습니다. 기능의 값이 false가 아닌 그 어떤 값이라도 가지고 있다면, 해당 기능은 "활성" 상태로 간주됩니다.
조건부 실행을 위한 when 메서드를 호출하면, 기능의 다채로운 값이 첫 번째 클로저의 인자로 전달됩니다:
Feature::when('purchase-button',
fn ($color) => /* ... */,
fn () => /* ... */,
);마찬가지로 unless 메서드를 호출할 때는, 기능의 다채로운 값이 두 번째(옵션) 클로저의 인자로 전달됩니다:
Feature::unless('purchase-button',
fn () => /* ... */,
fn ($color) => /* ... */,
);Pennant
여러 기능 한 번에 조회하기
values 메서드를 사용하면 주어진 스코프에 대해 여러 기능의 값을 한 번에 조회할 수 있습니다:
Feature::values(['billing-v2', 'purchase-button']);
// [
// 'billing-v2' => false,
// 'purchase-button' => 'blue-sapphire',
// ]또는 all 메서드를 사용해 주어진 스코프에 정의된 모든 기능의 값을 조회할 수도 있습니다:
Feature::all();
// [
// 'billing-v2' => false,
// 'purchase-button' => 'blue-sapphire',
// 'site-redesign' => true,
// ]다만 클래스 기반 기능은 명시적으로 확인(check)하기 전까지 Pennant가 그 존재를 알지 못한 채 동적으로 등록됩니다. 즉, 현재 요청에서 아직 한 번도 체크되지 않은 클래스 기반 기능은 all 메서드의 결과에 나타나지 않을 수 있습니다.
all 메서드를 사용할 때 기능 클래스가 항상 결과에 포함되도록 하고 싶다면, Pennant의 기능 탐색(feature discovery) 기능을 활용하면 됩니다. 애플리케이션의 서비스 프로바이더 중 한 곳에서 discover 메서드를 호출하는 것으로 시작해 보겠습니다:
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Laravel\Pennant\Feature;
class AppServiceProvider extends ServiceProvider
{
/**
* 애플리케이션 서비스를 부트스트랩합니다.
*/
public function boot(): void
{
Feature::discover();
// ...
}
}discover 메서드는 애플리케이션의 app/Features 디렉터리에 있는 모든 기능 클래스를 등록합니다. 이제 all 메서드는 현재 요청에서 실제로 체크되었는지 여부와 관계없이 이 클래스들을 결과에 포함시킵니다:
Feature::all();
// [
// 'App\Features\NewApi' => true,
// 'billing-v2' => false,
// 'purchase-button' => 'blue-sapphire',
// 'site-redesign' => true,
// ]NOTE
discover 메서드는 애플리케이션이 아직 명시적으로 체크하지 않은 기능 클래스까지 결과에 포함시키고 싶을 때 유용합니다. 예를 들어 관리자 대시보드에서 전체 기능 플래그 목록과 활성화 여부를 한눈에 보여주는 경우를 떠올려 보세요.
Eager Loading (즉시 로딩)
Pennant은 하나의 요청 안에서 이미 확인한 기능 값들을 메모리 캐시에 저장해두지만, 그럼에도 성능 문제가 발생할 수 있는 상황이 있습니다. 이를 해결하기 위해 Pennant는 기능 값을 미리 한꺼번에 불러오는 즉시 로딩(eager loading) 기능을 제공합니다.
예를 들어, 반복문 안에서 기능이 활성화되어 있는지 확인하는 다음과 같은 코드가 있다고 가정해 보겠습니다.
use Laravel\Pennant\Feature;
foreach ($users as $user) {
if (Feature::for($user)->active('notifications-beta')) {
$user->notify(new RegistrationSuccess);
}
}데이터베이스 드라이버를 사용 중이라면, 이 코드는 반복문을 도는 사용자 수만큼 데이터베이스 쿼리를 실행하게 됩니다. 사용자가 수백 명이라면 수백 번의 쿼리가 발생하는 셈이죠. 이런 잠재적인 성능 병목은 load 메서드를 사용해 여러 사용자(또는 스코프)에 대한 기능 값을 한 번에 미리 불러옴으로써 해결할 수 있습니다.
Feature::for($users)->load(['notifications-beta']);
foreach ($users as $user) {
if (Feature::for($user)->active('notifications-beta')) {
$user->notify(new RegistrationSuccess);
}
}이미 로드된 값은 다시 불러오지 않고, 아직 로드되지 않은 값만 불러오고 싶다면 loadMissing 메서드를 사용하면 됩니다.
Feature::for($users)->loadMissing([
'new-api',
'purchase-button',
'notifications-beta',
]);정의된 모든 기능을 한 번에 불러오고 싶다면 loadAll 메서드를 사용할 수 있습니다.
Feature::for($users)->loadAll();NOTE
반복문을 작성할 때는 항상 이러한 N+1 쿼리 문제를 염두에 두는 것이 좋습니다. 특히 대량의 사용자나 스코프를 대상으로 기능 플래그를 검사하는 배치 작업, 알림 발송 로직 등에서는 load 계열 메서드를 습관적으로 적용하는 것이 성능 관리에 큰 도움이 됩니다.
값 업데이트하기
특정 Feature의 값이 처음 해석(resolve)되면, 사용 중인 드라이버는 그 결과를 저장소에 저장합니다. 이는 여러 요청에 걸쳐 사용자에게 일관된 경험을 제공하기 위해 대부분 필요한 동작입니다. 하지만 경우에 따라 저장된 Feature 값을 수동으로 변경하고 싶을 때가 있습니다.
이럴 때는 activate와 deactivate 메서드를 사용해 Feature를 켜거나 끌 수 있습니다:
use Laravel\Pennant\Feature;
// 기본 스코프에 대해 Feature를 활성화합니다...
Feature::activate('new-api');
// 특정 스코프에 대해 Feature를 비활성화합니다...
Feature::for($user->team)->deactivate('billing-v2');activate 메서드의 두 번째 인자로 값을 전달하면, Feature에 원하는 값을 직접 수동으로 지정할 수도 있습니다:
Feature::activate('purchase-button', 'seafoam-green');저장된 Feature 값을 삭제하고 싶다면 forget 메서드를 사용하면 됩니다. 이후 해당 Feature를 다시 확인할 때, Pennant는 Feature 정의로부터 값을 새로 해석합니다:
Feature::forget('purchase-button');일괄 업데이트
저장된 Feature 값을 한 번에 여러 건 업데이트하려면 activateForEveryone과 deactivateForEveryone 메서드를 사용할 수 있습니다.
예를 들어, new-api Feature가 이제 충분히 안정적이라고 판단했고, 결제 화면의 purchase-button 색상도 'seafoam-green'으로 최종 결정했다고 가정해봅시다. 이럴 때는 아래처럼 모든 사용자에 대해 저장된 값을 한 번에 업데이트할 수 있습니다:
use Laravel\Pennant\Feature;
Feature::activateForEveryone('new-api');
Feature::activateForEveryone('purchase-button', 'seafoam-green');반대로, 모든 사용자에 대해 Feature를 비활성화할 수도 있습니다:
Feature::deactivateForEveryone('new-api');NOTE
이 메서드는 Pennant의 저장소 드라이버에 저장되어 있던, 이미 해석된 Feature 값만 업데이트합니다. 애플리케이션에 정의된 Feature 정의 자체도 함께 수정해야 합니다.
Feature 값 완전히 삭제하기 (Purging)
때로는 저장소에서 Feature 관련 데이터를 통째로 지워버리는 것이 유용할 때가 있습니다. 애플리케이션에서 해당 Feature를 완전히 제거했거나, Feature 정의를 변경하여 모든 사용자에게 새로 롤아웃하고 싶을 때 주로 필요한 작업입니다.
purge 메서드를 사용하면 특정 Feature에 대해 저장된 모든 값을 제거할 수 있습니다:
// 단일 Feature 삭제...
Feature::purge('new-api');
// 여러 Feature 한 번에 삭제...
Feature::purge(['new-api', 'purchase-button']);저장소에 있는 모든 Feature를 삭제하고 싶다면, 인자 없이 purge 메서드를 호출하면 됩니다:
Feature::purge();배포 파이프라인의 일부로 Feature 값을 삭제하는 작업이 유용할 수 있으므로, Pennant는 지정한 Feature들을 저장소에서 삭제해주는 pennant:purge Artisan 명령어를 제공합니다:
php artisan pennant:purge new-apiphp artisan pennant:purge new-api purchase-button특정 Feature 목록만 제외하고 나머지를 모두 삭제하는 것도 가능합니다. 예를 들어 "new-api"와 "purchase-button" Feature는 그대로 유지한 채 나머지 모든 Feature를 삭제하고 싶다면, --except 옵션에 해당 Feature 이름을 전달하면 됩니다:
php artisan pennant:purge --except=new-api --except=purchase-button편의를 위해 pennant:purge 명령어는 --except-registered 플래그도 지원합니다. 이 플래그를 사용하면 서비스 프로바이더에 명시적으로 등록된 Feature를 제외한 나머지 모든 Feature가 삭제됩니다:
php artisan pennant:purge --except-registeredPennant
테스트
기능 플래그와 상호작용하는 코드를 테스트할 때, 테스트에서 기능 플래그가 반환하는 값을 제어하는 가장 쉬운 방법은 단순히 해당 기능을 다시 정의하는 것입니다. 예를 들어 애플리케이션의 서비스 프로바이더 중 하나에 다음과 같은 기능이 정의되어 있다고 가정해봅시다.
use Illuminate\Support\Arr;
use Laravel\Pennant\Feature;
Feature::define('purchase-button', fn () => Arr::random([
'blue-sapphire',
'seafoam-green',
'tart-orange',
]));테스트에서 이 기능이 반환하는 값을 바꾸고 싶다면, 테스트 시작 부분에서 해당 기능을 다시 정의하면 됩니다. 서비스 프로바이더에는 여전히 Arr::random()을 사용하는 구현이 남아 있지만, 아래 테스트는 항상 통과합니다.
Pest
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');
});PHPUnit
use Laravel\Pennant\Feature;
public function test_it_can_control_feature_values()
{
Feature::define('purchase-button', 'seafoam-green');
$this->assertSame('seafoam-green', Feature::value('purchase-button'));
}같은 방식을 클래스 기반 기능에도 사용할 수 있습니다.
Pest
use Laravel\Pennant\Feature;
test('it can control feature values', function () {
Feature::define(NewApi::class, true);
expect(Feature::value(NewApi::class))->toBeTrue();
});PHPUnit
use App\Features\NewApi;
use Laravel\Pennant\Feature;
public function test_it_can_control_feature_values()
{
Feature::define(NewApi::class, true);
$this->assertTrue(Feature::value(NewApi::class));
}기능이 Lottery 인스턴스를 반환하는 경우라면, 테스트에 유용한 헬퍼를 활용할 수 있습니다.
스토어 설정
애플리케이션의 phpunit.xml 파일에 PENNANT_STORE 환경 변수를 정의하면, 테스트 시 Pennant가 사용할 스토어를 지정할 수 있습니다.
<?xml version="1.0" encoding="UTF-8"?>
<phpunit colors="true">
<!-- ... -->
<php>
<env name="PENNANT_STORE" value="array"/>
<!-- ... -->
</php>
</phpunit>NOTE
테스트 환경에서는 매 테스트마다 상태가 초기화되도록 array 드라이버를 사용하는 것이 일반적이며, 이렇게 하면 이전 테스트에서 저장된 기능 값이 다음 테스트에 영향을 주지 않습니다.
Pennant
드라이버 구현하기
Pennant에서 기본으로 제공하는 스토리지 드라이버가 애플리케이션의 요구사항에 맞지 않는다면, 직접 커스텀 스토리지 드라이버를 작성할 수 있습니다. 커스텀 드라이버는 Laravel\Pennant\Contracts\Driver 인터페이스를 구현해야 합니다.
<?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 {}
}이제 Redis 커넥션을 사용해 각 메서드를 구현하면 됩니다. 각 메서드를 어떻게 구현하면 되는지 참고하려면 Pennant 소스 코드에 있는 Laravel\Pennant\Drivers\DatabaseDriver를 살펴보시길 권장합니다.
NOTE
Laravel은 확장 클래스를 담을 디렉토리를 기본으로 제공하지 않습니다. 원하는 위치에 자유롭게 배치하면 됩니다. 이 예제에서는 RedisFeatureDriver를 담기 위해 Extensions 디렉토리를 새로 만들었습니다.
드라이버 등록하기
드라이버 구현이 끝났다면, 이제 Laravel에 이를 등록할 차례입니다. Pennant에 새로운 드라이버를 추가하려면 Feature 파사드가 제공하는 extend 메서드를 사용하면 됩니다. extend 메서드는 애플리케이션의 서비스 프로바이더 중 하나의 boot 메서드에서 호출해야 합니다.
<?php
namespace App\Providers;
use App\Extensions\RedisFeatureDriver;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\ServiceProvider;
use Laravel\Pennant\Feature;
class AppServiceProvider extends ServiceProvider
{
/**
* 애플리케이션 서비스를 등록합니다.
*/
public function register(): void
{
// ...
}
/**
* 애플리케이션 서비스를 부트스트랩합니다.
*/
public function boot(): void
{
Feature::extend('redis', function (Application $app) {
return new RedisFeatureDriver($app->make('redis'), $app->make('events'), []);
});
}
}드라이버를 등록했다면, 애플리케이션의 config/pennant.php 설정 파일에서 redis 드라이버를 사용할 수 있습니다.
'stores' => [
'redis' => [
'driver' => 'redis',
'connection' => null,
],
// ...
],기능을 외부에서 정의하기
만약 커스텀 드라이버가 서드파티 기능 플래그 플랫폼을 감싸는 래퍼(wrapper) 형태라면, 대부분의 경우 Pennant의 Feature::define 메서드 대신 해당 플랫폼에서 기능을 정의하게 될 것입니다. 이런 경우라면 커스텀 드라이버는 Laravel\Pennant\Contracts\DefinesFeaturesExternally 인터페이스도 함께 구현해야 합니다.
<?php
namespace App\Extensions;
use Laravel\Pennant\Contracts\Driver;
use Laravel\Pennant\Contracts\DefinesFeaturesExternally;
class FeatureFlagServiceDriver implements Driver, DefinesFeaturesExternally
{
/**
* 주어진 스코프에 대해 정의된 기능 목록을 가져옵니다.
*/
public function definedFeaturesForScope(mixed $scope): array {}
/* ... */
}definedFeaturesForScope 메서드는 전달받은 스코프에 대해 정의된 기능 이름의 목록을 반환해야 합니다.
이벤트
Pennant는 애플리케이션 전반에서 기능 플래그를 추적할 때 유용하게 활용할 수 있는 다양한 이벤트를 발생시킵니다.
Laravel\Pennant\Events\FeatureRetrieved
이 이벤트는 기능이 확인될 때마다 발생합니다. 애플리케이션 전반에서 기능 플래그의 사용 현황에 대한 지표를 생성하고 추적하는 데 유용하게 사용할 수 있습니다.
Laravel\Pennant\Events\FeatureResolved
이 이벤트는 특정 스코프에 대해 기능의 값이 처음으로 확인(resolve)될 때 발생합니다.
Laravel\Pennant\Events\UnknownFeatureResolved
이 이벤트는 특정 스코프에 대해 알 수 없는(정의되지 않은) 기능이 처음으로 확인될 때 발생합니다. 기능 플래그를 제거하려고 했지만 애플리케이션 곳곳에 실수로 참조 코드가 남아있는 경우, 이 이벤트를 리스닝하면 이를 찾아내는 데 도움이 됩니다.
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log;
use Laravel\Pennant\Events\UnknownFeatureResolved;
class AppServiceProvider extends ServiceProvider
{
/**
* 애플리케이션 서비스를 부트스트랩합니다.
*/
public function boot(): void
{
Event::listen(function (UnknownFeatureResolved $event) {
Log::error("알 수 없는 기능 [{$event->feature}]을(를) 확인하려고 시도했습니다.");
});
}
}Laravel\Pennant\Events\DynamicallyRegisteringFeatureClass
이 이벤트는 요청 처리 중 클래스 기반 기능이 처음으로 동적 확인될 때 발생합니다.
Laravel\Pennant\Events\UnexpectedNullScopeEncountered
이 이벤트는 null 스코프를 지원하지 않는 기능 정의에 null 스코프가 전달될 때 발생합니다.
이러한 상황은 기본적으로 문제없이 처리되며, 기능은 false를 반환합니다. 하지만 이 기본 동작 대신 다른 처리를 원한다면, 애플리케이션의 AppServiceProvider의 boot 메서드에서 이 이벤트에 대한 리스너를 등록할 수 있습니다.
use Illuminate\Support\Facades\Log;
use Laravel\Pennant\Events\UnexpectedNullScopeEncountered;
/**
* 애플리케이션 서비스를 부트스트랩합니다.
*/
public function boot(): void
{
Event::listen(UnexpectedNullScopeEncountered::class, fn () => abort(500));
}Laravel\Pennant\Events\FeatureUpdated
이 이벤트는 보통 activate 또는 deactivate를 호출하여 특정 스코프에 대한 기능을 업데이트할 때 발생합니다.
Laravel\Pennant\Events\FeatureUpdatedForAllScopes
이 이벤트는 보통 activateForEveryone 또는 deactivateForEveryone을 호출하여 모든 스코프에 대한 기능을 업데이트할 때 발생합니다.
Laravel\Pennant\Events\FeatureDeleted
이 이벤트는 보통 forget을 호출하여 특정 스코프에 대한 기능을 삭제할 때 발생합니다.
Laravel\Pennant\Events\FeaturesPurged
이 이벤트는 특정 기능들을 삭제(purge)할 때 발생합니다.
Laravel\Pennant\Events\AllFeaturesPurged
이 이벤트는 모든 기능을 삭제(purge)할 때 발생합니다.