본문 바로가기

Pennant

업데이트됨

번역일: 2026년 9월 17일

이 페이지는 원문이 업데이트되어 번역이 갱신되었습니다.

원문 수정
2026년 9월 17일
번역 갱신
2026년 9월 17일

Pennant

소개

Laravel Pennant는 군더더기 없이 가벼운 기능 플래그(feature flag) 패키지입니다. 기능 플래그를 사용하면 새로운 애플리케이션 기능을 자신 있게 점진적으로 배포할 수 있습니다. 새 UI를 A/B 테스트하거나, 트렁크 기반 개발(trunk-based development) 방식을 채택하거나, 베타 사용자에게 기능을 먼저 공개하는 등 다양한 방식으로 활용할 수 있습니다.

NOTE

예를 들어 신규 결제 플로우를 전체 사용자에게 한 번에 배포하는 대신, Pennant를 이용해 특정 사용자 그룹에게만 먼저 노출한 뒤 문제가 없는지 확인하고 점진적으로 확대할 수 있습니다. 코드를 배포하는 것과 기능을 "켜는" 것을 분리해서 생각하면 이해하기 쉽습니다.

설치

먼저 Composer 패키지 관리자를 사용해 Pennant를 프로젝트에 설치합니다.

composer require laravel/pennant

다음으로 vendor:publish Artisan 명령어를 사용해 Pennant의 설정 파일과 마이그레이션 파일을 게시합니다.

php artisan vendor:publish --provider="Laravel\Pennant\PennantServiceProvider"

마지막으로 애플리케이션의 데이터베이스 마이그레이션을 실행합니다. 이 과정에서 Pennant가 database 드라이버를 사용할 때 필요로 하는 features 테이블이 생성됩니다.

php artisan migrate

설정

패키지 파일을 게시하고 나면 config/pennant.php 설정 파일이 애플리케이션 루트의 config 디렉터리에 생성됩니다. 이 설정 파일을 통해 Pennant가 기능 플래그 값을 저장할 때 사용할 기본 저장 방식을 지정할 수 있습니다.

Pennant는 인메모리 배열에 값을 저장하는 array 드라이버와, 관계형 데이터베이스에 저장하는 database 드라이버(기본값)를 지원합니다.

기능 정의하기

기능을 정의하려면 Feature 파사드가 제공하는 define 메서드를 사용합니다. 기능의 이름과, 사용자에게 해당 기능의 초기 값을 결정하는 클로저를 전달해야 합니다.

일반적으로 기능은 서비스 프로바이더에서 Feature 파사드를 통해 정의합니다. 클로저는 기능을 확인하는 대상, 즉 "스코프(scope)"를 전달받습니다. 보통 스코프는 현재 인증된 사용자입니다. 아래 예제에서는 신규 결제 화면을 사용자 대상으로 점진적으로 배포하는 기능을 정의합니다.

<?php namespace App\Providers; use App\Models\User; use Illuminate\Support\ServiceProvider; use Illuminate\Support\Lottery; 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->isSubscriber() => Lottery::odds(1 / 100), default => false, }); } }

이 예시에서 살펴볼 규칙을 정리하면 다음과 같습니다.

  • 사내 구성원에게는 항상 해당 기능을 활성화합니다.
  • 유료 구독자에게는 1/100 확률로 활성화합니다.
  • 나머지 사용자에게는 기능을 비활성화합니다.

new-api 기능을 처음으로 확인할 때, 클로저의 결과값이 스토리지 드라이버에 저장됩니다. 다음번에 동일한 사용자에 대해 기능을 확인하면 스토리지에 저장된 값이 그대로 조회되며, 클로저는 다시 실행되지 않습니다.

편의를 위해, 기능 정의가 스코프 확인만 반환하는 경우에는 클로저를 생략할 수도 있습니다.

Feature::define('site-redesign', fn (User $user) => Lottery::odds(1 / 100)); // 위 코드는 아래와 동일하게 작성할 수 있습니다... Feature::define('site-redesign', Lottery::odds(1 / 100));

클래스 기반 기능

Pennant는 클래스 기반으로 기능을 정의하는 방법도 지원합니다. 클로저 기반 정의와 달리 클래스를 서비스 프로바이더에 등록할 필요가 없습니다. 클래스 기반 기능을 만들려면 pennant:feature Artisan 명령어를 실행합니다. 기본적으로 기능 클래스는 애플리케이션의 app/Features 디렉터리에 생성됩니다.

php artisan pennant:feature NewApi

기능 클래스를 작성할 때는 resolve 메서드만 정의하면 됩니다. 이 메서드는 주어진 스코프에 대한 기능의 초기 값을 해석하는 역할을 합니다. 물론 이 스코프 역시 대부분 현재 인증된 사용자입니다.

<?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->isSubscriber() => Lottery::odds(1 / 100), default => false, }; } }

NOTE

기능 클래스는 컨테이너를 통해 해석되므로, 필요하다면 기능 클래스의 생성자에 의존성을 주입할 수 있습니다.

기능 이름 커스터마이징

기본적으로 기능 클래스는 클래스명을 기능 이름으로 사용해 스토리지에 저장됩니다. 애플리케이션 내부에서 사용하는 기능 이름을 다르게 지정하고 싶다면, 클래스에 $name 속성을 정의할 수 있습니다. 이 속성 값이 클래스명 대신 기능 이름으로 사용됩니다.

<?php namespace App\Features; class NewApi { /** * 기능의 이름 * * @var string */ public $name = 'new-api'; // ... }

기능 확인하기

기능이 활성화되어 있는지 확인하려면 Feature 파사드가 제공하는 active 메서드를 사용합니다. 기본적으로 기능은 현재 인증된 사용자를 기준으로 확인됩니다.

<?php namespace App\Http\Controllers; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Laravel\Pennant\Feature; class PodcastController extends Controller { /** * 새로운 팟캐스트를 업로드합니다. */ public function store(Request $request): RedirectResponse { if (Feature::active('new-api')) { // ... } // ... } }

기본적으로 기능은 현재 인증된 사용자를 기준으로 확인되지만, 다른 사용자나 스코프에 대해 기능을 확인하고 싶을 때도 있습니다. 이 경우 Feature 파사드가 제공하는 for 메서드를 사용해 확인할 수 있습니다.

return Feature::for($user)->active('new-api');

Pennant는 주어진 스코프에 대해 기능이 비활성화되어 있는지 손쉽게 확인할 수 있는 inactive 메서드도 제공합니다.

return Feature::inactive('new-api');

allAreActive 메서드를 사용하면 주어진 여러 기능이 모두 활성화되어 있는지 한 번에 확인할 수 있습니다.

return Feature::allAreActive(['new-api', 'site-redesign']);

allAreInactive 메서드를 사용하면 주어진 여러 기능이 모두 비활성화되어 있는지 확인할 수 있습니다.

return Feature::allAreInactive(['new-api', 'site-redesign']);

someAreActive 메서드를 사용하면 주어진 여러 기능 중 하나라도 활성화되어 있는지 확인할 수 있습니다.

return Feature::someAreActive(['new-api', 'site-redesign']);

NOTE

Feature 파사드 대신 HTTP 요청이나 Blade 등의 외부 컨텍스트에서 기능을 확인하고 싶다면, Pennant가 제공하는 다양한 다른 방법들을 함께 참고하시기 바랍니다.

조건부 실행

when 메서드를 사용하면 주어진 기능이 활성화되어 있을 때 실행할 클로저를 유연하게 지정할 수 있습니다. 추가로 두 번째 클로저를 전달하면 기능이 비활성화된 경우에 실행됩니다.

<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Http\Response; use Laravel\Pennant\Feature; class PodcastController extends Controller { /** * 새로운 팟캐스트를 업로드합니다. */ public function store(Request $request): Response { return Feature::when('new-api', fn () => $this->resolveNewApiResponse($request), fn () => $this->resolveLegacyApiResponse($request), ); } // ... }

when 메서드와 반대 동작을 하는 unless 메서드도 사용할 수 있습니다. 이 메서드는 기능이 비활성화되어 있을 때 첫 번째 클로저를 실행합니다.

return Feature::unless('new-api', fn () => $this->resolveLegacyApiResponse($request), fn () => $this->resolveNewApiResponse($request), );

`HasFeatures` 트레이트

Pennant의 HasFeatures 트레이트를 애플리케이션의 User 모델(또는 기능을 가진 다른 모델)에 추가하면, 모델에서 직접 기능을 편리하게 확인할 수 있습니다.

<?php namespace App\Models; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Laravel\Pennant\Concerns\HasFeatures; class User extends Authenticatable { use HasFeatures, Notifiable; // ... }

이 트레이트를 모델에 추가하면 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 디렉티브를 제공합니다.

@feature('site-redesign') <!-- 'site-redesign'이 활성화된 경우 --> @else <!-- 'site-redesign'이 비활성화된 경우 --> @endfeature

미들웨어

Pennant는 라우트를 진입하기 전에 현재 인증된 사용자가 특정 기능을 가지고 있는지 검증할 수 있는 미들웨어도 제공합니다. 라우트에 이 미들웨어를 지정해두면, 지정된 기능 중 어느 하나라도 사용자에게 비활성화되어 있을 경우 HttpException이 발생해 403 응답을 반환하도록 설정할 수 있습니다. EnsureFeaturesAreActive 미들웨어의 using 정적 메서드를 사용하면 여러 개의 기능을 전달할 수 있습니다.

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 Laravel\Pennant\Middleware\EnsureFeaturesAreActive; /** * 애플리케이션 서비스를 부트스트랩합니다. */ public function boot(): void { EnsureFeaturesAreActive::whenInactive( function (Request $request, array $features) { return new Response(status: 403); } ); // ... }

기능 확인 가로채기

특정 기능에 대한 저장된 값을 조회하기 전에 인메모리 확인을 먼저 수행하고 싶은 경우가 있습니다. 예를 들어 새로운 API를 점진적으로 배포하는 동안에는 데이터베이스에서 기능 플래그를 조회하는 부하를 없애기 위해, 헤더에 X-Pennant-Beta 값이 있는 요청은 항상 새 API를 사용하도록 처리하고 싶을 수 있습니다.

Feature 파사드의 before 메서드를 사용하면 저장된 값을 조회하기 전에 실행할 클로저를 등록할 수 있습니다. 이 클로저는 보통 애플리케이션 서비스 프로바이더의 boot 메서드에서 등록합니다.

use Illuminate\Support\Facades\Request; /** * 애플리케이션 서비스를 부트스트랩합니다. */ public function boot(): void { Feature::before('new-api', function () { return Request::hasHeader('X-Pennant-Beta') ? true : null; }); // ... }

before 클로저가 null이 아닌 값을 반환하면, 요청 전체 기간 동안 해당 값이 기능의 결과로 사용되며 저장된 값 조회는 건너뜁니다. 전역에서 실행할 검사를 편리하게 등록하고 싶다면 globalBefore 메서드를 사용할 수 있습니다.

Feature::globalBefore(function (string $feature, mixed $scope) { if ($feature === 'new-api' && $scope instanceof User) { return Request::hasHeader('X-Pennant-Beta') ? true : null; } });

인메모리 캐시

기능을 확인할 때 Pennant는 결과를 인메모리 캐시에 저장합니다. database 드라이버를 사용하는 경우, 하나의 요청 안에서 동일한 기능 플래그를 반복적으로 확인해도 추가 데이터베이스 쿼리가 발생하지 않는다는 뜻입니다. 이를 통해 요청이 끝날 때까지 기능의 결과가 일관되게 유지됩니다.

수동으로 인메모리 캐시를 초기화하고 싶다면 Feature 파사드가 제공하는 flushCache 메서드를 사용할 수 있습니다.

Feature::flushCache();

Pennant

소개

Laravel Pennant는 불필요한 기능 없이 가볍고 간단하게 사용할 수 있는 피처 플래그(feature flag) 패키지입니다. 피처 플래그를 사용하면 새로운 애플리케이션 기능을 점진적으로, 안전하게 배포할 수 있고, 새로운 UI 디자인에 대한 A/B 테스트를 진행하거나, 트렁크 기반 개발(trunk-based development) 전략을 보완하는 등 다양한 방식으로 활용할 수 있습니다.

NOTE

피처 플래그가 낯선 개념이라면, "특정 조건(사용자, 그룹, 환경 등)에 따라 코드의 특정 부분을 켜고 끌 수 있는 스위치"라고 생각하면 이해하기 쉽습니다. 예를 들어 신규 결제 시스템을 전체 사용자에게 한 번에 배포하는 대신, 일부 베타 사용자에게만 먼저 노출시켜 안정성을 검증한 뒤 점진적으로 확대 적용할 수 있습니다.

Pennant

설치

먼저 Composer 패키지 매니저를 사용해 프로젝트에 Pennant를 설치합니다:

composer require laravel/pennant

다음으로, vendor:publish Artisan 명령어를 사용해 Pennant의 설정 파일과 마이그레이션 파일을 게시합니다:

php artisan vendor:publish --provider="Laravel\Pennant\PennantServiceProvider"

마지막으로 애플리케이션의 데이터베이스 마이그레이션을 실행합니다. 이 과정에서 Pennant의 database 드라이버가 사용할 features 테이블이 생성됩니다:

php artisan migrate

NOTE

vendor:publish 명령어로 게시되는 설정 파일(config/pennant.php)을 통해 기본 저장 드라이버 등을 원하는 대로 조정할 수 있습니다. 자세한 내용은 바로 다음 섹션에서 다룹니다.

Pennant

설정

Pennant의 에셋을 퍼블리시하고 나면 설정 파일이 config/pennant.php에 생성됩니다. 이 설정 파일에서 Pennant가 계산된 기능 플래그 값을 저장할 때 사용할 기본 저장소 방식을 지정할 수 있습니다.

Pennant는 array 드라이버를 통해 계산된 기능 플래그 값을 메모리 배열에 저장하는 방식을 지원합니다. 또는 database 드라이버를 사용하여 관계형 데이터베이스에 값을 영구적으로 저장할 수도 있는데, 이 방식이 Pennant의 기본 저장소입니다.

NOTE

array 드라이버는 요청이 끝나면 저장된 값이 사라지므로, 주로 테스트 환경이나 임시 확인 용도로 적합합니다. 실제 운영 환경에서는 값을 영속적으로 유지할 수 있는 database 드라이버 사용을 권장합니다.

기능 정의하기

기능을 정의하려면 Feature 파사드가 제공하는 define 메서드를 사용합니다. 기능의 이름과 함께, 해당 기능의 초기값을 계산하는 클로저를 전달해야 합니다.

일반적으로 기능은 서비스 프로바이더 안에서 Feature 파사드를 이용해 정의합니다. 클로저는 기능 확인 시 사용할 "스코프(scope)"를 인자로 전달받는데, 대부분의 경우 이 스코프는 현재 인증된 사용자가 됩니다. 아래 예제에서는 애플리케이션 사용자를 대상으로 새로운 API를 점진적으로 배포하기 위한 기능을 정의해보겠습니다.

<?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를 사용하지 않습니다.
  • 그 외의 경우에는 1/100의 확률로 무작위로 기능이 활성화됩니다.

특정 사용자에 대해 new-api 기능을 처음 확인하는 시점에 클로저가 실행되고, 그 결과값이 스토리지 드라이버에 저장됩니다. 이후 같은 사용자에 대해 다시 기능을 확인하면 저장된 값을 그대로 가져오며, 클로저는 다시 실행되지 않습니다.

편의를 위해, 기능 정의가 단순히 로터리(lottery) 값만 반환한다면 클로저를 아예 생략할 수도 있습니다.

Feature::define('site-redesign', Lottery::odds(1, 1000));

클래스 기반 기능

Pennant는 클래스 기반으로 기능을 정의하는 방법도 지원합니다. 클로저 기반 정의와 달리, 클래스 기반 기능은 서비스 프로바이더에 별도로 등록할 필요가 없습니다. 클래스 기반 기능을 만들려면 pennant:feature Artisan 명령어를 실행하면 됩니다. 기본적으로 기능 클래스는 애플리케이션의 app/Features 디렉터리에 생성됩니다.

php artisan pennant:feature NewApi

기능 클래스를 작성할 때는 resolve 메서드 하나만 정의하면 됩니다. 이 메서드는 주어진 스코프에 대한 기능의 초기값을 계산할 때 호출되며, 이때도 스코프는 보통 현재 인증된 사용자입니다.

<?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), }; } }

클래스 기반 기능의 인스턴스를 직접 얻고 싶다면, Feature 파사드의 instance 메서드를 사용할 수 있습니다.

use App\Features\NewApi; use Laravel\Pennant\Feature; $instance = Feature::instance(NewApi::class);

NOTE

기능 클래스는 컨테이너를 통해 해석(resolve)되므로, 필요하다면 기능 클래스의 생성자에 의존성을 주입받을 수 있습니다.

저장되는 기능 이름 커스터마이징하기

기본적으로 Pennant는 기능 클래스의 전체 네임스페이스가 포함된 클래스명을 그대로 저장합니다. 저장되는 기능 이름을 애플리케이션의 내부 구조와 분리하고 싶다면, 기능 클래스에 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); } // ... }

기본적으로는 현재 인증된 사용자를 대상으로 기능을 확인하지만, Feature 파사드의 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를 언제든 끌 수 있는 방법이 필요하다고 가정해봅시다. 새 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)은 기본적으로 현재 인증된 사용자를 기준으로 검사됩니다. 하지만 항상 이 방식이 적합한 것은 아닙니다. 이럴 때는 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을 지원하지 않는다면 Pennant는 자동으로 false를 결과값으로 반환합니다.

따라서 기능에 전달되는 스코프가 null일 가능성이 있고, 그 경우에도 기능의 값 리졸버가 정상적으로 호출되기를 원한다면 기능 정의 시 이를 반드시 고려해야 합니다. Artisan 명령어, 큐에 등록된 Job, 또는 인증되지 않은 라우트에서 기능을 검사하면 스코프가 null이 되는 경우가 흔합니다. 이런 컨텍스트에서는 인증된 사용자가 없는 경우가 많아 기본 스코프가 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는 스코프로 사용되는 객체에 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 모델과 연결된 기능을 저장할 때 완전한 네임스페이스가 포함된 클래스명을 사용합니다. 만약 이미 Eloquent 다형성 매핑(morph map)을 사용하고 있다면, Pennant도 이 morph map을 활용해 저장되는 기능 데이터를 애플리케이션 구조와 분리시킬 수 있습니다.

이를 적용하려면, 서비스 프로바이더에서 Eloquent morph map을 정의한 후 Feature 파사드의 useMorphMap 메서드를 호출하면 됩니다:

use Illuminate\Database\Eloquent\Relations\Relation; use Laravel\Pennant\Feature; Relation::enforceMorphMap([ 'post' => 'App\Models\Post', 'video' => 'App\Models\Video', ]); Feature::useMorphMap();

Pennant

풍부한 값을 가지는 기능(Rich Feature Values)

지금까지는 기능이 "활성" 또는 "비활성"이라는 두 가지 상태만 갖는 것처럼 설명했지만, Pennant는 단순한 참/거짓 값 외에도 훨씬 풍부한 값을 저장할 수 있습니다.

예를 들어, 애플리케이션의 "지금 구매하기" 버튼에 세 가지 색상을 테스트하고 싶다고 가정해 보겠습니다. 이런 경우 기능 정의에서 truefalse를 반환하는 대신, 아래와 같이 문자열을 반환할 수 있습니다:

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'가 활성화된 경우 --> @endfeature

NOTE

풍부한 값을 사용할 때 알아두어야 할 점이 있습니다. 기능의 값이 false가 아닌 어떤 값이라도 가지고 있으면 해당 기능은 "활성" 상태로 간주됩니다.

조건부 실행에 사용하는 when 메서드를 호출하면, 기능의 값이 첫 번째 클로저에 전달됩니다:

Feature::when('purchase-button', fn ($color) => /* ... */, fn () => /* ... */, );

마찬가지로 unless 메서드를 호출하면, 기능의 값이 두 번째(옵션) 클로저에 전달됩니다:

Feature::unless('purchase-button', fn () => /* ... */, fn ($color) => /* ... */, );

여러 기능값 조회하기

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의 기능 탐색(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 메서드는 애플리케이션이 부팅될 때마다 app/Features 디렉터리를 스캔하기 때문에, 프로덕션 환경에서는 설정 캐싱과 함께 사용해 성능 저하를 방지하는 것이 좋습니다.

Eager Loading (사전 로딩)

Pennant은 하나의 요청 내에서 이미 확인한 기능들을 메모리 캐시에 저장해두기 때문에 같은 요청 안에서는 동일한 기능 조회가 중복 실행되지 않습니다. 하지만 이것만으로는 충분하지 않은 경우가 있습니다. 대표적으로 반복문(loop) 안에서 여러 스코프에 대해 기능을 확인하는 경우인데, 이럴 때는 여전히 성능 문제가 발생할 수 있습니다. 이를 해결하기 위해 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 쿼리 문제처럼 성능에 큰 영향을 줄 수 있습니다.

값 업데이트하기

기능의 값이 처음 확인(resolve)되면, 사용 중인 드라이버는 그 결과를 스토리지에 저장합니다. 이는 여러 요청에 걸쳐 사용자에게 일관된 경험을 제공하기 위해 대부분 필요한 동작입니다. 하지만 때로는 저장된 기능 값을 수동으로 업데이트하고 싶을 때가 있습니다.

이럴 때는 activatedeactivate 메서드를 사용해 기능을 켜거나 끌 수 있습니다:

use Laravel\Pennant\Feature; // 기본 스코프에 대해 기능을 활성화합니다... Feature::activate('new-api'); // 지정한 스코프에 대해 기능을 비활성화합니다... Feature::for($user->team)->deactivate('billing-v2');

activate 메서드에 두 번째 인자를 전달하면, 단순한 불리언 값이 아니라 원하는 값(rich value)을 직접 지정할 수도 있습니다:

Feature::activate('purchase-button', 'seafoam-green');

저장된 기능 값을 잊어버리도록(초기화하도록) 지시하려면 forget 메서드를 사용하세요. 이후 해당 기능을 다시 확인하면, Pennant는 기능 정의(definition)로부터 값을 다시 계산합니다:

Feature::forget('purchase-button');

일괄 업데이트

저장된 기능 값을 한꺼번에 업데이트하려면 activateForEveryonedeactivateForEveryone 메서드를 사용할 수 있습니다.

예를 들어, new-api 기능의 안정성이 충분히 검증되어, 결제 화면에 사용할 purchase-button 색상을 seafoam-green으로 최종 확정했다고 가정해봅시다. 이 경우 모든 사용자에 대해 저장된 값을 다음과 같이 한 번에 업데이트할 수 있습니다:

use Laravel\Pennant\Feature; Feature::activateForEveryone('new-api'); Feature::activateForEveryone('purchase-button', 'seafoam-green');

반대로, 모든 사용자에 대해 기능을 비활성화할 수도 있습니다:

Feature::deactivateForEveryone('new-api');

NOTE

이 메서드들은 Pennant의 스토리지 드라이버에 저장되어 있는, 이미 확인된(resolve된) 기능 값만 업데이트합니다. 애플리케이션 코드 내의 기능 정의 자체도 함께 업데이트해야 합니다.

기능 데이터 제거하기 (Purging)

경우에 따라 특정 기능에 대한 저장 데이터를 스토리지에서 완전히 제거하고 싶을 수 있습니다. 이는 보통 애플리케이션에서 해당 기능을 완전히 제거했거나, 기능 정의를 변경해서 모든 사용자에게 새롭게 롤아웃하고 싶을 때 필요합니다.

purge 메서드를 사용하면 특정 기능에 대해 저장된 모든 값을 제거할 수 있습니다:

// 단일 기능 제거하기... Feature::purge('new-api'); // 여러 기능 한 번에 제거하기... Feature::purge(['new-api', 'purchase-button']);

스토리지에 있는 모든 기능을 제거하고 싶다면, 인자 없이 purge 메서드를 호출하면 됩니다:

Feature::purge();

배포 파이프라인의 일부로 기능 데이터를 제거하는 작업은 매우 유용할 수 있습니다. 이를 위해 Pennant는 지정한 기능들을 스토리지에서 제거해주는 pennant:purge Artisan 명령어를 제공합니다:

php artisan pennant:purge new-apiphp artisan pennant:purge new-api purchase-button

특정 기능 목록만 제외하고 나머지 모든 기능을 제거하는 것도 가능합니다. 예를 들어, 모든 기능을 제거하되 "new-api"와 "purchase-button" 기능의 값은 그대로 유지하고 싶다면, --except 옵션에 해당 기능 이름들을 전달하면 됩니다:

php artisan pennant:purge --except=new-api --except=purchase-button

편의를 위해 pennant:purge 명령어는 --except-registered 플래그도 지원합니다. 이 플래그를 사용하면 서비스 프로바이더에 명시적으로 등록된 기능들을 제외한 나머지 모든 기능을 제거합니다:

php artisan pennant:purge --except-registered

Pennant

테스트

기능 플래그와 상호작용하는 코드를 테스트할 때, 테스트 안에서 기능 플래그가 반환하는 값을 제어하는 가장 쉬운 방법은 기능을 다시 정의하는 것입니다. 예를 들어, 애플리케이션의 서비스 프로바이더에 다음과 같이 정의된 기능이 있다고 가정해 보겠습니다.

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 드라이버를 사용하는 것이 좋습니다. 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("Resolving unknown feature [{$event->feature}]."); }); } }

Laravel\Pennant\Events\DynamicallyRegisteringFeatureClass

이 이벤트는 요청 중 클래스 기반 기능이 처음으로 동적 확인될 때 발생합니다.

Laravel\Pennant\Events\UnexpectedNullScopeEncountered

이 이벤트는 null 스코프를 지원하지 않는 기능 정의에 null 스코프가 전달됐을 때 발생합니다.

이 상황은 기본적으로 문제없이 처리되며 기능은 false를 반환합니다. 하지만 이러한 기본 동작을 그대로 받아들이지 않고 직접 처리하고 싶다면, 애플리케이션의 AppServiceProviderboot 메서드에서 이 이벤트에 대한 리스너를 등록할 수 있습니다.

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

이 이벤트는 보통 activatedeactivate를 호출하여 특정 스코프의 기능을 업데이트할 때 발생합니다.

Laravel\Pennant\Events\FeatureUpdatedForAllScopes

이 이벤트는 보통 activateForEveryone이나 deactivateForEveryone을 호출하여 모든 스코프의 기능을 업데이트할 때 발생합니다.

Laravel\Pennant\Events\FeatureDeleted

이 이벤트는 보통 forget을 호출하여 특정 스코프의 기능을 삭제할 때 발생합니다.

Laravel\Pennant\Events\FeaturesPurged

이 이벤트는 특정 기능들을 제거(purge)할 때 발생합니다.

Laravel\Pennant\Events\AllFeaturesPurged

이 이벤트는 모든 기능을 제거(purge)할 때 발생합니다.

이 문서는 Laravel 공식 문서(MIT)를 한국 개발자를 위해 번역·재구성한 것입니다.

번역일: 2026년 9월 17일