OpenFeature SDK + flagd로 구현하는 타입 안전 피처 플래그 — 런타임 설정 변경과 A/B 테스트를 코드 배포 없이 제어하는 법
어느 날 오후, 배포 직후 결제 전환율이 20% 떨어진다는 알림이 왔다. 원인은 방금 배포한 새 체크아웃 UI였다. 코드를 되돌리고, 이미지를 다시 빌드하고, 파이프라인을 돌리는 동안 30분이 흘렀다. 그 30분이 피처 플래그를 진지하게 알아보게 된 계기였다.
피처 플래그를 도입하면 배포와 릴리즈를 분리할 수 있다. 코드를 배포한 뒤에도 기능을 끈 채로 유지하다가, 준비되면 플래그만 켜서 노출하는 방식이다. 그런데 "어떤 시스템으로 관리하느냐"가 기술 부채로 이어지기도 한다. LaunchDarkly 같은 SaaS를 직접 SDK로 통합하면 강력하지만, 나중에 다른 도구로 바꾸려 할 때 코드 전체를 뒤져야 한다.
OpenFeature는 그 문제를 정면으로 해결하기 위해 만들어진 CNCF 인큐베이팅 프로젝트다. 피처 플래그를 위한 벤더 중립 표준 API를 제공하고, 여기에 flagd라는 공식 레퍼런스 구현체를 결합하면 완전히 자체 호스팅 가능한 피처 플래그 시스템을 구성할 수 있다. 이 글에서는 타입 안전성, 재배포 없는 런타임 제어, 일관된 A/B 테스트라는 세 가지 관점에서 실제 도입 시나리오를 살펴본다.
핵심 개념
Provider 패턴: 벤더 중립성의 핵심
OpenFeature 설계에서 가장 중요한 개념이 Provider 패턴이다. 애플리케이션 코드는 항상 OpenFeature SDK의 표준 API만 호출하고, 실제 플래그 평가는 Provider가 담당한다. Provider를 교체해도 애플리케이션 코드는 한 줄도 바꾸지 않는다.
GrowthBook이나 LaunchDarkly로 갈아타야 한다면 Provider만 바꾸면 된다. getBooleanValue('feature-x', false) 같은 평가 코드는 그대로 유지된다. 실제로 Provider 교체 테스트를 해보면 SDK 레벨에서 완전히 추상화된 것을 확인할 수 있다.
현재 OpenFeature와 호환되는 Provider는 다양하다.
| Provider | 유형 | 특징 |
|---|---|---|
| flagd | 오픈소스 셀프호스팅 | 공식 레퍼런스 구현체 |
| GO Feature Flag | 오픈소스 셀프호스팅 | OFREP 기반 |
| GrowthBook | 오픈소스/클라우드 | A/B 테스팅 특화 |
| Flagsmith | 오픈소스/클라우드 | 관리 UI 포함 |
| LaunchDarkly | 상용 SaaS | 엔터프라이즈 기능 풍부 |
| ConfigCat | 상용 SaaS | 간단한 설정에 적합 |
flagd와 Sync Sources: 재배포 없이 반영되는 방식
flagd는 "Unix 철학을 따르는 피처 플래그 데몬"이라고 소개된다. 독립 바이너리로 동작하며, 플래그 설정이 변경되면 코드 재배포 없이 런타임에 반영한다.
flagd가 지원하는 동기화 방식은 4가지다.
- File Sync:
fsnotify로 로컬 파일 변경을 감지. 변경이 수 초 내 반영됨. 개발·스테이징 환경에 적합 - HTTP Sync: 외부 HTTP 엔드포인트를 주기적으로 폴링. 기본 폴링 주기는 60초
- gRPC Sync: proto 기반 스트리밍으로 실시간 푸시 수신
- Kubernetes CRD Sync:
FeatureFlagSourceCRD를 Operator가 감시
Sync 방식에 따라 변경이 적용되는 속도가 다르다. File Sync는 수 초 내 반영이 가능하지만, HTTP Sync는 폴링 주기만큼 지연이 생기고, 여러 레플리카가 있는 환경이라면 각 파드의 flagd 사이드카가 변경을 인식하는 시점이 서로 다를 수 있다. "즉시"라는 표현은 Sync 방식에 따라 달라진다는 점을 프로덕션 설계 단계에서 염두에 두어야 한다.
Kubernetes 환경이라면 Open Feature Operator를 사용하면 FeatureFlagSource CRD를 정의하는 것만으로 Operator가 자동으로 flagd 사이드카를 파드에 주입해준다. 최근에는 In-Process 평가 모드도 추가됐다. 플래그 룰셋을 애플리케이션 프로세스 내부에 동기화해 네트워크 지연 없이 로컬 평가하는 방식으로, 레이턴시에 민감한 경우에 선택지가 된다.
타입 안전성: 문자열 리터럴 실수를 컴파일 타임에 잡기
피처 플래그를 문자열 키로 다루다 보면 반드시 한 번은 겪는 사고가 있다. 플래그 키를 오타로 잘못 입력하거나, getBooleanValue로 가져와야 할 플래그를 getStringValue로 읽는 경우다. OpenFeature는 타입별 메서드를 통해 이 문제를 줄여준다.
const client = OpenFeature.getClient();
// 타입별로 분리된 메서드
const showNewUI = await client.getBooleanValue('new-checkout-ui', false);
const theme = await client.getStringValue('ui-theme', 'default');
const maxRetries = await client.getNumberValue('max-retries', 3); // JS SDK는 getNumberValue
const config = await client.getObjectValue('feature-config', {});
getIntegerValue/getDoubleValue는 Java·Go·.NET SDK에만 존재한다. JavaScript/TypeScript SDK는 정수와 실수를 구분하지 않고getNumberValue를 사용한다.
타입별 메서드만으로는 플래그 키 오타까지 막지는 못한다. 한 단계 더 나아가 플래그 키와 반환 타입을 타입 시스템으로 명시하면, 잘못된 키나 타입 불일치를 컴파일 타임에 잡을 수 있다.
// flags.ts — 플래그 키와 반환 타입을 한 곳에서 관리
import type { Client, EvaluationContext } from '@openfeature/server-sdk';
interface FlagSchema {
'new-checkout-ui': boolean;
'ui-theme': 'dark' | 'light';
'max-retries': number;
}
type FlagKey = keyof FlagSchema;
async function getFlag<K extends FlagKey>(
client: Client,
key: K,
defaultValue: FlagSchema[K],
ctx?: EvaluationContext
): Promise<FlagSchema[K]> {
if (typeof defaultValue === 'boolean') {
return client.getBooleanValue(key, defaultValue, ctx) as Promise<FlagSchema[K]>;
}
if (typeof defaultValue === 'number') {
return client.getNumberValue(key, defaultValue, ctx) as Promise<FlagSchema[K]>;
}
return client.getStringValue(key, defaultValue as string, ctx) as Promise<FlagSchema[K]>;
}
// 사용 예
const showNew = await getFlag(client, 'new-checkout-ui', false); // ✓ boolean
const theme = await getFlag(client, 'ui-theme', 'light'); // ✓ 'dark' | 'light'
// await getFlag(client, 'new-checkout-ui', 'default'); // ✗ 컴파일 타임 오류FlagSchema 인터페이스 하나가 전체 플래그 레지스트리 역할을 한다. 플래그를 추가하거나 제거할 때 이 파일 하나만 고치면 나머지 코드에서 타입 오류가 즉시 드러난다. Go나 Java 환경이라면 OpenFeature CLI를 사용해 플래그 매니페스트(JSON/YAML)에서 타입 안전 접근자 코드를 자동 생성하는 방법도 있다.
타게팅 규칙과 분수적 평가
flagd는 JSONLogic 기반의 규칙 엔진을 내장한다. userId, region, plan 같은 컨텍스트를 기반으로 어떤 변형(variant)을 반환할지 결정할 수 있다.
**분수적 평가(Fractional Evaluation)**는 murmurhash3 기반의 일관된 해싱으로 트래픽을 비율에 따라 분배한다. 핵심은 '일관성'이다. 동일한 사용자는 언제나 동일한 변형을 받으므로, A/B 테스트 도중 사용자 경험이 들쑥날쑥해지는 문제가 없다.
실전 적용
프로젝트 초기 설정 (Node.js + TypeScript)
먼저 flagd를 로컬에서 실행한다. Docker로 시작할 때 한 가지 주의할 점이 있다. 단일 파일을 바인드 마운트하면(-v $(pwd)/flags.json:/etc/flagd/flags.json) 대부분의 텍스트 에디터가 저장 시 inode를 교체하기 때문에, 컨테이너 내부 fsnotify가 변경을 감지하지 못하는 경우가 많다. 디렉터리를 마운트하고 플래그 파일을 그 안에 두는 방식이 표준 해결책이다.
mkdir -p flags-dir
cp flags.json flags-dir/flags.json
docker run --rm \
-p 8013:8013 \
-v $(pwd)/flags-dir:/etc/flagd/flags-dir \
ghcr.io/open-feature/flagd:latest \
start --uri file:/etc/flagd/flags-dir/flags.json플래그 정의 파일 flags-dir/flags.json을 작성한다.
{
"flags": {
"new-checkout-ui": {
"state": "ENABLED",
"variants": {
"on": true,
"off": false
},
"defaultVariant": "off"
},
"ui-theme": {
"state": "ENABLED",
"variants": {
"dark": "dark",
"light": "light"
},
"defaultVariant": "light"
}
}
}Node.js 애플리케이션에서 SDK를 초기화한다. 최신 패키지명은 공식 문서에서 확인하는 것을 권장한다.
import { OpenFeature } from '@openfeature/server-sdk';
import { FlagdProvider } from '@openfeature/flagd-provider';
// Provider 초기화 — 애플리케이션 시작 시 한 번만
await OpenFeature.setProviderAndWait(
new FlagdProvider({ host: 'localhost', port: 8013 })
);
const client = OpenFeature.getClient('my-service');
// 컨텍스트와 함께 플래그 평가
const evaluationContext = {
targetingKey: user.id,
region: user.region,
plan: user.plan,
};
const showNewUI = await client.getBooleanValue(
'new-checkout-ui',
false,
evaluationContext
);
if (showNewUI) {
return renderNewCheckout();
}
return renderLegacyCheckout();킬 스위치: 장애 시 재배포 없이 롤백
새 기능에 장애가 발생했을 때 재배포 없이 끌 수 있는 것이 피처 플래그의 가장 직접적인 가치다. flags.json에서 "state"를 "DISABLED"로 바꾸면 된다.
{
"flags": {
"new-payment-service": {
"state": "DISABLED",
"variants": {
"on": true,
"off": false
},
"defaultVariant": "off"
}
}
}여기서 중요한 동작 방식을 짚고 넘어가야 한다. "state": "DISABLED" 상태에서 flagd는 플래그가 존재하지 않는 것처럼 동작하며, SDK는 플래그 정의의 defaultVariant가 아니라 **코드에서 넘긴 두 번째 인자(기본값)**를 반환한다. 이유 코드로 DISABLED를 기록하면서 말이다.
// DISABLED 상태에서 flagd는 두 번째 인자 false를 반환한다
const showNewUI = await client.getBooleanValue('new-payment-service', false);따라서 킬 스위치가 의도대로 동작하려면 코드 레벨 기본값이 '안전한 방향(기존 동작)'이어야 한다. 기본값이 true이면 DISABLED 상태에서도 새 기능이 노출된다. 이 동작 방식은 뒤에서 다룰 실무 실수와도 직결된다.
반영 속도는 Sync 방식에 따라 다르다. File Sync 환경에서는 디렉터리 마운트 방식으로 띄운 flagd가 변경을 수 초 내에 감지한다. HTTP Sync라면 폴링 주기(기본 60초)만큼 지연이 생기고, 여러 레플리카가 있는 환경에서는 각 파드의 사이드카가 변경을 인식하는 시점이 다를 수 있다. 핫픽스 PR을 기다리는 것보다는 훨씬 빠르지만, 운영 환경에서는 사용 중인 Sync 방식의 지연 특성을 팀이 공유해두는 것이 좋다.
A/B 테스트: 사용자의 20%에게만 새 UI 노출
분수적 평가를 활용한 A/B 테스트 설정이다. variant 이름을 new/old처럼 의미 있게 정의하면 JSON, 코드, 다이어그램이 일관되게 유지된다.
{
"flags": {
"new-checkout-ui": {
"state": "ENABLED",
"variants": {
"new": true,
"old": false
},
"defaultVariant": "old",
"targeting": {
"fractional": [
["new", 20],
["old", 80]
]
}
}
}
}targetingKey(보통 userId)를 기준으로 해싱하기 때문에 동일 사용자는 세션이 바뀌어도 항상 같은 변형을 받는다. 지표 집계를 신뢰할 수 있게 되는 이유다.
// new variant = true, old variant = false
const showNewUI = await client.getBooleanValue(
'new-checkout-ui',
false, // 안전한 기본값: 기존 체크아웃
evaluationContext
);
if (showNewUI) {
return renderNewCheckout(); // new variant: true
}
return renderLegacyCheckout(); // old variant: false카나리 릴리즈나 링(ring) 배포로 확장하고 싶다면, targeting 블록에 JSONLogic 조건을 추가해 region이나 plan 같은 속성으로 먼저 특정 그룹을 대상으로 삼고, 문제가 없으면 분수 비율을 점진적으로 높이는 방식으로 운영할 수 있다.
Kubernetes 환경: OpenFeature Operator + 사이드카 패턴
프로덕션 Kubernetes 환경이라면 Open Feature Operator를 활용해 인프라 레벨에서 피처 플래그 생명주기를 관리할 수 있다. FeatureFlagSource CRD를 정의하면 Operator가 자동으로 flagd 사이드카를 파드에 주입한다.
apiVersion: core.openfeature.dev/v1beta1
kind: FeatureFlagSource
metadata:
name: my-feature-flags
spec:
sources:
- source: my-flags-configmap # ConfigMap 이름
provider: kubernetes # ConfigMap 소스는 kubernetes provider 사용
flagd:
port: 8013apiVersion: v1
kind: ConfigMap
metadata:
name: my-flags-configmap
data:
flags.json: |
{
"flags": {
"new-checkout-ui": {
"state": "ENABLED",
"variants": { "new": true, "old": false },
"defaultVariant": "old"
}
}
}
provider: file은 컨테이너 내부 로컬 파일시스템 경로용이다. ConfigMap 기반 소스에는provider: kubernetes를 사용한다. CRD 스펙은 Operator 버전마다 다를 수 있으니 공식 퀵스타트에서 확인하는 것을 권장한다.
ConfigMap을 수정하면 Operator가 변경을 감지하고, 파드 내 flagd 사이드카가 새 설정을 적용한다. 애플리케이션 파드는 재시작하지 않아도 된다.
멀티 언어 환경에서도 Go, Java, Node.js 서비스들이 각자 다른 OpenFeature SDK를 사용하면서 동일한 flagd 백엔드를 공유하는 구조가 자연스럽게 만들어진다. 평가 로직이 단일 출처(flagd)에서 관리되기 때문에 언어별로 타게팅 룰을 따로 구현할 필요가 없다.
장단점 분석
도입하면 얻는 것
| 항목 | 설명 |
|---|---|
| 벤더 중립성 | Provider만 교체하면 LaunchDarkly → flagd → Flagsmith 이동 시 애플리케이션 코드 수정 불필요 |
| 코드 배포 없는 변경 | 플래그 파일·CRD 수정만으로 런타임 반영 |
| 타입 안전성 | 타입별 메서드 + 타입 스키마 래퍼로 문자열 리터럴 실수 방지 |
| 멀티 언어 표준화 | Go, Java, Node, Python, .NET, Rust 등에서 동일한 개념과 메서드 시그니처 |
| 완전 자체 호스팅 | flagd는 완전 무료, 외부 SaaS 의존 없이 온프레미스 운영 가능 |
| 강력한 타게팅 엔진 | JSONLogic 기반 복잡한 조건 + 일관 해싱 기반 분수 평가 |
| CNCF 생태계 통합 | OpenTelemetry, Kubernetes Operator, Prometheus와 자연스럽게 연동 |
주의해야 할 부분
| 항목 | 설명 |
|---|---|
| 추상화 오버헤드 | 플래그가 5개 이하인 소규모 단일 서비스에서는 Provider 패턴이 불필요한 복잡성을 더할 수 있음 |
| 마이그레이션 한계 | 코드 레벨 벤더 락인은 해결되지만, 플래그 메타데이터·타게팅 규칙·권한 설정은 별도 재구성 필요 |
| 프로젝트 성숙도 | CNCF 인큐베이팅 단계로 일부 Provider와 기능이 개발 중. 프로덕션 도입 전 GitHub에서 변경사항 확인 권장 |
| 운영 복잡성 | flagd 데몬을 별도로 운영해야 하므로 로그·모니터링·사이드카 관리가 추가로 필요 |
| 관리 UI 부재 | flagd 자체에는 웹 관리 대시보드가 없음. UI가 필요하면 Flagsmith·GrowthBook 같은 OpenFeature 호환 백엔드를 조합해야 함 |
실무에서 자주 하는 실수
가장 많이 실수하는 것은 기본값(default value) 설계를 가볍게 보는 것이다. 앞서 킬 스위치 섹션에서 설명했듯이, flagd 서버에 연결이 안 되거나 플래그가 DISABLED 상태일 때 SDK는 플래그 정의의 defaultVariant가 아니라 코드에서 넘긴 두 번째 인자를 반환한다. 기본값이 새 기능을 켜는 방향(true)이면 장애 상황에서도 새 기능이 노출된다. 기본값은 항상 안전한 방향, 즉 기존 동작(false)으로 설정하는 것이 원칙이다.
두 번째는 플래그 정리를 미루는 것이다. 피처 플래그는 임시 코드 경로다. 릴리즈가 완료된 플래그를 제때 제거하지 않으면 코드베이스가 if (flag) { ... } 분기로 가득 차고, 나중엔 어떤 플래그가 살아있는지 파악조차 어려워진다. 플래그를 추가할 때부터 만료 날짜나 담당자를 메타데이터에 기록해두는 습관이 장기적으로 코드베이스 건강에 도움이 된다.
마치며
OpenFeature + flagd 조합의 핵심 가치는 세 가지다. Provider 패턴으로 특정 피처 플래그 벤더에 코드가 종속되지 않는다. flagd의 Sync Sources 덕분에 플래그 설정 변경이 코드 재배포 없이 런타임에 반영된다. 타입별 SDK 메서드와 타입 스키마 래퍼로 문자열 리터럴 실수를 컴파일 타임에 잡아낼 수 있다.
지금 시작해보고 싶다면 다음 순서로 접근하면 된다.
1단계 — 로컬에서 flagd 띄우기 Docker로 flagd를 실행하고(디렉터리 마운트 방식으로), 간단한 플래그 JSON 파일을 작성한 다음 OpenFeature SDK로 값을 읽어보면 된다. 환경 설정부터 첫 플래그 평가까지 30분이면 충분하다.
2단계 — 기존 환경변수 플래그를 하나씩 이관 전체를 한 번에 바꾸려 하지 않아도 된다. 가장 자주 건드리는 플래그 하나를 선택해 OpenFeature API로 이관해보면, 패턴을 체감하는 데 훨씬 도움이 된다.
3단계 — Kubernetes 환경이라면 Operator + CRD로 확장
로컬 검증이 끝나면 Open Feature Operator를 클러스터에 설치하고 FeatureFlagSource CRD로 관리하는 구조로 전환할 수 있다. 이 시점부터는 ConfigMap 편집만으로 프로덕션 플래그를 제어하는 워크플로가 완성된다.
2022년 샌드박스에서 시작해 2023년 CNCF 인큐베이팅 단계로 이동한 OpenFeature는 이미 많은 기업의 프로덕션 환경에서 검증되고 있다. Linux Foundation의 무료 강좌(LFS140)도 공개돼 있으니, 깊이 파고들고 싶다면 참고해보면 좋다.
참고 자료
- OpenFeature 공식 사이트
- OpenFeature 소개 문서
- flagd 공식 문서
- flagd GitHub 저장소
- flagd Sync 설정 문서
- OpenFeature Provider 개념
- OpenFeature Node.js SDK 문서
- OpenFeature CLI 튜토리얼
- OpenFeature Operator 퀵스타트
- CNCF OpenFeature 프로젝트 페이지
- Linux Foundation 무료 강좌 LFS140
- OpenFeature + flagd 심층 분석
- OpenFeature와 벤더 락인 분석
- OpenFeature + OpenTelemetry A/B 테스트 연동
- .NET에서 OpenFeature + flagd 구현 예시
- Node.js OpenFeature 2026 가이드
- 2026 피처 플래그 아키텍처 및 프로그레시브 딜리버리
- 2026 오픈소스 피처 플래그 도구 비교
- Top 10 OpenFeature Providers 비교