배포 로그의 두 해시가 같았습니다 — 롤백은 몇 달째 무동작이었습니다
배포는 몇 달째 성공으로 끝나고 있었습니다. 로그도 정상이었고 알림도 오지 않았습니다. 그런데 배포 로그 한 줄을 눈으로 읽어 보니 「이전 커밋」과 「지금 커밋」 자리에 같은 값이 찍혀 있었습니다. 헬스체크가 실패하면 이전 커밋으로 되돌아가게 만들어 둔 롤백이, 그동안 같은 커밋으로 되돌아가는 무동작이었다는 뜻입니다.
고친 코드의 양은 작았습니다. 그 작은 변경 전까지 롤백은 존재하지 않는 기능이었습니다.
다른 일을 하다 로그를 열었습니다
안전망을 점검하려던 것이 아니었습니다. 배포할 때 특정 조건에서만 서비스 프로세스를 다시 띄우는 조건부 재시작을 붙이던 중이었습니다. 방금 붙인 조건이 제대로 걸렸는지 보려고 배포 로그를 열었습니다.
거기 이렇게 찍혀 있었습니다.
배포됨 (A → A)「이전 커밋」과 「지금 커밋」이 같은 값이었습니다. 이것이 유일한 신호였습니다. 에러도 없었고, 실패한 배포도 없었고, 알림도 없었습니다. 이 줄을 눈으로 읽기 전까지는 아무도 이상하다고 말해주지 않았습니다.
이 값이 중요한 이유는 롤백이 이 값을 쓰기 때문입니다. 배포 뒤 헬스체크가 실패하면 「이전 커밋」으로 체크아웃해서 다시 띄우는 것이 롤백의 전부입니다. 이전 커밋이 지금 커밋과 같으면 롤백은 지금 커밋을 다시 체크아웃하고 끝납니다. 되돌아가지 않습니다.
값이 아니라 시점이 틀려 있었습니다
처음엔 값을 뽑는 명령이 잘못됐나 봤습니다. 아니었습니다. 명령은 「현재 작업 디렉터리의 HEAD」를 정확히 읽고 있었습니다. 틀린 것은 그 명령이 불리는 시점이었습니다.
워크플로는 새 코드를 먼저 내려받은 뒤 배포 스크립트를 부릅니다. 그리고 「이전 커밋」을 읽는 코드가 배포 스크립트 안에 있었습니다. 순서대로 놓으면 이렇게 됩니다.
워크플로가 새 코드를 내려받습니다. 이 순간 작업 디렉터리의 HEAD 가 새 커밋으로 바뀝니다
워크플로가 배포 스크립트를 부릅니다
배포 스크립트가 「이전 커밋」을 얻으려고 HEAD 를 읽습니다. 이미 새 커밋입니다
이전 커밋과 지금 커밋이 같은 값이 됩니다. 롤백 대상이 지금 커밋이 됩니다
같은 코드가 같은 방식으로 매번 조용히 틀린 답을 냈습니다. 값 자체는 유효한 커밋 해시였고, 그 값으로 하는 체크아웃도 성공합니다. 그래서 어느 단계도 실패로 표시되지 않았습니다.

같은 배포 골격을 복사해 쓰던 다른 저장소 하나도 똑같은 상태였습니다. 골격을 복사하면 결함도 같이 복사됩니다.
순서를 바꿀 것인가, 읽는 자리만 옮길 것인가
원인이 순서라면 순서를 바꾸는 것이 먼저 떠오릅니다. 그런데 여기서 내려받기를 먼저 하는 데는 이유가 있었습니다. 고칠 방법은 둘이었습니다.
옵션 | 방법 | 얻는 것 | 잃는 것 |
|---|---|---|---|
A. 내려받기를 스크립트 뒤로 미룬다 | 스크립트가 옛 코드 상태에서 HEAD 를 읽은 뒤에 내려받는다 | 순서가 자연스럽다 | 이번 배포에 방금 고친 배포 스크립트가 적용되지 않는다 |
B. 순서는 두고 값을 읽는 자리만 앞으로 옮긴다 | 워크플로가 내려받기 전에 HEAD 를 읽어 스크립트에 인자로 넘긴다 | 스크립트가 자기 자신을 갱신하며 도는 구조가 유지된다 | 스크립트가 인자에 의존한다. 손으로 돌릴 때 대체 규칙이 필요하다 |
A 의 잃는 것이 컸습니다. 배포 스크립트도 저장소 안에 있습니다. 내려받기가 먼저여야 이번에 고친 스크립트가 이번 배포에 걸립니다. 순서를 뒤집으면 스크립트를 고칠 때마다 한 번은 옛 스크립트로 배포되는 구간이 생깁니다.
그래서 B 를 골랐습니다. 순서는 그대로 두고, 「이전 커밋」을 캡처하는 지점만 내려받기 앞으로 옮겼습니다. 원인이 순서라고 해서 순서를 건드릴 필요는 없었습니다.
값을 읽는 시점을 한 칸 앞으로 옮겼습니다
고친 내용은 셋입니다.
워크플로가 코드를 내려받기 전에 현재 HEAD 를 읽어 두고, 배포 스크립트를 부를 때 인자로 넘깁니다
배포 스크립트는 받은 인자를 「이전 커밋」으로 씁니다. 자기가 처한 시점을 스스로 추측하지 않습니다
손으로 스크립트를 돌릴 때는 인자가 없습니다. 이때는 Git 참조 로그(reflog)의 직전 HEAD 를 대체값으로 씁니다. 인자가 없다고 조용히 같은 값으로 떨어지지 않게 한 것입니다
검증은 다음 배포 로그로 했습니다.
배포됨 (A → B)두 해시가 갈렸습니다. 같은 골격을 쓰던 다른 저장소도 같은 방식으로 고쳤습니다. 검증 기준이 「배포가 성공했는가」에서 「로그의 두 해시가 갈리는가」로 바뀌었습니다. 눈으로 볼 수 있는 값 하나가 생겼습니다.

작업 기록에는 원래 하려던 조건부 재시작보다 이쪽이 크다고 적어 뒀습니다. 헬스체크가 실패해도 복귀가 안 되는 상태였기 때문입니다. 안 적으면 그날 기록에는 작은 쪽만 남습니다.
안전망은 평소에 아무 신호도 주지 않습니다
롤백, 재시도, 서킷브레이커, 백업 복원, 알림 폴백은 전부 사고가 났을 때만 도는 코드입니다. 안 도는 동안은 고장과 정상이 겉모습이 같습니다. 이 건에서 배포는 몇 달 동안 성공이었고, 그 성공은 롤백이 살아 있는지 죽어 있는지를 조금도 말해주지 않았습니다.
같은 구조를 LLM 폴백에서도 겪었습니다. 주력 모델이 잘 도는 동안 폴백은 한 번도 불리지 않았고, 그래서 이미 죽어 있다는 것을 아무도 몰랐습니다. 안전망은 종류가 달라도 같은 방식으로 조용히 죽습니다.
이 건에서 하나 더 남은 것은 「이전 상태를 읽는 코드는 읽는 시점에 의존한다」는 점입니다. 그래서 이런 값은 코드가 스스로 읽지 않고, 시점을 아는 쪽이 읽어서 넘겨주는 편이 안전합니다.
그리고 관측할 값 하나가 필요합니다. 「배포가 성공했는가」는 롤백의 생사를 구분하지 못합니다. 「로그의 두 해시가 갈리는가」는 구분합니다. 상태가 아니라 변화를 보여주는 값이 있어야 조용한 고장이 눈에 띕니다.

반례와 한계
모든 안전망을 매번 발동시켜 볼 수는 없습니다. 운영에서 헬스체크를 일부러 실패시키는 데는 비용이 있습니다. 현실적인 절충은 스테이징에서 한 번 실제로 발동시켜 보는 것, 그리고 안전망이 쓰는 입력값을 로그에 찍어 눈으로 보는 것입니다. 이 건은 둘째만 있었어도 진작 걸렸을 겁니다.
다만 로그에 찍는 것도 만능은 아닙니다. 이 건은 값이 이미 찍히고 있었는데 아무도 안 읽었습니다. 사람이 읽어야 보이는 값은 결국 안 읽힙니다. 자동으로 잡으려면 「두 값이 같으면 실패로 표시」 같은 검사가 파이프라인 안에 있어야 걸립니다.
롤백 자체가 늘 정답인 것도 아닙니다. 마이그레이션이 이미 돈 배포는 코드만 되돌리면 더 나빠질 수 있습니다. 「되돌아가는가」와 「되돌아가도 되는가」는 다른 질문이고, 이 글은 앞엣것만 다뤘습니다.
체크리스트
롤백·재시도·폴백 같은 안전망을 달아 둔 뒤, 실제로 한 번이라도 발동시켜 본 적이 있습니까
안전망이 쓰는 입력값(이전 커밋, 폴백 대상, 백업 위치)이 로그에 찍히고 있습니까
그 값이 「같으면 실패」처럼 자동으로 판정됩니까, 아니면 사람이 읽어야 보입니까
「이전 상태」를 읽는 코드가 불리기 전에, 호출자가 먼저 그 상태를 바꾸지는 않습니까
같은 배포 골격을 복사해 쓰는 저장소가 더 있습니까
마무리
배포 로그의 두 해시가 같았습니다. 그 한 줄을 읽기 전까지 롤백은 몇 달째 같은 커밋으로 되돌아가는 무동작이었고, 배포는 계속 성공이었습니다. 고친 것은 「이전 커밋」을 읽는 시점을 내려받기 앞으로 한 칸 옮긴 것뿐입니다.
안전망은 실제로 쓰일 때까지 작동 여부를 알려주지 않습니다. 달아 뒀다는 사실과 되돌아간다는 사실은 다른 것이고, 뒤엣것은 눈으로 확인한 값이 있을 때만 압니다. 지금 운영 중인 시스템에 달아 둔 뒤 한 번도 안 돈 코드가 있다면, 그 코드가 쓰는 값 하나를 오늘 로그에서 찾아 읽어 보는 것이 시작입니다.
연작 「에러 없이 조용히 틀린다」
이 글은 다섯 편 중 4편입니다. 에러가 한 번도 안 났는데 실제로는 일이 안 되고 있던 사례를 이어서 다룹니다.