포스트

기능을 끄고 켜며 배포하는 법

피처 플래그로 미완성 기능을 숨기고, 점진적으로 노출하며, 장애 시 즉시 차단하는 킬스위치를 구현하는 원리를 다룬다.

기능을 끄고 켜며 배포하는 법

배포와 릴리스를 분리하고 싶을 때가 있다. 코드는 이미 운영에 올라가 있지만 사용자에게는 아직 보이면 안 되는 기능, 혹은 문제가 생기면 코드 롤백 없이 즉시 꺼야 하는 기능. 이 통제를 가능하게 하는 것이 피처 플래그(feature flag)다.

핵심 개념 — 배포 ≠ 릴리스

피처 플래그의 본질은 “코드 경로를 런타임 설정값으로 분기”하는 것이다. 기능을 새 브랜치에 숨겨 두는 대신, 이미 메인에 병합된 코드를 조건문으로 감싼다. 플래그가 꺼져 있으면 사용자는 기능을 못 본다.

이로써 세 가지가 가능해진다.

  • 트렁크 기반 개발: 미완성 기능도 플래그 뒤에 숨기고 매일 메인에 병합 → 거대한 장기 브랜치 머지 지옥 회피.
  • 점진 노출(canary/percentage rollout): 사용자의 1% → 10% → 100%로 단계적으로 켜며 지표를 관찰.
  • 킬 스위치: 장애 발생 시 재배포 없이 플래그만 꺼서 즉시 차단.

플래그는 성격에 따라 수명이 다르다. 릴리스 토글(기능 출시 후 제거)은 단명해야 하고, 운영 토글/킬스위치는 오래 산다. 이 둘을 구분하지 않으면 코드가 죽은 분기로 뒤덮인다.

flowchart LR
    A["요청"] --> B{"플래그 평가"}
    B -- "off" --> C["기존 경로"]
    B -- "on (1%→100%)" --> D["신규 경로"]
    B -. "장애 시 즉시 off" .-> C

코드 예시

평가 로직을 한 군데로 모은다. 호출부는 단순 boolean만 본다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface FeatureFlags {
    boolean isEnabled(String key, UserContext ctx);
}

@Service
public class CheckoutService {
    private final FeatureFlags flags;

    public Receipt checkout(Cart cart, UserContext ctx) {
        if (flags.isEnabled("new-pricing-engine", ctx)) {
            return newPricing.process(cart);   // 신규
        }
        return legacyPricing.process(cart);    // 기존(폴백)
    }
}

퍼센트 노출은 사용자 식별자를 해싱해 안정적으로 버킷팅한다. 같은 사용자는 항상 같은 그룹에 들어가야 깜빡임이 없다.

1
2
3
4
boolean inRollout(long userId, int percent) {
    int bucket = Math.floorMod(Long.hashCode(userId * 2654435761L), 100);
    return bucket < percent;
}

운영 함정

1) 평가 실패 시 기본값. 플래그 저장소(설정 서버/캐시)가 응답하지 않을 때 무엇을 반환할지 정해야 한다. 신규 기능은 실패 시 off(기존 경로)가 안전한 기본값이다. 예외를 그대로 던지면 플래그 장애가 전체 장애로 번진다.

2) 플래그 부채. 출시 후 영영 안 지운 릴리스 토글이 쌓이면, 켜질 일 없는 분기와 죽은 코드가 늘어 테스트 조합이 폭발한다. 릴리스 토글에는 만료/제거 일정을 함께 관리하라.

핵심 요약

  • 피처 플래그는 배포와 릴리스를 분리해, 숨기기·점진 노출·즉시 차단을 가능케 한다.
  • 평가 로직은 한곳에 모으고, 저장소 장애 시 안전한 기본값(보통 off)으로 폴백한다.
  • 릴리스 토글은 단명시키고 제거 일정을 관리해 플래그 부채를 막는다.
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.