배포와 릴리스는 같은 일이 아니다

많은 팀이 "배포(deploy)"와 "릴리스(release)"를 한 단어처럼 쓴다. 그러나 둘은 다른 사건이다. 배포는 새 코드를 프로덕션 서버에 올려두는 기술적 행위이고, 릴리스는 그 기능을 사용자에게 실제로 노출하는 비즈니스 결정이다. 이 둘이 묶여 있으면 코드를 올리는 순간 곧바로 사용자에게 노출되므로, 배포 타이밍이 곧 릴리스 타이밍이 된다.

문제는 여기서 생긴다. 새벽에 배포를 몰아서 하거나, 미완성 기능 때문에 브랜치를 몇 주씩 병합하지 못하거나, 장애가 나면 롤백을 위해 다시 빌드·배포 파이프라인을 처음부터 돌려야 한다. 배포와 릴리스를 코드 레벨에서 떼어내면 이 결합을 끊을 수 있다.

왜 결합이 문제가 되는가

배포와 릴리스가 붙어 있으면 세 가지 비용이 발생한다. 첫째, 롤백 지연이다. 문제가 생겼을 때 재배포는 보통 수 분에서 수십 분이 걸리고, 그동안 사용자는 계속 영향을 받는다. 둘째, 큰 병합(big-bang merge)이다. 기능이 끝날 때까지 브랜치를 살려두면 병합 충돌과 통합 위험이 누적된다. 셋째, 배포 창(window)의 경직성이다. 트래픽이 적은 시간에만 배포하려다 보니 배포 빈도가 떨어지고, 한 번의 배포에 담기는 변경량이 커진다.

피처 플래그가 주는 해결

피처 플래그(feature flag)는 코드 경로를 런타임 조건으로 감싸는 스위치다. 기능은 배포하되 플래그를 꺼둔 채로 두고, 준비가 되면 재배포 없이 플래그만 켠다. 이렇게 하면 미완성 코드도 안전하게 main에 병합·배포할 수 있고(trunk-based development), 릴리스는 운영자가 원하는 시점에 설정 변경만으로 수행한다.

def checkout(user, cart):
    if flags.is_enabled("new_pricing_engine", user_id=user.id):
        total = new_pricing.calculate(cart)
    else:
        total = legacy_pricing.calculate(cart)
    return total

핵심은 조건 판단을 배포 산출물이 아니라 외부 설정(플래그 스토어)에서 읽는 것이다. 코드는 두 경로를 모두 담고 있고, 어느 쪽을 탈지는 배포 이후에 결정된다.

점진적 릴리스와 타겟팅

플래그를 단순 on/off로만 쓰면 절반만 활용한 것이다. 사용자 ID 해시로 트래픽 비율을 나누면 1% → 10% → 50% → 100%로 점진 노출(canary)이 가능하고, 특정 조직·플랜·내부 직원에게만 먼저 여는 타겟팅도 할 수 있다.

flags:
  new_pricing_engine:
    default: false
    rules:
      - segment: internal_staff
        value: true
      - rollout:
          attribute: user_id
          percentage: 10   # 사용자 ID 해시 기준 10%

비율 롤아웃은 반드시 안정적인 해시(예: user_id 기준)를 써야 한다. 요청마다 무작위로 판정하면 같은 사용자가 새 경로와 옛 경로를 오가며 일관성이 깨진다.

롤백을 설정 변경으로

가장 큰 실무 이점은 즉시 되돌리기(kill switch)다. 지표가 나빠지면 재배포 없이 플래그를 끄는 것으로 수 초 안에 복구된다. 운영 지표와 플래그를 연결해두면 좋다.

구분재배포 롤백플래그 롤백
소요 시간수 분~수십 분수 초
파이프라인재빌드·재배포 필요설정 변경만
영향 범위배포 단위 전체플래그 단위로 국소적

기술 부채가 되지 않게 관리하기

플래그의 함정은 정리하지 않으면 죽은 코드와 분기가 쌓인다는 점이다. 릴리스가 100%로 안정화되면 플래그와 옛 경로를 제거하는 것까지가 한 사이클이다. 플래그마다 생성일·소유자·만료 예정일을 메타데이터로 남기고, 오래된 플래그를 주기적으로 찾아낸다.

SELECT key, owner, created_at
FROM feature_flags
WHERE state = 'permanent_on'
  AND created_at < NOW() - INTERVAL '60 days'
ORDER BY created_at;

여기서 걸린 플래그는 "이미 릴리스가 끝났으니 코드에서 분기를 제거하라"는 정리 대상 목록이 된다.

도입 시 주의점

몇 가지를 미리 정해두면 사고를 줄인다. 첫째, 플래그 평가 실패 시 기본값은 항상 안전한 쪽(보통 기존 동작)으로 설정한다. 플래그 스토어에 장애가 나도 서비스가 옛 경로로 계속 동작해야 한다. 둘째, 플래그 판정 결과를 로그·트레이스에 남겨 어떤 사용자가 어느 경로를 탔는지 추적 가능하게 한다. 셋째, 릴리스용 단기 플래그와 운영 설정용 장기 플래그를 구분해 수명 정책을 다르게 둔다. 넷째, 서로 얽힌 플래그 조합이 폭발적으로 늘지 않도록, 동시에 실험하는 플래그 수를 제한한다. 이 원칙만 지켜도 배포와 릴리스를 분리하는 효과를 대부분 얻을 수 있다.