피처 플래그로 하루 10회 안전하게 배포하는 방법
출처: Dev.to
내 이전 글들을 따라오셨다면, 저는 트렁크 기반 개발(Trunk‑Based Development)을 강력히 옹호하고, 풀 리퀘스트를 거의 눈에 보일 정도로 작게 만드는 것을 좋아한다는 걸 아실 겁니다. 이상적인 상황이라면, 개발자들은 하루에 여러 번 메인 브랜치에 직접 코드를 머지하고, 모든 것이 매끄럽게 흘러가며, 프로덕션은 견고하게 유지됩니다.
하지만 현실은 그렇지 않죠. 핵심 시스템을 담당하고 있는 백엔드 팀에 이 방식을 설득하려 하면, 거의 언제나 같은 저항에 부딪히게 됩니다.
방 뒤쪽에 있던 누군가가 손을 들어 물어볼 겁니다. “이론적으로는 멋지지만, 저는 현재 레거시 결제 서비스 리팩터링 중이에요. 아키텍처를 크게 바꾸는 데 사흘은 걸릴 거예요. 정말 반쯤 완성된, 깨진 코드를 메인 트렁크에 머지하고 바로 프로덕션에 올려서 실제 고객이 우리 제품을 사게 해야 한다는 말인가요?”
그것은 전혀 무리한 요구가 아닙니다. 미완성 작업을 숨기기 위한 유일한 도구가 오래 유지되는 거대한 피처 브랜치라면, 트렁크 기반 개발은 즉시 무너집니다. 앞서 언급한 악몽, 즉 거대한 코드 리뷰, 고통스러운 머지 충돌, 그리고 실제 환경에 배포되기도 전에 부패하는 코드를 맞이하게 됩니다.
연속적인 배포를 실제로 작동시키면서 매일 오후마다 대규모 프로덕션 장애가 발생하지 않게 하려면, 대부분의 엔지니어링 팀이 동일한 개념이라고 착각하는 배포(Deployment)와 릴리즈(Release) 를 분리해야 합니다.
이번 카테고리의 마지막 글은 트렁크 기반 개발에 초점을 맞추고 있습니다: https://codecraftdiary.com/2026/05/18/trunk-based-development-roadmap/
전통적인 개발 환경에서는 코드를 배포하고 기능을 릴리즈하는 것이 동시에 일어납니다. 거대한 피처 브랜치를 머지하고, CI/CD 파이프라인이 실행되며, 코드가 라이브 서버에 올라가고, 바로 사용자에게 새로운 기능이 보여지는 것이죠.
이 모델은 위험도가 매우 높습니다. 문제가 발생하면 선택지는 전체 배포를 롤백하는 것(다른 개발자의 무관한 수정까지 포함될 수 있음)이나, 고객 지원 티켓이 쌓이는 와중에 급히 핫픽스를 파이프라인에 투입하는 것뿐이며, 경영진이 목을 조이기 시작합니다.
피처 플래그(Feature flags, 혹은 feature toggles) 는 위험 구조를 완전히 뒤바꿉니다.
-
배포(Deployment) 는 코드를 서버에 올리는 행위입니다. 코드는 프로덕션 환경에 존재하고 안전하게 실행되지만, 최종 사용자에게는 보이거나 접근할 수 없습니다. 이는 순수한 기술 작업입니다.
-
릴리즈(Release) 는 그 코드를 사용자에게 활성화하는 비즈니스 결정이며, 배포 일정과는 완전히 독립적입니다.
새 코드를 간단한 조건문으로 감싸면, 미완성 로직이라도 하루에 열 번씩 안전하게 프로덕션에 배포할 수 있습니다. 코드는 물리적으로 서버에 존재하지만 실행 경로는 비활성화돼 있습니다. 이렇게 하면 배포 과정에서 오는 스트레스를 완전히 없앨 수 있습니다.
잠시 과도하게 설계된 엔터프라이즈 프레임워크는 제쳐두고, 일반적인 백엔드 상황에서 이것이 어떻게 적용되는지 살펴보겠습니다. 레거시 결제 게이트웨이 통합을 보다 신뢰할 수 있는 새로운 서드파티 API 제공자로 교체한다고 가정해 보세요.
전체 구현을 한 번에 교체하려면 몇 주가 걸리는 거대한 PR을 만들 필요 없이, 플래그 하나만 도입하면 됩니다. 가장 단순한 형태는 다음과 같습니다.
public class PaymentProcessor {
private final NewPaymentGateway newGateway;
private final LegacyPaymentGateway legacyGateway;
private final FeatureFlagClient flagClient;
public void processPayment(Order order) {
try {
if (flagClient.isFeatureEnabled("use-new-payment-gateway", order.getUserId())) {
newGateway.charge(order);
} else {
legacyGateway.charge(order);
}
} catch (Exception e) {
// Fallback safety net
if (flagClient.isFeatureEnabled("use-new-payment-gateway", order.getUserId())) {
logger.warn("New gateway failed, falling back to legacy for user: " + order.getUserId(), e);
legacyGateway.charge(order);
} else {
throw e;
}
}
}
}
여기서 우리는 단순히 전역 true/false 설정값을 확인하는 것이 아니라, order.getUserId() 를 플래그 클라이언트에 전달하고 있습니다. 이는 실행 시점에 컨텍스트에 따라 평가될 수 있게 해줍니다.
이 설정으로 새 게이트웨이 코드를 20% 정도만 구현된 상태에서도 머지할 수 있습니다. 인터페이스와 기본 구조는 존재하지만, 플래그는 프로덕션 전체에서 꺼져 있습니다. 실제 고객 거래를 위험에 빠뜨리지 않으면서, 실 스테이징 환경이나 숨겨진 프로덕션 경로에서 지속적으로 통합을 테스트할 수 있습니다.
자주 등장하는 반론: “데이터베이스 스키마 변경은 어떻게 플래그로 감출 수 있나요? 스키마 마이그레이션은 플래그만으로는 불가능합니다.”
많은 팀이 여기서 막히게 됩니다. 새 컬럼이 아직 존재하지 않으면 애플리케이션이 바로 크래시하기 때문이죠. 이를 해결하려면 Expand and Contract 패턴을 따라 데이터베이스 전략을 코드 격리와 동시에 진화시켜야 합니다.
Expand and Contract 패턴 단계
-
Expand – 새로운 컬럼이나 테이블을 추가하는 마이그레이션을 배포합니다. 기존 코드는 이를 알지 못하므로 아무 문제도 발생하지 않습니다.
-
Dual Write – 플래그를 이용해 데이터를 기존 컬럼과 새 컬럼에 동시에 기록하지만, 읽기는 아직 기존 컬럼만 합니다. 이렇게 하면 새 스키마가 실 데이터로 채워집니다.
-
Backfill – 백그라운드 스크립트를 실행해 기존 데이터 구조에서 새 구조로 과거 데이터를 복사합니다.
-
Flip the Switch – 플래그를 전환해 새 컬럼에서 읽도록 변경합니다. 성능이 저하되면 즉시 플래그를 다시 0%로 돌려 원상복구할 수 있습니다.
-
Contract – 100% 확신이 서면 플래그를 제거하고, 오래된 코드 경로를 삭제한 뒤, 최종 마이그레이션으로 옛 컬럼을 삭제합니다.
예, 단계가 더 많아집니다. 하지만 무서운 데이터베이스 마이그레이션을 완전히 안전한 일련의 작업으로 바꿔 줍니다.
배포와 릴리즈를 분리하면, 기존의 스테이징 환경을 완전히 대체할 수 있는 배포 워크플로우를 활용할 수 있습니다. 그 중 가장 강력한 것이 카나리 릴리즈(Canary Release) 혹은 점진적 롤아웃 입니다.
플래그 시스템을 퍼센트 기반이나 특정 사용자 속성 기반으로 평가하도록 설정하면, “스위치를 켜고 데이터베이스가 새로운 쿼리 부하에 녹아버리길 바란다”는 위험을 피할 수 있습니다. 새 결제 게이트웨이에 대한 현실적인 롤아웃 계획은 다음과 같습니다.
- Phase 1: 내부 테스트 (QA 티어)
- Phase 2: 카나리 (1% 트래픽)
- Phase 3: 램프업 (10% → 50%)
- Phase 4: 전체 릴리즈 (100%)
10% 단계에서 미묘한 엣지 케이스 버그가 나타나도 당황하지 마세요. 서비스 컨테이너 전체를 롤백할 필요가 없습니다(컴파일·배포에 15분 정도 소요될 수 있음). 플래그 대시보드에 로그인해 토글을 0%로 돌리고, 정상 근무 시간에 차분히 버그를 고치면 됩니다.
피처 플래그를 사용해본 백엔드 엔지니어는 언제나 같은 경고를 합니다: 기술 부채. 플래그를 마법의 막대처럼 여기고 어디에든 남발하면, 코드베이스는 얽힌 조건문 스파게티가 됩니다.
특정 기능이 100% 사용자에게 배포된 뒤 6개월 동안 플래그가 남아 있다면, 이는 연속 배포를 위한 도구가 아니라 아키텍처적 부채가 됩니다. 코드를 읽기 어렵게 만들고, 유닛 테스트 시 여러 플래그 상태를 모킹해야 하며, 죽은 코드 경로가 영원히 남아 있게 됩니다.
시스템이 유지보수 불가능한 미로가 되는 것을 방지하려면, 플래그의 수명 주기에 대한 엄격한 엔지니어링 규율을 수립해야 합니다.