기능 플래그 백엔드를 Provider 한 줄로 교체하기 — Node.js에서 OpenFeature SDK와 flagd로 벤더 종속 없애기
코드베이스 전체에 @launchdarkly/node-server-sdk가 퍼져 있고, client.variation('flag-key', user, false) 호출이 수백 군데에 박혀 있는 상황을 상상해보세요. 백엔드를 바꾸고 싶어도 그 비용이 너무 커서 그냥 현 상태를 유지하게 됩니다. 이게 벤더 락인입니다.
단일 SaaS 플래그 서비스에 전적으로 의존하면 그 취약성도 함께 떠안게 됩니다. 2021년 Fastly 장애처럼, 외부 서비스가 예고 없이 멈추면 기능 플래그 평가 자체가 불가능해집니다. 기본값으로 폴백되는 게 전부인데, 그게 안전한지는 상황마다 다릅니다.
OpenFeature SDK + flagd 조합으로 추상화 레이어를 도입하는 팀이 늘어나는 이유가 여기 있습니다. OpenFeature는 CNCF 프로젝트로, 어떤 플래그 백엔드를 쓰더라도 동일한 인터페이스로 평가할 수 있는 오픈 표준입니다. flagd는 그 표준을 구현한 데몬 중 하나입니다. 이 둘은 하나로 묶인 패키지가 아닙니다 — OpenFeature는 인터페이스이고, flagd는 그 인터페이스를 통해 연결할 수 있는 구현체 중 하나입니다.
이 글에서는 OpenFeature SDK의 Provider 추상화 모델을 이해하고, flagd를 사용해 점진적 롤아웃을 구현하는 방법을 Node.js 코드와 함께 살펴봅니다. 기존 LaunchDarkly나 Unleash를 쓰고 있다면 마이그레이션 경로도 같이 확인해볼 수 있습니다.
핵심 개념
Provider 추상화 모델
OpenFeature의 핵심은 단순합니다. 플래그를 평가하는 코드가 "어떤 백엔드를 쓰는지" 몰라도 된다는 거예요. @openfeature/server-sdk를 설치하면, 앱 코드에서는 항상 동일한 메서드(getBooleanValue, getStringValue, getNumberValue)를 사용합니다. 실제 평가 로직은 Provider가 담당하고, 이 Provider만 교체하면 백엔드가 바뀝니다.
한 시점에 하나의 Provider만 활성 상태입니다. 점선으로 표시한 나머지는 교체 대상이지, 동시에 연결되는 게 아닙니다.
어떤 프로바이더를 연결하더라도 아래 코드는 변하지 않습니다.
const client = OpenFeature.getClient();
const isNewCheckoutEnabled = await client.getBooleanValue('new-checkout', false, {
targetingKey: user.id,
email: user.email,
plan: user.plan,
});여기서 targetingKey는 OpenFeature Evaluation Context에서 평가 대상(보통 사용자)을 식별하는 예약 필드입니다. 문자열이어야 하며, user.id처럼 고유 식별자를 넣는 게 일반적입니다. userId가 아닌 targetingKey라는 이름을 사용하는 이유는, 어떤 백엔드 프로바이더를 연결하더라도 타겟팅 대상이 누구인지 표준화된 방식으로 전달하기 위해서입니다.
flagd란 무엇인가
flagd는 OpenFeature 프로젝트가 공식으로 제공하는 플래그 평가 데몬입니다. 플래그 정의 로드, 평가, 스트림 제공 외의 기능(인증, 웹 UI, RBAC)은 의도적으로 범위 밖에 두고 있습니다. JSON 파일, HTTP 엔드포인트, gRPC 소스에서 플래그 정의를 읽어오고 평가 요청에 응답합니다.
두 가지 동작 모드가 있는데, 처음엔 이 둘의 차이가 잘 와닿지 않을 수 있습니다.
RPC 모드는 앱이 flagd에 gRPC로 요청을 보내고 결과를 받아오는 방식입니다. 구성이 단순하고 flagd 규칙 업데이트가 즉시 반영됩니다. 기본 선택지로 충분합니다. In-process 모드는 flagd의 규칙셋을 앱 프로세스 내부로 동기화해 네트워크 왕복 없이 로컬에서 평가합니다. 마이크로서비스 환경이나 평가 레이턴시를 수 ms 이하로 줄여야 하는 명확한 요구가 생겼을 때 고려합니다. 확실한 필요가 생기기 전까지는 RPC 모드로 시작하는 게 낫습니다.
OFREP와 플래그 정의 포맷
**OFREP(OpenFeature Remote Evaluation Protocol)**는 HTTP 기반 와이어 프로토콜로, flagd는 기본 포트 8016에서 이를 제공합니다. gRPC 클라이언트를 사용할 수 없는 환경(서버리스, 엣지 런타임 등)에서 flagd와 통신할 때 쓰는 옵션입니다. 일반 Node.js 서버 환경이라면 gRPC 기반 FlagdProvider를 그대로 씁니다.
플래그 정의는 flags.json 파일에 작성합니다.
{
"$schema": "https://flagd.dev/schema/v0/flags.json",
"flags": {
"new-checkout": {
"state": "ENABLED",
"variants": {
"on": true,
"off": false
},
"defaultVariant": "off"
}
}
}실전 적용
1. 기본 셋업 — flagd 실행부터 플래그 평가까지
먼저 flagd를 Docker로 로컬에 띄웁니다.
docker run --rm -p 8013:8013 -p 8016:8016 \
-v $(pwd)/flags.json:/etc/flagd/flags.json \
ghcr.io/open-feature/flagd:latest start \
--uri file:/etc/flagd/flags.jsonNode.js 의존성을 설치합니다.
npm install @openfeature/server-sdk @openfeature/flagd-provider앱 진입점에서 Provider를 등록합니다. setProviderAndWait()를 쓰는 게 중요합니다. setProvider()만 쓰면 플래그 상태가 로드되기 전에 요청이 들어오는 레이스 컨디션이 생길 수 있거든요.
import express from 'express';
import { OpenFeature } from '@openfeature/server-sdk';
import { FlagdProvider } from '@openfeature/flagd-provider';
async function bootstrap() {
await OpenFeature.setProviderAndWait(
new FlagdProvider({ host: 'localhost', port: 8013 })
);
// 이 시점부터 플래그 평가가 안전합니다
const app = express();
app.listen(3000);
}2. 점진적 롤아웃 — fractional evaluation
50% 사용자에게만 새 체크아웃 UI를 노출하는 시나리오입니다. flagd의 fractional 기능을 사용하면 해시 기반으로 사용자를 결정론적으로 분배해줍니다. 같은 targetingKey를 가진 사용자는 항상 같은 변형을 받기 때문에 세션 일관성이 보장됩니다.
{
"flags": {
"new-checkout": {
"state": "ENABLED",
"variants": {
"control": false,
"treatment": true
},
"defaultVariant": "control",
"targeting": {
"fractional": [
{ "var": "targetingKey" },
["control", 50],
["treatment", 50]
]
}
}
}
}평가 흐름을 도식으로 보면 이렇습니다.
앱 코드에서는 Evaluation Context에 targetingKey만 포함해주면 flagd가 나머지를 처리합니다.
const client = OpenFeature.getClient();
app.post('/checkout', async (req, res) => {
const context = {
targetingKey: req.user.id,
email: req.user.email,
plan: req.user.plan,
};
const useNewCheckout = await client.getBooleanValue(
'new-checkout',
false,
context
);
if (useNewCheckout) {
return newCheckoutHandler(req, res);
}
return legacyCheckoutHandler(req, res);
});3. LaunchDarkly에서 마이그레이션하기
코드 인터페이스 교체 자체는 단순합니다.
Before (LaunchDarkly v9.x)
import * as ld from '@launchdarkly/node-server-sdk';
const ldClient = ld.init('sdk-key');
await ldClient.waitForInitialization();
// v9.x부터는 Context 객체에 kind 필드가 필요합니다
const value = await ldClient.variation('flag-key', { kind: 'user', key: user.id }, false);After (OpenFeature + FlagdProvider)
import { OpenFeature } from '@openfeature/server-sdk';
import { FlagdProvider } from '@openfeature/flagd-provider';
await OpenFeature.setProviderAndWait(new FlagdProvider());
const client = OpenFeature.getClient();
const value = await client.getBooleanValue('flag-key', false, {
targetingKey: user.id,
});나중에 Flagsmith나 DevCycle로 다시 옮기고 싶으면, new FlagdProvider() 부분만 바꾸면 됩니다.
그런데 이 코드 교체가 마이그레이션 비용의 전부가 아닙니다. 실제 비용의 대부분은 플래그 정의 변환에 있습니다. LaunchDarkly에서 수십 개의 플래그를 운영하고 있다면, 타겟팅 규칙, 세그먼트, 변형 값이 모두 LD 독자 포맷으로 저장되어 있습니다. 이걸 flags.json 스키마로 옮기는 작업은 수동이며, 롤아웃 퍼센티지와 타겟팅 조건을 flagd의 fractional, targeting 표현식으로 다시 작성해야 합니다. 코드베이스는 하루 만에 바꿀 수 있지만, 플래그 정의 변환은 별도 계획이 필요합니다. 플래그 수와 타겟팅 규칙 복잡도를 먼저 파악한 뒤 일정을 잡는 게 현실적입니다.
4. 단위 테스트에서 플래그 값 제어하기
InMemoryProvider를 쓰면 테스트에서 실제 flagd 없이 플래그 값을 완전히 제어할 수 있습니다. CI 환경에서 외부 서비스 의존성을 제거하는 데 유용합니다.
import { OpenFeature, InMemoryProvider } from '@openfeature/server-sdk';
describe('checkout handler', () => {
beforeEach(async () => {
await OpenFeature.setProviderAndWait(
new InMemoryProvider({
'new-checkout': {
defaultVariant: 'treatment',
variants: { treatment: true, control: false },
},
})
);
});
afterEach(async () => {
await OpenFeature.clearProviders();
});
it('treatment 변형에서 새 체크아웃을 사용합니다', async () => {
const client = OpenFeature.getClient();
const value = await client.getBooleanValue('new-checkout', false, {
targetingKey: 'user-123',
});
expect(value).toBe(true);
});
});afterEach에서 clearProviders()를 호출하는 게 중요합니다. OpenFeature SDK는 전역 싱글턴이라, 정리하지 않으면 다른 테스트 스위트에서 프로바이더 상태가 오염될 수 있습니다.
장단점 분석
| 항목 | 내용 |
|---|---|
| 벤더 독립성 | Provider 한 줄 교체로 백엔드 전환 가능, 코드 마이그레이션 비용 최소화 |
| 결정론적 롤아웃 | 해시 기반 fractional 평가로 동일 사용자에게 항상 동일 변형 보장 |
| 테스트 용이성 | InMemoryProvider로 실제 서비스 없이 단위 테스트 완전 제어 가능 |
| 표준 Evaluation Context | 어떤 백엔드를 쓰더라도 동일한 타겟팅 로직 적용 |
| 생태계 성장 | 여러 주요 기업이 참여하는 CNCF 프로젝트, 공식 엔드유저 목록은 OpenFeature 사이트에서 확인 가능 |
| flagd 운영 부담 | flagd 데몬을 직접 배포·운영해야 함, 오케스트레이션 없이는 설정 복잡성 증가 |
| 관리 UI 부재 | flagd는 JSON 파일 기반, 웹 UI·감사 로그·RBAC 필요 시 별도 도구 조합 필요 |
| 플래그 정의 변환 비용 | 기존 벤더에서 마이그레이션 시 타겟팅 규칙·세그먼트를 flagd 포맷으로 수동 변환 필요 |
| 프로바이더 품질 편차 | 공식 프로바이더와 커뮤니티 프로바이더 간 기능 완성도 차이 존재 |
| 스펙 발전 중 | 일부 영역이 계속 발전 중이라 버전 호환성을 배포 전에 확인하는 습관이 필요 |
기존 벤더의 OpenFeature 지원 수준은 마이그레이션 전에 직접 확인하는 게 좋습니다. LaunchDarkly는 공식 프로바이더를 제공하지만, 어느 기능까지 지원하는지는 버전마다 다를 수 있습니다. 커뮤니티 프로바이더에 의존하는 경우라면 더욱 그렇습니다.
실무에서 자주 만나는 실수
setProvider() vs setProviderAndWait()
setProvider()는 비동기로 프로바이더를 초기화하는데, 초기화가 완료되기 전에 서버가 요청을 받으면 기본값이 반환됩니다. 개발 환경에서는 그냥 넘어가다가 프로덕션에서 간헐적 버그로 나타나는 케이스입니다. 앱 시작 시에는 반드시 setProviderAndWait()를 사용하는 것이 좋습니다.
Evaluation Context 없이 호출하는 경우
getBooleanValue('flag', false)처럼 컨텍스트 없이 호출하면 fractional 타겟팅이 동작하지 않습니다. targetingKey를 항상 포함하는 Express 미들웨어 패턴을 하나 만들어두면 실수를 줄일 수 있습니다.
InMemoryProvider로만 테스트하고 JSON 스키마 유효성을 확인하지 않는 경우
단위 테스트는 통과하는데 실제 flagd에서 스키마 오류가 나는 상황이 생길 수 있습니다. flagd CLI의 validate 명령을 CI 파이프라인에 포함해두면 이런 상황을 배포 전에 잡을 수 있습니다.
# 플래그 파일 스키마 유효성 검증
flagd validate --flag-source-uri file:flags.jsonflags.json을 수정하는 PR마다 이 단계를 CI에 넣어두면, InMemoryProvider 테스트가 잡지 못하는 스키마 오류를 미리 발견할 수 있습니다.
마치며
OpenFeature SDK의 핵심 가치는 플래그 평가 코드와 플래그 백엔드를 완전히 분리하는 것입니다. 오늘 flagd를 쓰다가 내일 Flagsmith나 DevCycle로 바꾸더라도, 비즈니스 로직 코드는 손댈 필요가 없습니다. getBooleanValue, getStringValue 같은 표준 메서드로 평가하고, Provider 설정 한 줄만 바꾸면 백엔드 전환이 끝납니다. 단, 플래그 정의 파일은 별도로 변환해야 한다는 점은 미리 계획에 넣어두세요.
어떤 상황에서 시작하느냐에 따라 접근 방식이 다릅니다.
신규 기능을 막 만들기 시작하는 팀이라면 외부 플래그 서비스 없이도 시작할 수 있습니다. InMemoryProvider를 먼저 붙이고 OpenFeature 인터페이스에 익숙해진 뒤, Docker로 flagd를 띄우는 순서가 부담이 가장 적습니다.
이미 LaunchDarkly나 Unleash를 운영 중인 팀이라면 코드 교체보다 플래그 정의 변환 계획을 먼저 세우는 게 현실적입니다. 플래그 수와 타겟팅 규칙 복잡도를 파악한 뒤, 신규 플래그부터 OpenFeature 표준으로 작성하면서 기존 플래그를 점진적으로 옮기는 방식이 안전합니다.
자체 호스팅을 검토 중인 팀이라면 flagd 외에도 Flagsmith, Flipt 같은 셀프 호스팅 옵션도 OpenFeature 프로바이더를 제공합니다. 웹 UI나 감사 로그가 필요하다면 flagd 단독보다 이쪽을 먼저 살펴보는 게 나을 수 있습니다.
참고 자료
- OpenFeature 공식 사이트
- OpenFeature Node.js SDK 공식 문서
- flagd 공식 사이트
- flagd GitHub 저장소
- OpenFeature GitHub 조직
- OFREP 명세
- CNCF OpenFeature 프로젝트 페이지
- OpenFeature CNCF 인큐베이팅 발표
- LaunchDarkly → OpenFeature 마이그레이션 가이드 (Flipt)
- OpenFeature로 LaunchDarkly 이탈하기 (Flagsmith)
- OpenFeature + ConfigCat 표준화 사례 (Scoro Engineering)
- OpenFeature + flagd 딥다이브: Is it Observable?
- Five Minutes to Feature Flags 튜토리얼 (OpenFeature)