Tech Debt 없는 Feature Toggles, 팀이 숨겨진 함정을 피하는 전략

발행: (2026년 1월 8일 오후 07:47 GMT+9)
11 min read
원문: Dev.to

Source: Dev.to

Feature toggles can feel like magic when you’re rolling out a risky new API endpoint, testing a redesign, or letting product managers demo unfinished work without fear. They give teams confidence to deploy frequently and reduce the blast radius of change. In fast‑moving teams, toggles often become the safety net that enables continuous delivery.

그러나 실제 운영 코드베이스에서 몇 번의 스프린트를 넘겨 작업해 본 적이 있다면, feature flag의 어두운 면을 목격했을 가능성이 높습니다. 일시적인 스위치로 시작된 것이 서서히 영구적인 인프라가 됩니다. 시간이 지나면서 토글은 중첩된 if 문들의 미로, 문서화되지 않은 설정값, 그리고 “잠깐… 이게 프로덕션에서 켜져 있나요?” 라는 긴급한 Slack 메시지로 변질됩니다.

저는 이 양쪽 모두를 경험했습니다. 토글을 추가해 더 빠르게 배포했지만, 나중에 같은 토글 때문에 리팩토링이 두려워지는 대가를 치렀습니다. 이 글은 토글이 많이 사용된 시스템을 구축하고, 디버깅하고, 결국 정리하면서 배운 교훈을 공유합니다. 여러분은 이 혜택을 유지하면서 코드베이스가 부패하지 않도록 할 수 있습니다.

유혹: 플래그 하나만 추가하기

첫 번째로 기능 토글이 필요할 때, 해결책은 명백하고 무해해 보입니다. 설정 파일이나 설정 클래스에 불리언을 하나 추가합니다:

public bool EnableNewCheckoutFlow { get; set; }

또는 더 나빠서, 컨트롤러나 서비스에 바로 조건문을 넣습니다:

if (FeatureFlags.EnableNewCheckoutFlow)
{
    // New logic
}
else
{
    // Old logic
}

그 순간에는 실용적으로 느껴집니다. 빠르고, 동작하며, 과도한 고민을 피할 수 있기 때문이죠. 문제는 이 결정이 거의 항상 고립되지 않는다는 점입니다. 플래그가 퍼집니다. 다른 개발자가 같은 패턴을 복사합니다. 두 번째 플래그가 추가됩니다. 곧 코드베이스에는 의도가 명확하지 않은 수십 개의 조건문이 쌓이게 됩니다.

곧 다음과 같은 상황을 깨닫게 됩니다:

  • 어떤 기능이 활성화되어 있는지 확인할 수 있는 일관된 위치가 없음.
  • 토글이 존재하는 이유를 설명하는 감사 로그가 없음.
  • 언제 제거할 수 있는지에 대한 공동 이해가 없음.

토글은 위험을 줄이기 위해 도입되었지만, 이제는 그 자체가 위험이 되었습니다.

Source:

실제로 잘못되는 경우

1. 부패하는 코드 경로

기능 토글이 출시 후에도 남아 있으면, 사실상 동일한 동작의 두 버전을 유지하게 됩니다. 시간이 지나면 개발자들은 on 경로에 자연스럽게 집중하게 되는데, 이는 사용자가 보는 부분이기 때문입니다. off 경로는 점점 사용되지 않고, 테스트도 되지 않으며, 심지어 읽히지도 않게 됩니다.

결국 토글은 영구적인 레거시가 됩니다. 이를 끄면 시스템이 깨질 것이므로 아무도 제거하려 하지 않습니다. 이 시점에서 토글은 원래 목적을 상실했고, 조용히 유지 보수 부담을 두 배로 늘린 셈입니다.

2. 토글 폭발

공통된 전략이 없으면 각 팀이 토글을 추가하는 자체 방식을 만들게 됩니다. 어떤 팀은 appsettings.json에, 어떤 팀은 환경 변수에, 또 다른 팀은 데이터베이스에, 그리고 소수는 서드‑파티 도구에 저장합니다.

이제 단순히 기능을 디버깅하는 것이 아니라 구성 상태를 디버깅하게 됩니다. 토글이 다른 토글을 제어하는 상황이 생기고, 각 환경에서 시스템이 어떻게 동작하는지에 대한 완전한 정신 모델을 아무도 갖추지 못하게 됩니다.

3. 숨겨진 상태와 놀라움

기능 토글과 관련된 가장 고통스러운 문제 중 하나는 보이지 않는 상태입니다. 기능이 로컬에서는 정상 작동하지만 QA에서는 실패하고, QA에서는 정상인데 프로덕션에서는 깨지는 경우가 있습니다. 근본 원인은 종종 “한 번도 켜본 적 없는 토글”이거나, “누구도 그 결과를 인식하지 못한 채 켜진 토글”입니다.

토글이 코드 외부에 존재하기 때문에, 소스 코드를 읽는 것만으로는 드러나지 않는 동작을 만들게 됩니다. 이는 예상치 못한 상황, 긴급 수정, 그리고 심야 롤백을 초래합니다.

토글이 퇴보하지 않도록 하는 실용적인 접근법

1. 기능 플래그 로직을 중앙화하기

토글에 대한 단일 추상화를 정의합니다:

public interface IFeatureToggleService
{
    bool IsEnabled(string featureName);
}

이 서비스는 애플리케이션이 기능 상태를 확인하는 유일한 방법이 됩니다. 내부적으로는 Azure App Configuration, 데이터베이스, 혹은 다른 제공자를 통해 값을 읽을 수 있습니다. 나머지 코드베이스는 깔끔하고 일관성을 유지합니다.

2. 토글의 수명 주기를 항상 문서화하기

각 토글에는 다음이 포함되어야 합니다:

  • 소유자.
  • 존재 이유.
  • 제거 계획 (예: “v2.3 릴리스 이후 제거”).

토글을 숨겨진 스위치가 아니라 일급 아티팩트로 다루세요.

3. 토글은 일시적인 것으로 간주하기

스프린트 회의에 토글 리뷰를 포함시키세요. 토글이 목적을 달성했으면 제거하고 남은 코드를 삭제합니다. 정리 작업을 예외가 아니라 정상적인 워크플로의 일부로 만들세요.

4. 비즈니스 로직과 토글 검사를 섞지 않기

토글 검사는 경계층—컨트롤러, 애플리케이션 서비스, 혹은 퍼사드 레이어—에서 수행하고 도메인 모델 내부에 파묻히지 않도록 합니다. 이렇게 하면 비즈니스 규칙이 깨끗하게 유지되고 테스트가 쉬워집니다.

5. 환경 인식을 자동화하기

Azure App Configuration, AWS AppConfig, LaunchDarkly와 같은 클라우드 네이티브 도구를 사용해 토글을 환경 전반에 걸쳐 일관되게 관리하고, 감사 로그와 라벨을 활용하세요.

예시: 실제 토글 안티‑패턴 (및 리팩터링)

public IActionResult GetCustomer()
{
    if (ConfigurationManager.AppSettings["EnableNewAPI"] == "true")
    {
        // v2 logic
    }
    else
    {
        // legacy logic
    }
}

public IActionResult GetCustomer()
{
    if (_featureToggleService.IsEnabled("CustomerApiV2"))
    {
        // v2 logic
    }
    else
    {
        // legacy logic
    }
}

리팩터링은 의사결정 로직을 중앙 서비스로 이동시켜 코드 가독성, 테스트 용이성 및 감사성을 향상시킵니다.

트레이드‑오프: 모든 토글이 일시적인 것은 아니다

일부 토글은 본질적으로 영구적입니다—예를 들어, 지역 기반이거나 기업 전용 기능 등. 이러한 토글은 일반적인 토글 시스템에 숨기기보다 정책이나 고객 프로필을 사용해 명시적으로 모델링해야 합니다. 이렇게 하면 개발자인 여러분도 플래그가 존재하는 이유를 잊지 않게 됩니다.

Takeaway

기능 토글은 강력하지만, 의도적으로 관리될 때만 효과적이다. 로직을 중앙화하고, 수명 주기를 문서화하며, 짧은 수명으로 다루고, 비즈니스 로직을 깔끔하게 유지하고, 환경 일관성을 자동화한다. 이러한 습관을 통해 코드베이스가 부패하지 않으면서 속도 향상의 이점을 얻을 수 있다.

우리가 해야 할 일

솔직히 말해서, 우리가 무엇을 해야 할까요?

  • 기능 토글 중앙화
  • 소유권 및 의도 문서화
  • 토글 정기 검토
  • 핵심 로직을 깔끔하게 유지
  • 클라우드‑네이티브 구성 도구 사용
Back to Blog

관련 글

더 보기 »