[SE100 #057] 피처 플래그 — 배포와 출시를 분리하기
소프트웨어 공학 100 주제 시리즈의 57번째 글이다. (카테고리: 형상 관리와 전달)
한 줄 요약
피처 플래그(feature flag, feature toggle)는 코드를 바꾸지 않고 런타임에 동작 경로를 고르는 장치다. 이것으로 배포(deployment, 코드가 운영에 올라감) 와 출시(release, 사용자가 기능을 봄) 를 분리할 수 있다. 대가는 조건 분기라는 부채이며, 플래그는 재고처럼 관리해야 한다.
왜 필요한가
트렁크 기반 개발(SE100 #052 에서 다룬다)을 하면 미완성 기능도 매일 main 에 들어간다. 지속적 배포를 하면 그 main 이 매일 운영에 나간다. 그렇다면 미완성 기능은 어떻게 사용자에게서 숨길까? 마케팅 일정에 맞춰 기능을 공개하려면 그날 배포해야 할까? 새 기능이 부하를 감당하지 못할 때 롤백 말고 다른 방법은 없을까?
피처 플래그는 이 질문들에 하나의 답을 준다. 코드는 미리 배포해 두고, 기능을 켜는 시점과 대상은 설정으로 결정한다. 롤백 대신 플래그를 끄면 된다.
핵심 개념
Hodgson 의 분류: 수명과 동적성
Pete Hodgson 의 Feature Toggles (aka Feature Flags)(martinfowler.com, 2017)는 이 주제의 표준 참고 문헌이다. 그는 모든 플래그를 같은 방식으로 관리하는 것이 위험하다고 하며, 두 축으로 분류한다. 플래그가 얼마나 오래 사는가(longevity) 와 켜고 끄는 결정이 얼마나 동적인가(dynamism).
| 종류 | 용도 | 수명 | 동적성 |
|---|---|---|---|
| Release Toggle | 미완성·미검증 코드를 꺼진 상태로 운영에 배포 | 짧음. 대개 1~2주를 넘기지 말 것 | 정적(릴리스 단위로 같은 결정) |
| Experiment Toggle | A/B·다변량 실험. 사용자를 코호트로 나눠 일관되게 한 경로로 | 통계적 유의성을 얻을 만큼(시간~주) | 매우 동적(요청마다) |
| Ops Toggle | 운영자가 성능 영향이 불확실한 기능을 빠르게 끄거나 낮춤 | 대부분 짧음. 일부는 장기 “킬 스위치” | 동적 |
| Permissioning Toggle | 특정 사용자군(유료, 내부, 베타)에게만 기능 제공 | 매우 김(수년) | 매우 동적(사용자별) |
Hodgson 은 Release Toggle 을 “[기능] 출시를 [코드] 배포와 분리한다” 는 지속적 전달 원칙을 구현하는 가장 흔한 방법이라고 설명한다. 장기 Ops Toggle 은 고부하 시 추천 패널처럼 비싸고 필수적이지 않은 기능을 끄는 수동 서킷 브레이커로 볼 수 있다고도 쓴다.
이 분류가 중요한 이유는 종류마다 구현과 관리 방식이 달라야 하기 때문이다. 정적인 Release Toggle 은 설정 파일이나 환경 변수로 충분하지만, Permissioning Toggle 은 요청마다 사용자 컨텍스트를 보고 결정하는 라우터가 필요하다.
구성 요소
Hodgson 의 글이 쓰는 용어를 정리하면 이렇다.
┌───────────────── 토글 설정(Toggle Configuration) ─────────────┐
│ 파일 / 환경 변수 / DB / 플래그 서비스(관리 UI) │
└───────────────────────────┬──────────────────────────────────┘
▼
요청 + 컨텍스트(사용자, 지역…) ──▶ [토글 라우터(Toggle Router)] ── 결정 ──┐
▼
코드 안의 [토글 지점(Toggle Point)]: if (enabled) A else B
토글 지점이 코드 곳곳에 흩어지면 유지보수가 어려워진다. Hodgson 은 기능 코드가 플래그 시스템을 직접 묻지 않도록 결정 로직을 분리하고(decision point 와 decision logic 분리), 결정을 주입하는 결정의 역전(Inversion of Decision) 을 권한다.
플래그는 재고다
같은 글의 가장 중요한 메시지는 이것이다. 숙련된 팀은 피처 토글을 유지 비용(carrying cost)이 있는 재고(inventory) 로 보고 그 재고를 가능한 한 적게 유지하려고 한다. 실천 방법으로는 Release Toggle 을 도입할 때 제거 작업을 백로그에 함께 넣거나, 토글에 만료일을 두거나, 만료일이 지나면 테스트를 실패시키는 “시한폭탄” 을 두는 방식이 소개된다.
경고 사례: Knight Capital (2012)
플래그 재사용과 죽은 코드가 결합하면 어떤 일이 생기는지, 미국 증권거래위원회(SEC)의 Knight Capital 제재 결정문(Release No. 70694, 2013)이 자세히 기록하고 있다.
- Knight 는 수년 전 사용을 중단한 “Power Peg” 기능의 코드를 운영 시스템에 그대로 남겨 두었다.
- 새 기능(RLP) 코드는 예전에 Power Peg 를 켜던 플래그를 재사용했다. 의도는 Power Peg 코드를 지우고 그 플래그가 새 기능을 켜게 하는 것이었다.
- 배포 중 기술자 한 명이 8대 서버 중 1대에 새 코드를 복사하지 않았고, 이를 확인하는 두 번째 검토자도 절차도 없었다.
- 2012년 8월 1일, 재사용된 플래그가 붙은 주문이 8번째 서버에 도달하자 남아 있던 Power Peg 코드가 동작했다. 약 45분 동안 수백만 건의 주문이 나갔고, Knight 는 4억 6천만 달러가 넘는 손실을 입었다.
- 대응 중 정상 배포된 7대 서버에서 새 코드를 제거하자, 그 서버들에서도 Power Peg 가 동작해 문제가 악화되었다.
교훈은 셋이다. 플래그 이름(의미)을 재사용하지 말 것, 사용하지 않는 플래그와 그 뒤의 코드를 지울 것, 배포가 모든 노드에 일관되게 적용되었는지 검증할 것.
표준화: OpenFeature
플래그 서비스마다 SDK 가 달라 공급자에 묶이는 문제를 줄이기 위해 OpenFeature가 벤더 중립 API 명세를 만든다. OpenFeature 는 CNCF 인큐베이팅 프로젝트이며, 명세는 평가 API, 공급자(Provider), 평가 컨텍스트, 훅(Hooks), 이벤트 등을 정의한다. 애플리케이션은 OpenFeature API 로 코딩하고, 실제 플래그 저장소는 공급자로 교체할 수 있다.
실무 적용
결정 로직을 분리한 토글 (Kotlin)
// 기능 코드는 "무엇을 할지" 만 받는다. 플래그 시스템을 직접 모른다.
class CheckoutService(private val shippingEstimator: ShippingEstimator) {
fun summary(cart: Cart) = Summary(cart.total, shippingEstimator.estimate(cart))
}
interface ShippingEstimator { fun estimate(cart: Cart): EstimatedDate? }
// 결정은 조립 지점 한 곳에서. 플래그를 지울 때 여기만 바꾸면 된다.
fun shippingEstimator(flags: FeatureFlags, ctx: EvalContext): ShippingEstimator =
if (flags.isEnabled("checkout.shipping-estimate.v2", ctx)) NewEstimator()
else NoEstimate
플래그 등록부 템플릿
# flags.yaml — 모든 플래그는 여기 등록하지 않으면 생성 불가 (CI 검사)
- key: checkout.shipping-estimate.v2
type: release # release | experiment | ops | permission
owner: team-checkout
created: 2026-10-01
expires: 2026-10-29 # 지나면 CI 경고 → 2주 뒤 실패
default: false
removal_ticket: CHK-1423
- key: home.recommendations.killswitch
type: ops
owner: team-discovery
expires: never # 장기 플래그는 사유를 적는다
reason: 고부하 시 비필수 기능 차단
운영 원칙 체크리스트
[ ] 플래그 키는 의미가 바뀌면 새 키를 만든다 (재사용 금지)
[ ] 기본값(플래그 서비스 장애 시 값)이 안전한 쪽인가
[ ] 테스트는 플래그 켬/끔 두 경로를 모두 돈다 (적어도 현재 운영 조합 + 출시 예정 조합)
[ ] 플래그 변경도 감사 로그에 남는다 (누가, 언제, 무엇을)
[ ] 출시 완료 후 정해진 기간 안에 플래그와 죽은 경로를 삭제한다
흔한 오해와 함정
- “플래그가 있으니 테스트는 덜 해도 된다.” 플래그 n 개는 이론상 2^n 개의 조합을 만든다. 모든 조합을 테스트할 수는 없으므로, 운영에 실제로 존재할 조합을 정의하고 그것을 테스트해야 한다.
- 모든 플래그를 한 시스템, 한 방식으로. 수명과 동적성이 다른 플래그를 같은 방식으로 다루면 Hodgson 의 경고대로 고통이 따른다.
- 플래그 재사용. Knight Capital 사례의 직접 원인 중 하나다.
- 플래그 서비스를 단일 장애점으로. 요청마다 원격 호출하면 플래그 서비스 장애가 전체 장애가 된다. 로컬 캐시와 안전한 기본값이 필요하다.
- 권한 관리를 Release Toggle 로. 유료 기능 제한은 수년 가는 Permissioning 이다. 임시 플래그로 구현하면 지워야 할 때 지울 수 없다.
확인 문제
- “배포” 와 “출시” 의 차이를 설명하고, 피처 플래그가 둘을 어떻게 분리하는지 써라.
- Hodgson 의 네 가지 토글 종류를 수명과 동적성 관점에서 비교하라.
- “토글은 재고다” 라는 말의 의미와, 재고를 줄이는 실천 두 가지를 들어라.
- Knight Capital 사례에서 플래그와 관련된 결정적 실수는 무엇이었나?
- 플래그 서비스가 다운되었을 때 시스템이 안전하게 동작하려면 무엇을 설계해야 하는가?
풀이
- 배포는 코드가 운영 환경에 올라가는 기술적 사건, 출시는 사용자가 기능을 사용할 수 있게 되는 사업적 사건이다. 플래그로 기능을 꺼 둔 채 배포하고, 나중에 설정만 바꿔 출시한다.
- Release: 짧고 정적. Experiment: 실험 기간만큼, 요청마다 동적. Ops: 대개 짧지만 일부 킬 스위치는 장기, 동적. Permissioning: 수년, 사용자별로 매우 동적.
- 플래그마다 코드 복잡도와 테스트 조합이라는 유지 비용이 생기므로 수를 최소로 유지해야 한다는 뜻. 도입 시 제거 작업을 백로그에 같이 등록, 만료일 설정(지나면 테스트 실패).
- 수년 전 중단한 기능의 코드를 남겨 둔 채 그 기능의 플래그를 새 기능에 재사용했다. 한 서버에 새 코드가 배포되지 않자 재사용된 플래그가 옛 코드를 실행했다.
- 로컬 캐시된 마지막 설정으로 평가하고, 캐시도 없을 때 쓸 안전한 기본값(대개 새 기능 꺼짐)을 정한다. 플래그 평가가 요청 경로의 동기 원격 호출에 의존하지 않게 한다.
더 읽을거리 (References)
- Pete Hodgson, Feature Toggles (aka Feature Flags), martinfowler.com, 2017
- Martin Fowler, FeatureToggle
- Martin Fowler, DarkLaunching
- U.S. SEC, In the Matter of Knight Capital Americas LLC, Release No. 70694, 2013
- OpenFeature, Specification
- Trunk Based Development, Feature Flags