기능 플래그
고친 사람 github-actions[bot]
기능 플래그는 코드를 운영에 올리는 일과 그 기능을 사용자에게 여는 일을 따로 떼어 놓습니다. 아직 덜 끝난 새 결제 화면도 조건문으로 감싸 꺼 둔 채로 올려 둡니다. 그 조건이 참인지 거짓인지는 코드 바깥에서 바꿉니다. 문제가 나면 다시 배포하지 않고 그 기능만 끕니다.
쉽고 빠른 이해
기능 플래그는 새로 만든 기능 앞에 스위치를 하나 달아 둡니다. 새 결제 화면을 만들었다면 스위치를 꺼 둔 채 배포해 두었다가, 준비가 되는 날 스위치만 켭니다.
스위치가 없으면 덜 끝난 기능을 올릴 방법이 없습니다. 기능이 다 될 때까지 코드는 따로 낸 브랜치에 머뭅니다. 몇 주 만에 합치려 들면 고칠 것이 잔뜩 나옵니다. 그 기다림을 없애려고 올리는 일과 여는 일을 떼어 놓습니다.
- 새 코드와 옛 코드를 둘 다 두고 조건문으로 가릅니다
- 그 조건값은 코드 밖의 설정이나 데이터베이스에서 읽습니다
- 값을 바꾸면 다시 배포하지 않고 기능이 열리거나 닫힙니다
대가는 조건문이 코드에 남는다는 것입니다. 플래그가 늘수록 거쳐 갈 수 있는 경로가 불어나고, 다 쓴 플래그를 걷어내지 않으면 아무도 손대지 못하는 옛 코드가 남습니다.
상세
배포와 릴리스를 갈라 놓는다
배포는 만든 코드를 운영 서버에 올려 두는 일입니다. 릴리스는 그 기능을 사용자가 쓸 수 있게 여는 일입니다. 두 일을 한꺼번에 하면 코드가 올라가는 순간이 곧 기능이 열리는 순간입니다.
묶여 있으면 덜 끝난 기능은 올릴 수 없습니다. 기능이 끝날 때까지 따로 낸 브랜치에 코드를 쌓아 둡니다. 몇 주 떨어져 있던 코드를 뒤늦게 합치면 그사이 바뀐 것들과 부딪혀 머지 충돌이 커집니다.
기능 플래그는 그 묶음을 풉니다. 덜 끝난 기능도 꺼 둔 채 공용 브랜치에 합칩니다. 매일 배포합니다. 기능을 여는 시점은 따로 고릅니다.
두 방식에서 배포와 릴리스가 각각 몇 번 일어나는지 견주면 이렇습니다.
flowchart TD
subgraph G1["묶여 있을 때"]
A["코드 완성"] --> B["배포 = 릴리스"]
end
subgraph G2["떼어 놓았을 때"]
C["배포 1 · 꺼짐"] --> D["배포 2 · 꺼짐"]
D --> E["배포 3 · 꺼짐"]
E --> F["릴리스 · 켠다"]
end
묶여 있으면 배포 한 번이 곧 릴리스 한 번입니다. 떼어 놓으면 배포는 여러 번 하고 릴리스는 그 가운데 고른 한 시점입니다. 짧게 쪼개 자주 합치는 트렁크 기반 개발이 이 장치 위에서 돕니다.
코드 안의 스위치
모양은 조건문 하나입니다. 새 코드와 옛 코드를 둘 다 남겨 둡니다. 어느 쪽으로 갈지는 값 하나가 정합니다.
if (flags.on("new-checkout")) { // 켜짐
newCheckout(); // 새 코드
} else {
oldCheckout(); // 옛 코드
}
값이 참이면 새 결제 코드가 돌고, 거짓이면 옛 코드가 돕니다. 두 코드가 모두 운영 서버에 올라가 있습니다. 사용자가 무엇을 보는지는 저 값 하나가 정합니다.
이 스위치를 기능 토글이라고도 부릅니다. 영어로도 feature flag 와 feature toggle 을 섞어 씁니다. 가리키는 것은 같습니다.
값을 코드 밖에 두는 까닭
값을 코드에 상수로 박아 두면 스위치를 켤 때마다 빌드와 배포를 다시 해야 합니다. 그러면 배포하지 않고 기능을 연다는 목적이 사라집니다. 그래서 값은 코드 밖에 둡니다 — 설정 파일, 데이터베이스 테이블, 플래그만 다루는 별도 서비스 가운데 하나입니다.
설정 파일이든 테이블이든 별도 서비스든, 코드가 보기에는 값 하나를 읽어 오는 플래그 저장소입니다. 요청 하나가 이렇게 지나갑니다.
flowchart TD
A["요청이 들어온다"] --> B{"캐시에 값이 있나"}
B -->|있다| E{"켜짐인가"}
B -->|없다| C["플래그 저장소에서 읽는다"]
C --> D{"읽기에 성공했나"}
D -->|예| E
D -->|"아니오 · 꺼짐으로 본다"| H
E -->|켜짐| F["새 코드"]
E -->|꺼짐| H["옛 코드"]
요청이 올 때마다 값을 읽습니다. 저장소의 값을 바꾸면 그다음 요청부터 다른 코드가 돕니다.
대신 요청 하나마다 조회가 한 번 늘어납니다. 그래서 대개 값을 짧게 캐시해 두고 주기적으로 새로 읽습니다. 캐시에 값이 있는 요청은 저장소까지 가지 않습니다.
저장소가 멈췄을 때 어떤 값으로 돌지도 미리 정해 둡니다. 읽기에 실패하면 꺼짐으로 보는 것이 기본 선택입니다. 아직 안 열린 기능이 사고로 열리는 쪽을 피하려는 것입니다.
누구에게 켤지도 고른다
값이 참과 거짓 둘뿐일 필요는 없습니다. 요청에 실린 사용자 정보를 보고 사람마다 다르게 답할 수 있습니다. 사내 직원에게만 열거나, 전체 사용자 가운데 일부에게만 여는 식입니다.
저장소도 하나이고 플래그도 하나인데 답이 사람마다 갈립니다. 규칙을 이렇게 씁니다.
flowchart TD
A["요청 · 사용자 정보"] --> B["플래그 규칙"]
B --> C{"사내 직원인가"}
C -->|예| D["켜짐"]
C -->|아니오| E{"지정한 일부에 드는가"}
E -->|예| D
E -->|아니오| F["꺼짐"]
규칙은 위에서부터 훑습니다. 사내 직원이면 켜짐입니다. 아니면 지정한 일부에 드는지 보고, 들면 켜짐이고 안 들면 꺼짐입니다.
여는 범위를 이렇게 조금씩 넓히는 것이 점진적 롤아웃입니다. 아주 적은 수에게 먼저 열어 두고, 문제가 없으면 범위를 키우다 끝내 전체에 엽니다.
카나리 배포와 자주 같이 씁니다. 다만 가르는 것이 다릅니다 — 플래그가 가르는 것은 사용자이고, 카나리 배포가 가르는 것은 서버에 올라간 판입니다.
플래그 값을 사용자마다 다르게 주면 A/B 테스트도 이 위에서 만듭니다. 사용자를 두 무리로 갈라 어느 쪽 반응이 나은지 재는 방식입니다.
플래그가 하는 일은 넷으로 갈린다
이름은 하나지만 쓰임이 다릅니다. 무엇을 가르려고 켜는지에 따라 네 갈래로 나눕니다. 갈래마다 플래그가 사는 기간이 다릅니다.
| 갈래 | 무엇을 가르나 | 얼마나 사나 |
|---|---|---|
| 릴리스 토글 | 덜 끝난 기능을 감춘다 | 짧다. 열고 나면 걷는다 |
| 실험 토글 | 사용자를 갈라 반응을 잰다 | 실험이 끝날 때까지 |
| 운영 토글 | 부하가 큰 기능을 급할 때 끈다 | 길다. 남겨 둔다 |
| 권한 토글 | 정해진 사용자에게만 연다 | 제품이 사는 동안 |
위 둘은 임시입니다. 목적을 이루면 조건문을 지웁니다. 아래 둘은 오래 삽니다. 운영 토글은 장애가 났을 때 끄는 스위치이고, 권한 토글은 유료 사용자에게만 여는 기능처럼 제품 규칙 자체입니다.
임시인 것과 오래 사는 것을 안 가르면 코드에 조건문만 남습니다. 플래그를 심을 때 어느 갈래인지 적어 두면 걷을 때가 왔는지 나중에 판단할 수 있습니다.
걷어내는 것까지가 한 벌
플래그는 심을 때 태어나 걷을 때 죽습니다. 이 한살이를 안 끝내면 기능은 열렸는데 조건문만 남은 코드가 됩니다.
stateDiagram-v2
[*] --> 꺼짐: 플래그를 심어 배포한다
꺼짐 --> 켜짐: 기능을 연다
켜짐 --> 꺼짐: 문제가 나면 되돌린다
켜짐 --> 걷힘: 조건문과 옛 코드를 지운다
걷힘 --> [*]
켜짐에서 꺼짐으로 돌아가는 줄이 이 장치의 값어치입니다. 배포한 것을 직전 판으로 되돌리는 일을 롤백이라고 합니다. 롤백을 하면 같은 배포에 실려 나간 다른 변경까지 함께 사라집니다. 플래그가 있으면 배포는 그대로 두고 문제 난 기능 하나만 닫습니다.
걷힘으로 가는 줄은 자주 잊힙니다. 기능이 열리고 나면 급한 일이 사라지기 때문입니다. 심을 때 걷을 날짜를 같이 적어 둡니다. 그 날짜가 지난 플래그를 모아 보여 주는 팀도 많습니다.
값을 치르는 곳
조건문 하나가 코드 경로를 둘로 가릅니다. 플래그를 하나 더 심으면 앞서 갈린 경로가 저마다 또 둘로 갈립니다. 경로 수가 플래그 하나마다 배로 불어난다는 뜻입니다.
flowchart TD
subgraph L0["플래그 0개"]
A["경로 1"]
end
subgraph L1["플래그 1개"]
B["경로 2"]
end
subgraph L2["플래그 2개"]
C["경로 4"]
end
subgraph L3["플래그 3개"]
D["경로 8"]
end
subgraph L10["플래그 10개"]
E["경로 1024"]
end
A --> B --> C --> D --> E
플래그가 열 개면 켜고 끈 조합이 천 가지를 넘습니다. 그중 실제로 테스트하는 것은 몇 가지뿐입니다.
테스트는 조합을 다 돌지 않습니다. 대개 지금 켜 둔 값으로 한 벌, 다음에 켤 값으로 한 벌만 돌립니다.
이 두 벌로 덮으려면 플래그가 서로 얽혀 있으면 안 됩니다. 한 플래그의 값이 다른 플래그의 값에 따라 달라지게 짜지 않습니다. 플래그 하나는 자기 조건문만 가릅니다.
다 쓴 플래그를 안 걷으면 옛 코드가 계속 남습니다. 몇 달 지나면 그 코드가 왜 있는지 아는 사람이 없어집니다. 이렇게 쌓인 것이 기술 부채입니다.
값을 읽는 코드가 여기저기 흩어지는 것도 대가입니다. 조건문을 화면·업무 로직·데이터 접근에 다 흩어 놓으면 기능 하나를 끄는 데 손댈 곳이 늘어납니다. 읽는 곳을 한 군데로 모아 두면 걷어낼 때 한 번에 지웁니다.
플래그로 못 되돌리는 것
스위치를 끄면 코드 경로는 돌아옵니다. 이미 일어난 일은 안 돌아옵니다. 메일이 나갔거나 결제가 잡혔으면 플래그를 꺼도 그 기록은 남습니다.
데이터베이스 구조를 바꾸는 변경도 마찬가지입니다. 칼럼을 지운 뒤에 플래그를 꺼도 지워진 칼럼은 안 돌아옵니다. 그래서 구조 변경은 확장-수축 패턴처럼 옛 모양과 새 모양이 함께 사는 단계를 두고 옮깁니다.
플래그가 덮는 것은 코드가 어느 길로 가느냐까지입니다. 밖으로 나간 것과 저장된 모양은 다른 절차가 맡습니다. 이 선을 미리 긋지 않으면 끄기만 하면 된다고 믿다가 못 되돌립니다.
관련 항목
기능 플래그의 하위 종류
릴리스 토글 · 실험 토글 · 운영 토글 · 권한 토글
기능 플래그와 함께 쓰는 배포 방식
지속적 배포 · 지속적 전달 · 지속적 통합 · 트렁크 기반 개발 · 배포 파이프라인 · 배포
사용자 일부에게만 기능을 여는 방법
카나리 배포 · 점진적 롤아웃 · 단계적 출시 · 다크 런칭 · A/B 테스트 · 링 배포
배포를 되돌릴 때 대신 쓰는 수단
롤백 · 되돌리기 · 블루-그린 배포 · 롤링 업데이트 · 핫픽스
오래 사는 브랜치를 피하는 다른 방법
브랜치 바이 앱스트랙션 · 키스톤 인터페이스 · 병렬 변경 · 확장-수축 패턴 · 브랜치 전략
플래그 값을 두는 저장 수단
설정 파일 · 환경 변수 · 설정 관리 · 동적 설정 · 분산 캐시
플래그가 쌓일 때 생기는 문제
기술 부채 · 죽은 코드 · 머지 충돌 · 설정 드리프트
기능을 연 뒤 반응을 지켜보는 수단
관측 가능성 · 모니터링 · 지표 · 로그 집계 · 오류 예산
배포와 복구를 재는 지표
배포 빈도 · 변경 실패율 · 서비스 복구 시간 · 평균 복구 시간 · 배포 재작업률
기능 플래그가 속하는 상위 분류
다른 이름: feature flag · feature toggle · 기능 토글 · 피처 플래그 · 기능 스위치