feature flag 은 "코드 배포"와 "기능 노출"을 분리하는 스위치다. 배포는 끝났는데 기능은 아직 꺼둔 채로, 나중에 코드 재배포 없이 켤 수 있다. 단순해 보이지만 flag 유형마다 수명과 운영이 다르고, 점진 rollout·A/B 연계·평가 위치(서버 vs 클라)에서 함정이 숨어 있다. 이 가이드는 그 전부를 정리한다. A/B 실험 통계의 세부는 별도 가이드 (how-ab-testing-actually-works) 참조.
flag 이 푸는 문제 — 배포와 릴리스의 분리
flag 없이:
코드 머지 = 즉시 모든 사용자에게 노출
→ 버그 발견 시 롤백 = 재배포 (수 분~수십 분)
→ "금요일 배포 금지" 문화의 원인
flag 로:
if (flags.newCheckout) { 새 결제 } else { 기존 결제 }
- 코드는 배포됐지만 flag=off → 아무도 안 봄
- 준비되면 flag=on → 재배포 없이 즉시 노출
- 문제 생기면 flag=off → 재배포 없이 즉시 회수 (초 단위)
핵심: 배포(deploy)와 릴리스(release)를 분리.
trunk-based development 와 짝 — 미완성 기능도 flag 뒤에 두고 main 에 머지.flag 유형 — 수명이 다르다
모든 flag 을 같게 취급하면 망한다. 수명과 목적이 다르면 관리 방식도 달라야 한다.
| 유형 | 목적 | 수명 | 변경 주체 |
|---|---|---|---|
| Release | 미완성/새 기능 점진 노출 | 짧음 (출시 후 제거) | 개발팀 |
| Ops / kill-switch | 장애 시 기능 즉시 차단 | 긺 (상시 유지) | 운영/SRE |
| Experiment (A/B) | 변형 비교·측정 | 실험 기간만 | PM/데이터팀 |
| Permission | 플랜·권한별 기능 게이트 | 영구 (제품의 일부) | 제품/영업 |
구분이 중요한 이유:
- Release flag 은 출시되면 즉시 제거해야 (안 그러면 flag 부채)
- Ops/kill-switch 는 영구 — 절대 제거하면 안 됨 (장애 대비)
- Experiment flag 은 실험 끝나면 결과 반영 후 제거
- Permission flag 은 제품 로직 자체 — flag 라기보다 entitlement
→ 같은 "if (flag)" 처럼 보여도 관리 정책이 정반대.
flag 에 유형 태그를 달아 수명을 추적하는 게 운영의 핵심.점진 rollout — % 와 ring
퍼센트 기반 rollout:
Day 1: 1% 사용자에게 on (canary — 소수로 위험 확인)
Day 2: 5% → 메트릭 정상이면
Day 3: 25%
Day 4: 50%
Day 5: 100%
문제 감지 시 즉시 이전 단계로 (또는 0%)
핵심: 같은 사용자는 항상 같은 결과 (sticky)
rollout % = 사용자ID 해시가 임계값 아래인지로 결정
bucket = hash(userId + flagKey) % 100
if (bucket < rolloutPercent) → on
→ 1%→5% 로 올려도 기존 1% 는 그대로 유지 (깜빡임 없음)
ring 기반 rollout (점진 동심원):
Ring 0: 내부 직원 (dogfooding)
Ring 1: 베타 신청자 / 일부 지역
Ring 2: 일반 사용자 일부 %
Ring 3: 전체
→ 각 ring 통과 후 다음으로 (Microsoft 등이 쓰는 패턴)sticky bucketing(같은 사용자=같은 결과)이 핵심이다. 매 요청 랜덤이면 같은 사용자가 새 UI 와 옛 UI 를 오가며 깜빡여 혼란스럽고, A/B 측정도 오염된다. 사용자ID 해시로 안정적으로 배정한다.
A/B 테스트와의 연계
feature flag 과 A/B 실험은 같은 인프라를 공유:
- 둘 다 "사용자를 그룹으로 나눔" (bucketing)
- 둘 다 sticky (같은 사용자 = 같은 변형)
차이:
rollout flag: 목표 = 안전하게 100% 도달 (점진 노출)
experiment: 목표 = 변형 간 지표 비교 (통계적 결론)
연계 흐름:
1. 새 기능을 flag 뒤에 둠
2. 50/50 으로 나눠 A/B 실험 (control vs new)
3. new 가 지표 우위 + 통계적 유의 → 의사결정
4. 이긴 변형으로 100% rollout
5. flag 제거 (코드에서 분기 삭제)
주의: 한 사용자가 동시에 여러 실험에 들어가면 상호작용으로 결과 오염.
→ 직교 배정(orthogonal assignment) 또는 mutually-exclusive layer 로 분리.
실험 통계(표본 크기·유의수준·peeking)는 how-ab-testing-actually-works 참조.flag 평가 — 서버 vs 클라이언트
서버 사이드 평가:
- 서버가 flag 값을 결정해 결과(또는 평가된 UI)를 내려줌
- 장점: flag 규칙·미출시 기능이 클라이언트에 노출 안 됨 (보안)
- 장점: 즉시 일관 (서버가 단일 진실)
- 단점: 평가가 요청 경로에 있어 latency 영향 가능
클라이언트 사이드 평가:
- 클라이언트가 flag 정의(규칙)를 받아 직접 평가
- 장점: 네트워크 왕복 없이 즉시 분기 (빠른 UI 전환)
- 단점: 미출시 기능 코드·flag 규칙이 번들에 노출됨
- 단점: 사용자마다 flag 갱신 시점이 달라 일관성 문제
엣지/SDK 캐싱:
- SDK 가 flag 정의를 polling/streaming 으로 받아 로컬 캐시
- 평가는 로컬(빠름) + 정의는 중앙 관리 → 둘의 절충일관성 함정
함정 1 — flag 전파 지연:
flag 을 off 로 바꿔도 SDK 캐시 TTL/polling 주기만큼 지연.
kill-switch 인데 30 초 polling 이면 → 30 초간 장애 지속.
→ kill-switch 는 streaming(즉시 push) 또는 짧은 TTL 필수.
함정 2 — 같은 요청 안 일관성:
한 요청 처리 중 flag 을 여러 번 평가하는데 그 사이 값이 바뀌면
앞부분은 새 로직, 뒷부분은 옛 로직 → 깨진 상태.
→ 요청 시작 시 flag 을 한 번 snapshot 해서 그 요청 내내 고정.
함정 3 — 서버/클라 불일치:
서버는 new, 클라는 (캐시 지연으로) old 로 평가 → UI 와 API 미스매치.
→ 평가 위치를 한쪽으로 통일하거나, 클라가 서버 결정을 따르게.
함정 4 — flag 서비스 장애 시 fallback:
flag 서비스가 죽으면? 기본값(default) 이 명확해야 함.
→ 새 기능 default=off, kill-switch 의 "정상" default=on (fail-safe).
서비스 불가 시 안전한 쪽으로 떨어지도록 default 설계.flag 부채 — 쌓이면 코드가 썩는다
문제:
출시 끝난 release flag 을 안 지우면:
if (flags.newCheckoutV1) {...} else {...} ← V1 은 이미 100%
if (flags.newCheckoutV2) {...} else {...} ← 그 위에 또
→ 죽은 분기(dead branch)가 쌓여 코드 가독성·테스트 폭증
→ "이 else 는 언제 타지?" 아무도 모름 → 수정 두려움
조합 폭발:
flag 10 개면 이론상 2^10 = 1024 가지 상태 조합.
대부분 테스트 안 됨 → 예측 못 한 상호작용 버그.
정리 규칙:
- release flag 에 만료일/티켓 부착 → 100% 도달 후 즉시 제거 작업
- "flag 제거"를 출시 체크리스트에 포함 (출시 ≠ 완료, 정리까지가 완료)
- 주기적 flag 감사 — 오래된 flag, 항상 같은 값인 flag 찾아 제거
- kill-switch·permission 은 영구 → 부채 아님 (구분해서 추적)구현 — 좋은 flag 시스템의 요건
- 중앙 관리 + 즉시 변경 — 코드 재배포 없이 대시보드 /API 로 flag 토글. 변경이 초 단위로 반영.
- 타게팅 규칙 — % 뿐 아니라 사용자 속성(플랜·지역 ·버전)으로 대상 지정.
- sticky bucketing — 같은 사용자=같은 결과 (해시 기반).
- 감사 로그 — 누가 언제 어떤 flag 을 바꿨나 (장애 원인 추적).
- 안전한 default — flag 서비스 장애 시 떨어질 기본값 명시.
- 유형/만료 메타데이터 — flag 부채 추적용.
흔한 함정
- flag 을 안 지움 — release flag 부채가 죽은 분기로 쌓여 코드를 썩힌다. 만료일·정리 작업을 출시 프로세스에 박아라.
- sticky 하지 않은 bucketing — 매 요청 랜덤이면 같은 사용자가 UI 를 오가며 깜빡이고 A/B 측정이 오염된다. 사용자ID 해시로 고정.
- kill-switch 의 전파가 느림 — polling 주기만큼 장애 지속. kill-switch 는 streaming/즉시 push 또는 짧은 TTL.
- 한 요청 안에서 flag 값이 바뀜 — 요청 시작 시 snapshot 해서 그 요청 내내 고정해야 일관.
- flag 규칙을 클라에 노출 — 미출시 기능·내부 규칙이 번들에 들어가 유출. 민감한 건 서버 평가.
- flag 서비스 장애 시 default 미정의 — 서비스가 죽으면 앱이 깨진다. 새 기능 off, kill-switch 정상값으로 fail-safe.
- 겹치는 실험의 상호작용 무시 — 한 사용자가 여러 실험에 동시에 들면 결과 오염. mutually-exclusive layer 로 분리.
마무리
feature flag 의 본질은 배포와 릴리스의 분리다. 코드는 안전하게 먼저 배포하고, 노출은 점진적으로(% 또는 ring) 켜며, 문제가 생기면 재배포 없이 초 단위로 회수한다. A/B 실험도 같은 bucketing 인프라 위에 얹힌다.
함정은 대부분 두 곳에 있다 — 일관성(sticky bucketing, 요청 내 snapshot, kill-switch 의 빠른 전파, 안전한 default)과 flag 부채(출시 후 즉시 제거). flag 유형을 구분해 release 는 짧게 살리고, kill-switch·permission 은 영구로 추적하는 것이 건강한 flag 운영의 핵심이다.