Featured image of post [Computer Terms] 피처 플래그 (Feature Flags)

[Computer Terms] 피처 플래그 (Feature Flags)

피처 플래그는 배포와 기능 노출 시점을 분리하는 기법으로, 코드는 배포됐지만 꺼둔 채 카나리·A/B 테스트로 점진적으로 켤 수 있습니다. 플래그가 쌓일 때 생기는 복잡도 문제와 언제 플래그를 정리해야 하는지도 함께 다룹니다.

이 장을 읽기 전에

CI/CD와 테스트 유형에서 다룬 지속적 배포 개념과, 로드 밸런싱에서 언급된 카나리 배포(일부 서버에만 먼저 배포해 문제 여부를 확인하는 방식)를 안다고 가정한다.

배포와 노출을 분리한다는 것

CI/CD와 테스트 유형에서 다룬 지속적 배포는 “테스트를 통과한 코드를 자동으로 운영 환경까지 내보낸다"는 것이었다. 그런데 코드가 운영 서버에 올라갔다는 것과, 그 코드가 실제 사용자에게 보인다는 것은 서로 다른 문제다. 신규 결제 기능처럼 실패 시 손실이 큰 변경을 전체 사용자에게 한 번에 노출했다가 문제가 발견되면, 되돌리기 위해 다시 배포·롤백 절차를 거쳐야 하고 그 사이 이미 많은 사용자가 영향을 받는다. **피처 플래그(Feature Flag)**는 이 문제를 코드 배포 시점과 기능 노출 시점을 분리해서 푼다 — 새 기능 코드는 이미 운영 서버에 올라가 있지만, 조건문 하나로 꺼진 채 존재해 사용자에게는 보이지 않는다.

1
2
3
4
5
6
7
// 피처 플래그로 감싼 신규 결제 기능
function checkout(user, cart) {
  if (featureFlags.isEnabled("new-payment-flow", user)) {
    return newPaymentFlow(user, cart);   // 새 코드 (기본은 꺼짐)
  }
  return legacyPaymentFlow(user, cart);  // 기존 코드
}

이 코드는 이미 운영 환경에 배포되어 있지만, featureFlags.isEnabled(...)false를 반환하는 한 사용자는 기존 결제 흐름만 경험한다. 플래그 값을 외부 설정(플래그 관리 서비스, 환경 변수, 데이터베이스 값)으로 두면, 코드를 다시 배포하지 않고도 이 값을 바꿔 기능을 켜고 끌 수 있다. 배포는 “코드를 서버에 올리는 것"이고 릴리스는 “그 코드를 사용자에게 보이는 것"이라는 두 개념을 분리했을 때 얻는 핵심 이점이 여기 있다 — 배포는 언제든 위험 없이 자주 할 수 있고, 릴리스(노출)만 신중하게 통제하면 된다.

점진적으로 켜기: 카나리와 A/B 테스트

기능을 껐다 켰다만 할 수 있다면 피처 플래그의 가치는 절반뿐이다. 실무에서는 대개 isEnabled(flag, user)처럼 누구에게 보일지를 조건으로 판단해, 전체 사용자가 아니라 일부에게만 먼저 노출한다. 사내 직원에게만 먼저 켜서 실사용 환경에서 검증하거나, 전체 사용자의 5퍼센트에게만 켜서 오류율·응답 시간에 이상이 없는지 지켜본 뒤 점진적으로 비율을 늘리는 방식이다. 로드 밸런싱에서 언급된 카나리 배포가 “일부 서버 인스턴스"를 기준으로 점진적 롤아웃을 하는 것과 대응해서, 피처 플래그는 “일부 사용자"를 기준으로 같은 발상을 코드 레벨에서 구현한다.

graph LR
    A["기능 배포
플래그 OFF"] --> B["내부 직원 대상
플래그 ON"] B --> C["사용자 5%
점진적 확대"] C --> D["사용자 50%"] D --> E["전체 사용자
플래그 ON"] E --> F["플래그 코드 제거"]

같은 메커니즘이 A/B 테스트에도 쓰인다 — 사용자를 무작위로 두 그룹으로 나눠 한쪽에는 기존 버전, 다른 쪽에는 새 버전을 보여주고, 전환율이나 체류 시간 같은 지표를 비교해 어느 쪽이 더 나은지 데이터로 판단한다. 이 경우 플래그는 위험을 줄이는 안전장치가 아니라, 두 버전을 동시에 실사용자에게 노출해 비교하는 실험 도구로 쓰인 것이다. 목적(안전한 롤아웃 vs 실험적 비교)은 다르지만, “배포된 코드 중 어느 경로를 실행할지 런타임에 결정한다"는 구조 자체는 동일하다.

플래그가 쌓이면 생기는 복잡도 문제

피처 플래그는 공짜가 아니다. 조건문 하나를 추가하는 것은 쉽지만, 그 조건문은 코드베이스에 경로(path)를 하나 더 추가하는 것과 같다. 플래그가 10개, 20개 쌓이면 이론적으로 가능한 코드 실행 경로의 조합이 조합적으로 늘어나고, 테스트해야 할 시나리오도 그만큼 늘어난다. 더 심각한 문제는, 롤아웃이 끝나 100퍼센트로 켜진 뒤에도 조건문과 legacyPaymentFlow 같은 옛 코드 경로를 지우지 않고 방치하는 경우다. 아무도 실행하지 않는 죽은 분기가 코드베이스에 계속 남아, 새로 합류한 개발자는 “이 플래그가 지금도 의미가 있는지” 확인하는 데 시간을 쓰게 되고, 결국 어떤 조건문도 함부로 지울 수 없는 상태가 된다.

상황관리 방식리스크
롤아웃 진행 중 (부분 노출)플래그 유지, 모니터링낮음 — 의도된 임시 상태
100% 롤아웃 완료, 문제 없음플래그와 구 코드 경로 즉시 제거제거하지 않으면 기술 부채로 축적
영구적인 설정값(요금제별 기능 차이 등)플래그가 아니라 설정(Configuration)으로 재분류플래그 수명 관리 대상에서 빠짐

흔한 오개념

“플래그는 껐다 켜는 스위치일 뿐 코드 설계와 무관하다” — 플래그로 감싼 신규 경로와 기존 경로가 내부 상태(캐시, 데이터베이스 스키마)를 공유한다면, 두 경로가 서로 다른 가정을 하고 있을 때 플래그를 껐다 켰다 하는 것만으로 데이터 불일치가 발생할 수 있다. 예를 들어 새 결제 흐름이 다른 형식으로 트랜잭션을 기록한다면, 롤백 시 기존 코드가 그 데이터를 제대로 못 읽을 수 있다. 피처 플래그를 안전하게 쓰려면 두 경로가 공유하는 상태까지 설계 단계에서 고려해야 한다.

“롤아웃이 끝나면 플래그는 저절로 사라진다” — 플래그는 코드에 남아 있는 조건문일 뿐이라 아무도 지우지 않으면 영원히 남는다. 실무에서 수백 개의 죽은 플래그가 쌓여 코드 읽기 자체가 어려워지는 사고가 흔히 보고되는 이유가 여기 있다. 롤아웃 완료를 플래그 생성 시점부터 추적하고(만료일, 담당자 지정), 100퍼센트 도달 후 일정 기간이 지나면 플래그와 구 코드 경로를 제거하는 절차를 팀 차원에서 강제하지 않으면 이 문제는 저절로 해결되지 않는다.

다른 개념과의 연결

피처 플래그는 CI/CD와 테스트 유형의 지속적 배포가 “배포는 자주, 노출은 신중하게"라는 원칙으로 이어지도록 만드는 실행 수단이고, 롤아웃 전략은 로드 밸런싱에서 다룬 카나리 배포와 같은 위험 분산 발상을 사용자 단위로 옮긴 것이다. 플래그를 방치했을 때 생기는 복잡도 누적 문제는 리팩토링과 코드 스멜에서 다룬 코드 스멜의 한 형태로 볼 수 있다 — 조건 분기가 죽은 채로 남아있는 것 자체가 제거 대상 신호다.

평가 기준

이 챕터를 읽은 후에는 다음을 할 수 있어야 한다. 배포(Deploy)와 릴리스(Release)를 분리하는 것이 왜 위험을 줄이는지, 피처 플래그가 그 분리를 어떻게 구현하는지 설명할 수 있다. 카나리 롤아웃과 A/B 테스트가 같은 플래그 메커니즘을 서로 다른 목적으로 쓰는 사례임을 설명할 수 있다. 플래그를 제때 제거하지 않았을 때 코드 복잡도와 테스트 부담이 어떻게 늘어나는지 설명할 수 있다.

참고 자료

Hodgson, P. (2017). “Feature Toggles (aka Feature Flags)”. martinfowler.com.