내부 private repo 작업을 바탕으로 썼다. 외부 접근이 불가한 저장소라 링크 없이, 조직·시스템·사람을 식별할 수 있는 정보는 걷어내고 구조만 남긴다.
피처 플래그 하나를 코드에서 지웠다. 켜진 채로 지웠으니 화면은 그대로여야 한다. 작업 자체는 여섯 파일, 반나절이면 끝나는 일이었다.
그런데 “그대로다” 를 증명하려다 이상한 자리에 도착했다. 새 코드는 플래그를 읽지 않는다. 그래서 플래그가 실제로 꺼졌는지를 그 앱에서는 관측할 수 없다. 내가 바라던 바로 그 성질이, 검증의 사각지대가 됐다.
TL;DR
- 제거를 검증할 때 변경 전·후 코드를 대조하는 것만으로는 “배포된 것이 그 대상에 반응하지 않는다” 를 못 본다.
- 그걸 보려면 같은 배포본에 대상만 토글해야 한다. 그런데 새 코드는 그 대상을 읽지 않으므로, 토글이 실제로 됐는지도 그 앱에서는 알 수 없다. 증거 자리에 내 진술이 들어간다.
- 아직 제거되지 않은 코드가 도는 환경을 대조군으로 두면 그 구멍이 닫힌다. 배포 파이프라인이 그런 환경을 잠깐 공짜로 내준다. 머지되면 닫히는 창이다.
- 곁들여: 근거로 쓴 숫자를 재지 않았다. 타임아웃 상한을 평시 비용처럼 세 군데에 적었고, 재보니 자릿수가 달랐다.
1. 지운 이유는 성능이 아니었다
앱 웹뷰에는 화면을 전체로 쓰라는 URL 파라미터가 있다. 그 파라미터가 붙으면 앱은 자기 헤더를 숨긴다. 헤더가 사라지면 뒤로 갈 수단도 같이 사라지므로, 웹이 대신 백버튼을 그려야 한다.
앞선 작업에서 백버튼 노출 조건을 웹뷰 UA && 그 파라미터 로 정리했다. 그러자 남아 있던 피처 플래그의 성격이 바뀌었다.
웹뷰 UA && 파라미터 && flag
파라미터가 붙은 화면은 이미 “앱이 헤더를 숨긴 화면” 이다. 거기서 백버튼은 유일한 탈출구다. 끄면 사용자가 갇힌다. 끌 수 없는 스위치는 스위치가 아니다.
그래서 지우기로 했다. 조건에서 && flag 한 항만 빠지고, 그 값은 계속 참이었다. 목표는 단 하나였다 — 동작 변경 0.
2. 첫 번째 증명: 코드 대조. 그리고 그것이 답하지 못한 질문
변경 전 커밋과 변경 후 커밋을 각각 로컬에 띄우고, 같은 브라우저 스크립트를 양쪽에 돌렸다. 웹뷰 UA 와 파라미터 유무를 엇갈려 다섯 칸을 만들고, 각 칸에서 백버튼 개수를 단언했다.
| 조합 | 변경 전 | 변경 후 |
|---|---|---|
| 웹뷰 UA + 파라미터 | 1개 | 1개 |
| 웹뷰 UA, 파라미터 없음 | 0개 | 0개 |
| 일반 모바일 웹 | 0개 | 0개 |
다섯 칸 모두 같았다. 여기까지는 깔끔하다. 그런데 이 표가 답하는 질문은 “두 코드가 같은 결과를 내는가” 다. 내가 정말 알고 싶은 것은 그다음이었다.
배포된 것이 그 플래그에 반응하지 않는가.
코드 대조로는 이걸 못 본다. 두 코드를 나란히 돌리는 동안 플래그는 계속 켜져 있었기 때문이다. 변경 전 코드는 flag(참) 을 읽어서 백버튼을 그렸고, 변경 후 코드는 읽지 않고 그렸다. 결과가 같으니 구분되지 않는다.
3. 두 번째 증명: 같은 배포본에 플래그만 토글
테스트 환경에 새 코드를 올린 뒤, 플래그를 끄고 같은 다섯 칸을 다시 돌렸다.
| 조합 | flag ON | flag OFF |
|---|---|---|
| 웹뷰 UA + 파라미터 | 1개 | 1개 |
| 웹뷰 UA, 파라미터 없음 | 0개 | 0개 |
| 일반 모바일 웹 | 0개 | 0개 |
이번엔 축이 다르다. 코드는 고정이고 플래그만 움직였다. 껐는데도 백버튼이 남아 있다 → 배포된 것이 플래그를 읽지 않는다. 구 코드였다면 그 순간 사라졌어야 한다.
여기서 만족하고 기록을 적었다. 그런데 적다 보니 문장 하나가 걸렸다.
flag 를 실제로 껐다는 사실은 요청자의 조작에 의존한다.
그게 맞았다. 내가 관리 화면에서 스위치를 내렸다고 적었을 뿐, 그 앱에서는 플래그 상태를 볼 방법이 없다. 읽지 않게 만드는 것이 목표였으니 당연히 읽지 않고, 읽지 않으니 상태를 알려줄 수도 없다. 증거여야 할 자리에 내 진술이 들어가 있었다.
4. 세 번째: 지워지지 않은 환경을 대조군으로
이 구멍을 어떻게 메울지 한참 헤맸는데, 답은 이미 옆에 있었다. 개발 환경에는 아직 구 코드가 떠 있었다. 변경은 테스트 환경까지만 올라갔고, 개발 환경은 이전 버전을 서빙하고 있었다.
두 환경은 같은 플래그 서버를 본다. 그러니 플래그가 정말 꺼졌다면, 구 코드가 도는 쪽에서는 백버튼이 사라져 있어야 한다.
같은 요청을 두 환경에 보냈다. 웹뷰 UA, 파라미터 있음.
| 환경 | 서빙 코드 | 플래그 조회 | 백버튼 |
|---|---|---|---|
| 개발 | 구 코드 | 읽는다 | 0개 |
| 테스트 | 새 코드 | 안 읽는다 | 1개 |
같은 요청에 두 환경이 다르게 답했다. 이 한 줄이 세 가지를 동시에 확정한다.
- 플래그는 실제로 꺼져 있다. 구 코드가 백버튼을 안 그리는 것이 증거다. 내 진술이 아니라 서버 응답이다.
- 플래그는 원래 그 버튼을 지배하던 것이 맞다. 같은 코드가 켜졌을 때는 그렸고 껐더니 사라졌다. 지우려던 대상이 정확했다는 뜻이다.
- 새 코드는 그 지배에서 벗어났다. 똑같이 꺼진 플래그인데 다르게 답한다.
덤으로 하나 더 보였다. 그 순간 개발 환경의 그 화면은 앱 헤더도 없고 백버튼도 없는 상태였다. 나가는 수단이 없다. 티켓에 “끄면 갇힌다” 고 적어 둔 근거가 눈앞에 재현돼 있었다.
5. 근거로 쓴 숫자를 재지 않았다
플래그를 지우려는 이유로 성능도 적었다. 플래그 조회가 캐시 없이 원격을 왕복하고 타임아웃이 3초라, 응답 시작 시간을 먹는다고.
세 군데에 적었다. PR 본문, 티켓, 커밋 메시지. 그리고 한 번도 재지 않았다.
동료가 “감탄 말고 측정을 해야지” 라고 짚었을 때에야 잴 생각을 했다. 마침 재기 좋은 상태였다 — 개발 환경에는 조회하는 코드가, 테스트 환경에는 조회하지 않는 코드가 떠 있었다. 게다가 조회는 웹뷰 UA && 파라미터 단락평가 뒤에만 일어나니, 같은 서버에서 파라미터 유무만 바꾸면 조회 비용만 분리된다. 조회하지 않는 환경에서 같은 쌍을 재면 “파라미터 유무 자체의 비용” 이 기준선으로 빠진다.
| 환경 | 요청 | 조회 | 중앙값 |
|---|---|---|---|
| 개발 | 웹뷰 UA + 파라미터 | 발생 | 107ms |
| 개발 | 웹뷰 UA, 파라미터 없음 | 없음 | 94ms |
| 테스트 | 위 두 요청 | 없음 | 102ms / 97ms |
(107 − 94) − (102 − 97) ≈ 8ms. 각 15회, 자릿수를 확인하는 수준의 측정이다.
3초 는 타임아웃 상한이지 평시 비용이 아니다. 나는 그만큼 드는 것처럼 읽히게 썼다. “요청마다” 라고 쓴 것도 부정확했다 — 단락평가 때문에 웹뷰이면서 파라미터가 붙은 요청에서만 조회가 일어난다.
주장이 통째로 틀린 건 아니다. 플래그 서버가 느려지거나 죽으면 그 3초가 실제로 드러나는 꼬리 위험은 남는다. 정확한 표현은 “평시 8ms, 장애 시 최대 3초 꼬리” 였다. 그리고 이 작업의 진짜 근거는 성능이 아니라 끌 수 없는 스위치 쪽이었다. 곁가지를 근거처럼 세게 적은 것이 문제였다.
원문을 고치는 대신 측정 결과를 코멘트로 덧붙였다. 틀린 문장을 조용히 지우는 것보다, 무엇을 잘못 적었고 재보니 어땠는지가 남는 편이 낫다고 봤다.
6. 남는 것 — 제거를 증명하는 세 질문
이번 일을 일반화하면 이렇다. 제거 작업에는 관측 불가능성이 내장된다. 읽지 않게 만드는 것이 목표인데, 읽지 않으면 그 대상의 상태를 그 앱에서 알 수 없다. 그래서 검증이 어느 순간 “내가 그렇게 했다” 는 진술로 미끄러진다.
순서대로 세 가지를 물으면 미끄러지지 않는다.
| 질문 | 방법 | 답하지 못하는 것 |
|---|---|---|
| 두 코드가 같은 결과를 내는가 | 변경 전·후 대조 | 배포본이 그 대상에 반응하는지 |
| 배포본이 그 대상에 반응하지 않는가 | 같은 배포본에 대상만 토글 | 토글이 실제로 됐는지 |
| 토글이 실제로 됐는가 | 아직 제거되지 않은 코드가 도는 환경과 대조 | — |
세 번째가 핵심이고, 재료는 대개 이미 있다. 배포 파이프라인이 환경마다 다른 버전을 서빙하는 구간을 잠깐 만들어 주기 때문이다. 그 창은 변경이 전 환경에 퍼지면 닫힌다. 재려면 그 전에 재야 한다.
플래그 졸업뿐 아니라 의존성 제거, 데드 코드 삭제, 폴리필 걷어내기에도 그대로 옮겨진다. 공통점은 하나다 — 없어진 것을 증명하려면, 아직 있는 쪽이 하나 필요하다.
7. 마지막에 바뀐 것 하나
배포가 안정된 뒤 플래그를 저장소에서 아카이브했다. 웹과 실기기 양쪽에서 다시 확인했고 아무것도 바뀌지 않았다. 조회하는 코드가 없으니 당연하다.
다만 롤백 순서가 바뀌었다. 이제 구 이미지로 되돌리면 그 코드는 없어진 플래그를 조회해 “꺼짐” 을 받고, 백버튼이 사라진다. 파라미터가 붙은 화면에서는 나갈 수단이 없어진다.
그러니 롤백이 필요해지면 코드를 되돌리기 전에 플래그를 먼저 되살려야 한다. 아카이브는 되돌릴 수 있는 조작이지만, 되돌릴 순서가 바뀌었다는 사실 자체는 기록해 두지 않으면 사라진다. 티켓에 한 줄로 남겼다.