소프트웨어 공학 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 이다. 임시 플래그로 구현하면 지워야 할 때 지울 수 없다.

확인 문제

  1. “배포” 와 “출시” 의 차이를 설명하고, 피처 플래그가 둘을 어떻게 분리하는지 써라.
  2. Hodgson 의 네 가지 토글 종류를 수명과 동적성 관점에서 비교하라.
  3. “토글은 재고다” 라는 말의 의미와, 재고를 줄이는 실천 두 가지를 들어라.
  4. Knight Capital 사례에서 플래그와 관련된 결정적 실수는 무엇이었나?
  5. 플래그 서비스가 다운되었을 때 시스템이 안전하게 동작하려면 무엇을 설계해야 하는가?

풀이

  1. 배포는 코드가 운영 환경에 올라가는 기술적 사건, 출시는 사용자가 기능을 사용할 수 있게 되는 사업적 사건이다. 플래그로 기능을 꺼 둔 채 배포하고, 나중에 설정만 바꿔 출시한다.
  2. Release: 짧고 정적. Experiment: 실험 기간만큼, 요청마다 동적. Ops: 대개 짧지만 일부 킬 스위치는 장기, 동적. Permissioning: 수년, 사용자별로 매우 동적.
  3. 플래그마다 코드 복잡도와 테스트 조합이라는 유지 비용이 생기므로 수를 최소로 유지해야 한다는 뜻. 도입 시 제거 작업을 백로그에 같이 등록, 만료일 설정(지나면 테스트 실패).
  4. 수년 전 중단한 기능의 코드를 남겨 둔 채 그 기능의 플래그를 새 기능에 재사용했다. 한 서버에 새 코드가 배포되지 않자 재사용된 플래그가 옛 코드를 실행했다.
  5. 로컬 캐시된 마지막 설정으로 평가하고, 캐시도 없을 때 쓸 안전한 기본값(대개 새 기능 꺼짐)을 정한다. 플래그 평가가 요청 경로의 동기 원격 호출에 의존하지 않게 한다.

더 읽을거리 (References)