본문으로 건너뛰기
yutils

feature flag 는 어떻게 동작할까?

flag 유형(release · ops/kill-switch · experiment · permission), 점진 rollout(% · ring), A/B 테스트 연계, flag 부채와 정리, 서버 vs 클라 평가와 일관성 함정.

약 10분 읽기

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 운영의 핵심이다.

가이드 목록으로