왜 feature flag가 기술부채가 되는가
Feature flag는 배포와 릴리스를 분리하고 점진적 롤아웃을 가능하게 하지만, 대부분의 팀에서 '켜는' 코드는 열심히 짜도 '지우는' 과정은 방치된다. 롤아웃이 100%에 도달한 flag가 코드에 6개월 넘게 남아 있으면 다음 문제가 생긴다.
- 죽은 분기(dead branch)가 늘어 코드 가독성과 테스트 조합 수가 폭증한다.
- 이미 제거된 flag를 코드가 여전히 조회해 런타임에서 기본값으로 조용히 떨어진다.
- flag 관리 시스템(LaunchDarkly, Unleash, 자체 테이블)의 항목이 수천 개로 불어 어떤 게 살아있는지 아무도 모른다.
핵심은 flag에 명시적 수명주기와 만료일이 없기 때문이다. 수명주기를 메타데이터로 강제하고, 만료된 flag를 자동으로 탐지·경고·정리하는 파이프라인이 필요하다.
수명주기 상태 모델 정의
모든 flag를 4가지 상태로 강제한다. 상태 없이 생성된 flag는 CI에서 거부한다.
| 상태 | 의미 | 권장 최대 수명 |
|---|---|---|
| experiment | A/B 실험, 지표 수집용 | 30일 |
| release | 점진 롤아웃 중 | 60일 |
| ops | 킬스위치·서킷브레이커(장기 유지) | 무기한(연 1회 검토) |
| permanent | 정책상 영구(요금제 분기 등) | 무기한 |
대부분의 부채는 experiment/release에서 나온다. ops/permanent만 예외로 두면 자동 정리 대상이 명확해진다.
flag를 메타데이터로 선언하기
코드에 흩뿌리지 말고 단일 매니페스트로 관리한다. 생성일·만료일·소유자를 필수로 둔다.
flags:
checkout_new_pricing:
type: release
owner: payments-team
created: 2026-07-01
expires: 2026-08-30
jira: PAY-1421
emergency_read_only:
type: ops
owner: sre
created: 2026-01-10
expires: null # ops는 만료 없음, 대신 연간 검토
이 매니페스트가 단일 진실 공급원(SSOT)이 되고, 정리 자동화·문서·코드 검증이 모두 이 파일을 참조한다.
만료 flag 탐지 자동화
매니페스트를 읽어 만료일이 지난 flag를 찾는 스크립트를 CI 스케줄 잡으로 매일 돌린다.
import yaml, sys
from datetime import date
flags = yaml.safe_load(open("flags.yaml"))["flags"]
today = date.today()
stale = []
for name, meta in flags.items():
exp = meta.get("expires")
if meta["type"] in ("ops", "permanent") or not exp:
continue
if exp < today:
overdue = (today - exp).days
stale.append((name, meta["owner"], overdue, meta.get("jira")))
for name, owner, days, jira in sorted(stale, key=lambda x: -x[2]):
print(f"STALE {name} owner={owner} {days}d overdue jira={jira}")
sys.exit(1 if stale else 0)
이 잡이 실패(exit 1)하면 소유 팀에게 알림을 보내고, 만료 flag 목록으로 자동 티켓을 생성한다. '언젠가 정리'가 아니라 만료일이 곧 액션 트리거가 된다.
코드와 매니페스트 동기화 검증
매니페스트에 없는 flag를 코드가 조회하거나, 매니페스트에는 있는데 코드에서 이미 사라진 경우를 PR 단계에서 잡는다. 정규식으로 실제 사용처를 뽑아 대조한다.
# 코드에서 실제 참조되는 flag 키 추출
grep -rhoE 'flags\.get\("([a-z_]+)"' src/ \
| sed -E 's/.*"(.*)"/\1/' | sort -u > used.txt
# 매니페스트에 선언된 키
yq '.flags | keys | .[]' flags.yaml | sort -u > declared.txt
# 선언 없이 코드에서만 쓰이는 flag = 차단
comm -23 used.txt declared.txt | grep . \
&& echo "선언되지 않은 flag 사용" && exit 1
# 코드에서 사라졌는데 매니페스트에 남은 flag = 정리 후보(경고)
comm -13 used.txt declared.txt
전자는 빌드를 깨고, 후자는 경고로 남겨 실제 코드 제거 PR과 매니페스트 정리를 연결한다.
안전하게 제거하는 순서
flag 제거는 코드 삭제만으로 끝나지 않는다. 순서를 틀리면 롤백 불가능한 배포가 나간다.
- 먼저 flag 값을 100%로 고정한 상태로 최소 한 배포 주기를 관찰한다(설정만, 코드 유지).
- 코드에서 분기를 제거하되 flag 조회 자체는 마지막에 지운다. 조회를 먼저 지우면 롤백 시 기본값으로 떨어진다.
- 코드 배포가 안정화된 뒤 flag 관리 시스템과 매니페스트에서 삭제한다.
주의점
자동화가 과하면 역효과가 난다. 만료 잡이 자동으로 flag를 삭제하게 만들지 말고, 티켓 생성·알림까지만 자동화하고 실제 제거는 사람이 PR로 승인하게 한다. ops/permanent 예외를 남용해 experiment를 ops로 바꿔 만료를 회피하는 패턴이 흔하니, ops 신규 등록은 SRE 리뷰를 거치게 한다. 마지막으로 만료일을 지나치게 길게 잡으면 자동화가 무의미해진다. 실험은 30일이면 결론이 나야 하고, 안 나면 flag가 아니라 실험 설계를 재검토할 때다.