모든 글
Silent Failures · 2분 읽기

조용히 실패하는 것들 8 — 커밋도 옳고 출시도 옳았는데, 갈라진 자리만 틀렸어요

v1.0.8은 잘 나갔습니다. 번호도, CHANGELOG도, 출시 노트도 다 있었어요. 그래서 아무도 몰랐습니다 — develop이 그 판을 통째로 모르고 있다는 걸요. 브랜치가 main에서 갈라져 main으로 돌아갔기 때문입니다.

#git#브랜치#출시#develop#main

저희 저장소의 브랜치 규칙은 단순합니다.

  • 코드는 기능 브랜치에서 낳아 develop으로 합친다
  • main은 출시된 것만 담는다 — developmain은 출시할 때 한 번

main에 밀면 CI가 곧장 Play 내부 테스트 트랙에 번들을 올리기 때문에, main을 작업 공간처럼 쓰면 고치는 중인 코드가 스토어로 나갑니다. 이 규칙은 잘 지켜지고 있었어요. 적어도 그렇게 보였습니다.

사고는 아무 문제 없이 일어났습니다

8월 26일, 이력 상세 화면을 다듬는 브랜치가 하나 만들어졌습니다. 작업은 깔끔했고, 리뷰도 통과했고, 합쳐졌고, v1.0.8로 출시됐습니다. 버전 번호도 옳았고, CHANGELOG도 적혔고, 출시 노트도 있었어요.

그래서 아무도 몰랐습니다.

그 브랜치는 develop이 아니라 main의 커밋에서 갈라졌고, main으로 곧장 합쳐졌거든요. 커밋 하나하나는 다 옳았습니다. 틀린 것은 갈라진 자리 하나뿐이었어요. 그리고 그 자리는 이력 그래프를 그려 보기 전에는 눈에 보이지 않습니다.

이게 무슨 문제냐면 — main에서 갈라져 main으로 돌아간 일은, develop이 영영 모릅니다. v1.0.8이 바꾼 동작, 되돌린 결정, 고친 버그 — 전부 develop에는 없는 채로, 두 브랜치가 각자 멀쩡히 자랍니다.

하루 뒤에, 우연히

다음 날 develop에서 다른 작업을 하다가 이상한 걸 봤습니다. 이력 항목을 누르면 상세가 열리는 옛 동작이 그대로 있는 거예요. 어제 v1.0.8이 바꾼 자리인데요.

따라가 보니 develop은 v1.0.8을 통째로 모르고 있었습니다.

발견하지 못했다면 어떻게 됐을까요. 다음 출시 때 developmain이 나가는 순간, v1.0.8이 되돌린 것과 고친 것이 조용히 되살아납니다. 사용자 입장에서는 ‘고쳐졌던 버그가 다음 판에서 돌아온’ 거예요. 그리고 그 되살아남은 diff에서조차 자연스러워 보입니다 — develop 쪽에는 그 코드가 원래부터 그렇게 있었으니까요.

처치는 머지 하나였습니다.

Merge origin/main into develop — v1.0.8 이 develop 을 거치지 않았다

maindevelop으로 되돌려 합쳐서 두 브랜치가 다시 같은 역사를 공유하게 했어요. 빠져 있던 v1.0.8 태그도 다음 출시 때 보충했습니다.

‘main에서 일하지 않는다’는 두 문장이었습니다

이 사고에서 배운 건, 규칙이 반쪽만 적혀 있었다는 겁니다.

‘main에서 일하지 않는다’를 저희는 ‘main에서 커밋하지 않는다’로 읽고 있었어요. 그 반은 잘 지켜졌습니다. 그런데 이 규칙에는 반이 더 있었어요 — ‘main에서 브랜치를 따지 않는다’.

브랜치는 갈라진 곳으로 돌아가려는 관성이 있습니다. main에서 딴 브랜치는 자연스럽게 main으로 머지되고, 그 순간 develop은 소외됩니다. 커밋 금지만으로는 이 경로가 막히지 않아요.

그래서 규칙을 두 가지로 보강했습니다.

하나, 브랜치를 따기 전에 한 번 봅니다.

git rev-parse --abbrev-ref HEAD

develop이 아니면 따지 않습니다. 5초짜리 확인이에요 — 폴더를 옮기는 일은 5초고, 옮긴 걸 아무도 못 알아채는 일은 몇 달이라는 걸 이미 겪어봤거든요. 갈라진 자리도 똑같습니다.

둘, 사람이 세지 않습니다. 출시가 끝난 뒤 main에는 develop에 없는 커밋이 하나도 없어야 한다 — 이걸 커밋 훅이 부르는 검사 스크립트가 저장소마다 셉니다. 눈으로 안 잡히는 사고는 기계가 세야 해요.

이번 작업에서 정한 기준

  • 모든 단계가 옳아도 전체가 틀릴 수 있습니다. 커밋도, 리뷰도, 출시도 옳았어요. 틀린 건 시작점 하나였고, 그건 어느 단계의 체크리스트에도 없었습니다.
  • 성공한 출시가 사고를 가립니다. v1.0.8이 잘 나갔기 때문에 아무도 의심하지 않았어요. 실패는 시끄럽지만 성공은 조용합니다.
  • 되살아나는 코드는 diff에서 자연스러워 보입니다. 잃어버린 판을 모르는 브랜치에게 옛 코드는 ‘원래 있던 것’이에요. 리뷰로는 못 잡습니다.
  • 규칙은 금지하려는 경로를 전부 적어야 합니다. ‘거기서 일하지 않는다’는 커밋 금지 더하기 분기 금지였어요. 반만 적힌 규칙은 반만 지켜집니다.