기능을 조건문으로 감싸 두고, 켜고 끄는 것을 배포와 별개로 다루는 방식.
if (flags.isEnabled("new-checkout", userId)) {
return newCheckout(order);
}
return legacyCheckout(order);
배포와 출시는 다른 일이다
- 배포(deploy) — 코드가 서버에 올라간다. 기술적인 일
- 출시(release) — 사용자가 그것을 쓴다. 사업적인 결정
둘을 묶어 두면 출시 일정이 배포 일정을 인질로 잡는다. 마케팅이 다음 주 화요일에 열자고 하면 그때까지 배포를 미루고, 그동안 브랜치가 자라며 통합 지옥이 시작된다.
플래그로 분리하면 코드는 꺼진 채로 계속 합쳐지고, 여는 것은 버튼 하나가 된다.
사고 대응 수단이기도 하다
새 기능이 문제를 일으켰을 때 롤백하지 않고 플래그만 끈다. 재배포가 필요 없으니 초 단위로 끝나고, 다른 정상 변경까지 함께 되돌리는 일도 없다. 되돌림의 단위를 배포 전체가 아니라 기능 하나로 줄이는 것이다.
어떻게 나누나
전체 켜기 / 끄기
비율 — 사용자 5%
속성 — 특정 등급·지역·내부 직원
개별 지정 — 베타 테스터 목록
같은 사용자가 항상 같은 쪽에 배정돼야 한다. 요청마다 무작위로 갈리면 사용자가 화면이 바뀌는 것을 보게 된다. 그래서 보통 사용자 ID 를 해싱해 결정한다 — 같은 입력이면 같은 결과라 안정적이다.
종류를 구분해야 관리가 된다
수명과 목적이 다른 것을 한 통에 담으면 정리가 안 된다.
| 종류 | 수명 | 예 |
|---|---|---|
| 릴리스 토글 | 짧다 — 열면 지운다 | 새 결제 화면 |
| 운영 토글 | 길다 | 부하 시 추천 기능 끄기 |
| 실험 토글 | 실험 기간 | A/B 테스트 |
| 권한 토글 | 영구 | 유료 사용자 전용 기능 |
릴리스 토글만 정리 대상이다. 나머지는 원래 계속 있어야 하는 것이므로 같은 기준으로 지우려 하면 혼란이 생긴다.
어디까지 감싸나
플래그를 함수 안 여기저기에 흩뿌리면 코드가 조건문 덩어리가 된다. 한 지점에서 분기하고 구현을 통째로 갈아 끼우는 편이 낫다 — 인터페이스 하나에 구현 둘을 두고 플래그로 고르는 식이다. 나중에 지울 때도 구현 하나와 분기 하나만 지우면 끝난다.
빚이 쌓인다
플래그의 진짜 비용은 분기가 남는 것이다. 플래그 하나마다 코드 경로가 둘이 되고, 열 개가 쌓이면 조합이 폭발해 테스트가 불가능해진다. 무엇이 켜져 있는 상태를 검증한 것인지 아무도 모르게 된다.
그래서 제거 계획을 만들 때 같이 세운다. 완전히 열린 플래그는 정해진 기간 안에 분기와 함께 지운다. 만료일을 붙여 두고 지난 것을 자동으로 보고하게 하는 팀도 많다.
어디에 두나
플래그 상태를 어디서 읽느냐로 성격이 갈린다.
- 설정 파일·환경 변수 — 가장 단순하다. 대신 바꾸려면 재배포가 필요해 사고 대응 수단으로는 못 쓴다
- 전용 서비스·DB — 실행 중에 바꿀 수 있다. 대신 그것이 새로운 장애 지점이 된다
두 번째를 쓴다면 반드시 정해 둬야 할 것이 있다 — 플래그 서비스가 죽으면 어떻게 되나. 기본값을 로컬에 캐시해 두고 조회 실패 시 마지막 값이나 안전한 기본값을 쓰게 한다. 그러지 않으면 플래그를 도입한 대가로 전체 장애 확률이 올라간다.
면접 함정
- ❌ "플래그를 쓰면 브랜치가 필요 없다" → 브랜치 수명을 줄이는 도구지 대체물이 아니다.
- ❌ "플래그는 공짜" → 분기가 쌓이면 테스트 조합이 폭발한다. 제거가 설계의 일부다.
- ❌ "비율 배정은 요청마다 무작위로" → 사용자별로 고정돼야 한다.