Moment Note

피처 플래그로 배포와 릴리스 분리하기

CI/CD & DevOps ·

배포(deploy)와 릴리스(release)를 같은 것으로 취급하면, 코드를 프로덕션에 올리는 순간이 곧 사용자에게 기능이 노출되는 순간이 됩니다. 이 결합은 릴리스 타이밍을 배포 파이프라인에 묶어버리고, 문제가 생겼을 때 롤백을 위해 다시 빌드하고 다시 배포하는 값비싼 절차를 강요합니다. 피처 플래그(feature flag)는 이 둘을 분리합니다. 코드는 언제든 프로덕션에 배포하되, 기능이 켜지는 시점은 런타임 스위치로 따로 통제하는 것이죠.

이 글에서는 피처 플래그로 배포와 릴리스를 분리하는 실전 패턴을 다룹니다. 단순 on/off를 넘어 점진적 롤아웃, 킬 스위치, 플래그 수명 관리까지 짚습니다. 도구는 특정 SaaS에 종속되지 않는 방식으로 설명하되, 오픈소스인 Unleash와 OpenFeature 표준을 예시로 사용합니다.

배포와 릴리스는 왜 분리되어야 하는가

트렁크 기반 개발과 지속적 배포를 도입하면 하루에도 수십 번 프로덕션에 코드가 올라갑니다. 하지만 모든 커밋이 곧바로 사용자에게 노출되어야 하는 것은 아닙니다. 아직 완성되지 않은 기능, 마케팅 일정에 맞춰야 하는 기능, A/B 테스트가 필요한 기능은 코드가 올라가 있어도 꺼진 상태여야 합니다.

피처 플래그를 쓰면 다음이 가능해집니다.

  • 미완성 기능을 main 브랜치에 병합하되 사용자에게는 숨김 (long-lived 브랜치 회피)
  • 문제 발생 시 재배포 없이 즉시 기능만 끄는 킬 스위치
  • 내부 직원 → 1% → 10% → 100% 순의 점진적 노출
  • 특정 세그먼트(베타 유저, 특정 지역)에만 선노출
# 릴리스가 배포에 묶인 전통적 흐름
git merge feature/new-checkout
deploy production        # 배포 = 릴리스, 되돌리려면 재배포

# 피처 플래그로 분리한 흐름
git merge feature/new-checkout   # 코드는 프로덕션에 있지만 flag=off
deploy production
# 준비되면 대시보드에서 flag=on (재배포 없음)

가장 단순한 플래그부터 시작하기

도구를 도입하기 전에, 플래그의 본질은 조건 분기라는 점을 이해하는 게 좋습니다. 환경변수 기반의 간단한 플래그로도 배포/릴리스 분리의 절반은 얻을 수 있습니다.

import os

def is_enabled(flag: str) -> bool:
    return os.getenv(f"FF_{flag.upper()}", "false").lower() == "true"

def checkout(cart):
    if is_enabled("new_checkout"):
        return new_checkout_flow(cart)
    return legacy_checkout_flow(cart)

하지만 환경변수 방식은 한계가 명확합니다. 값을 바꾸려면 재배포 또는 최소한 프로세스 재시작이 필요하고, 사용자별 타게팅이나 퍼센티지 롤아웃을 할 수 없습니다. 런타임에 원격으로 값을 바꾸고, 컨텍스트 기반으로 평가하려면 플래그 관리 시스템이 필요합니다.

OpenFeature로 벤더 종속 피하기

플래그 SDK를 특정 벤더에 직접 붙이면 나중에 도구를 바꿀 때 코드 전체를 손대야 합니다. OpenFeature는 CNCF 표준 인터페이스로, 애플리케이션 코드는 표준 API를 쓰고 뒤에서 provider만 교체하는 구조를 제공합니다.

from openfeature import api
from openfeature.contrib.provider.flagd import FlagdProvider

api.set_provider(FlagdProvider())   # provider만 교체 가능
client = api.get_client()

def checkout(user, cart):
    ctx = {
        "targetingKey": user.id,
        "plan": user.plan,
        "country": user.country,
    }
    if client.get_boolean_value("new-checkout", False, ctx):
        return new_checkout_flow(cart)
    return legacy_checkout_flow(cart)

핵심은 get_boolean_value("new-checkout", False, ctx)의 두 번째 인자인 기본값입니다. 플래그 서버에 연결하지 못하거나 플래그가 정의되지 않았을 때 이 값이 반환됩니다. 신규 기능의 기본값은 항상 안전한 쪽(대개 False)으로 두어, 장애 시 자동으로 기존 동작을 유지하도록 설계합니다.

점진적 롤아웃과 타게팅

플래그의 진짜 가치는 on/off가 아니라 누구에게 켤지를 세밀하게 통제하는 데 있습니다. Unleash를 예로 들면, 하나의 플래그에 여러 전략(strategy)을 붙여 점진적으로 노출합니다.

# Unleash 플래그 정의 (개념적 표현)
name: new-checkout
enabled: true
strategies:
  - name: gradualRolloutUserId    # 사용자 ID 해시 기반 안정적 롤아웃
    parameters:
      percentage: "10"            # 10%에게 노출
      groupId: new-checkout       # 같은 사용자는 항상 같은 결과
  - name: userWithId              # 내부 직원은 항상 노출
    parameters:
      userIds: "qa-01,qa-02,pm-lead"

퍼센티지 롤아웃에서 중요한 것은 일관성입니다. 사용자 ID를 해시해서 버킷을 정하기 때문에, 같은 사용자는 요청마다 같은 결과를 받습니다. 이게 없으면 새로고침할 때마다 UI가 바뀌는 최악의 경험이 됩니다. groupId는 여러 플래그가 같은 사용자 집합에 겹쳐 노출되는 것을 분산시키는 시드 역할을 합니다.

킬 스위치와 관측성

피처 플래그의 방어적 활용이 바로 킬 스위치입니다. 신규 결제 연동, 외부 API 호출, 무거운 쿼리처럼 장애 파급이 큰 코드 경로를 플래그로 감싸두면, 문제가 터졌을 때 재배포 없이 몇 초 만에 차단할 수 있습니다.

if client.get_boolean_value("call-fraud-scoring-v2", False, ctx):
    score = fraud_service.score(txn)     # 외부 의존, 장애 시 이 플래그를 off
else:
    score = fallback_rule_engine(txn)    # 항상 살아있는 대체 경로

단, 플래그를 켜고 끌 때는 반드시 지표와 함께 봐야 합니다. 롤아웃 퍼센티지를 올릴 때 에러율, 지연시간, 전환율을 대조하지 않으면 문제를 늦게 발견합니다. 로그에 플래그 평가 결과를 함께 남기고, 대시보드에서 “플래그 on/off 코호트별 지표”를 비교할 수 있게 해두는 것이 좋습니다.

logger.info(
    "checkout_completed",
    extra={
        "flag_new_checkout": enabled,   # 코호트 구분 키
        "latency_ms": elapsed,
        "user_id": user.id,
    },
)

플래그 부채 관리: 만들었으면 지워라

피처 플래그의 가장 큰 함정은 정리하지 않는 것입니다. 롤아웃이 100% 완료되어 영구히 켜진 플래그가 코드에 그대로 남으면, 죽은 분기와 조건문이 쌓여 코드가 스파게티가 됩니다. 릴리스용 임시 플래그(short-lived)와 운영 스위치용 영구 플래그(permanent)를 명확히 구분하고, 임시 플래그에는 만료 정책을 둡니다.

# CI에서 오래된 플래그 참조를 탐지하는 간단한 가드 예시
grep -rEn 'get_boolean_value\("(new-checkout|beta-search)"' src/ \
  && echo "정리 대상 플래그가 아직 코드에 남아있습니다" && exit 1 || true

운영 규칙으로는 다음을 권장합니다.

  • 플래그 생성 시 제거 예정일소유자를 메타데이터로 기록
  • 100% 롤아웃 후 일정 기간이 지나면 플래그와 죽은 분기를 함께 제거하는 정리 티켓 자동 생성
  • 영구 킬 스위치는 별도 태그로 관리해 임시 플래그 정리 대상에서 제외

마무리

피처 플래그는 “배포는 자주, 릴리스는 통제되게”라는 현대 배포 전략의 핵심 도구입니다. 코드를 언제든 프로덕션에 올리되 노출은 런타임에 결정하고, 문제가 생기면 재배포 없이 끄고, 위험한 경로는 킬 스위치로 감싸는 것이 기본 문법입니다. 다만 플래그는 공짜가 아닙니다. 조건 분기가 늘고, 테스트 조합이 복잡해지며, 정리하지 않으면 부채가 됩니다. 안전한 기본값, 일관된 롤아웃, 지표 연동, 그리고 수명 관리까지 갖췄을 때 비로소 플래그가 부담이 아닌 자산이 됩니다.