창고에는 고쳤는데 현장은 옛날 버전이었다
텔레그램 화면에 보이면 안 되는 내부 표식이 보였다. 메시지를 라우팅하고 중복을 막기 위해 붙여둔 봉투 태그였다. 사람에게 보여주려고 만든 글자가 아니었다. 확인해보니 그걸 숨기는 수정은 이미 코드에 들어와 있었다. main에도 있었다. 그런데 화면에는 계속 보였다. 이상했다. 창고에는 고쳐둔 물건이 있는데, 현장에서는 옛날 물건을 쓰고 있었다.
이런 순간이 제일 헷갈린다. 코드를 보면 고쳐져 있다. PR도 머지됐다. 테스트도 통과했다. 그러면 사람은 자연스럽게 “고쳤다”고 말한다.
하지만 사용자는 여전히 같은 문제를 보고 있다. 그러면 둘 중 하나다. 내가 잘못 봤거나, 고친 코드가 실제로 돌고 있지 않거나.
이번에는 후자였다.
저장소의 진실과 런타임의 진실
개발자는 저장소를 많이 본다. 브랜치, 커밋, PR, main. 그래서 어느 순간 저장소의 상태를 현실로 착각한다.
main에 코드가 있으면 고친 것 같다. 테스트가 초록이면 끝난 것 같다. PR이 머지되면 배포된 것 같다.
하지만 운영 중인 프로그램은 저장소가 아니다. 지금 메모리에 올라가서 돌아가는 프로세스다. 그 프로세스가 언제 시작됐는지, 어떤 파일을 읽었는지, 어떤 패키지 버전을 물고 있는지가 진짜 현장 상태다.
이번 브릿지는 패키지로 실행 중이었다. 소스 저장소에는 수정이 들어가 있었지만, 살아 있는 브릿지 프로세스는 새 코드를 로드하지 않았다. 그러니 옛날 버릇을 그대로 했다. 내부 봉투 태그를 다시 붙였고, 그게 화면에 새어나왔다.
창고에 새 부품을 넣어두는 것과, 현장 기계를 멈추고 그 부품으로 갈아 끼우는 것은 다른 일이다. 둘 사이에는 배포와 재시작이라는 구간이 있다. 그 구간을 지나지 않으면 현장은 변하지 않는다.
”고쳤다”는 말이 너무 빨랐다
나도 자주 “고쳤다”고 말한다. 코드 수정이 끝났을 때. 테스트가 통과했을 때. PR이 머지됐을 때.
틀린 말은 아니다. 개발 작업 관점에서는 고친 게 맞다. 하지만 운영 문제를 보고 있는 사용자에게는 부족한 말이다. 사용자가 원하는 건 저장소에 고쳐진 코드가 있는지가 아니다. 지금 자기 화면에서 더 이상 문제가 보이지 않는지다.
그래서 운영에서 “고쳤다”는 말은 두 단계로 나눠야 한다.
첫째, 코드가 고쳐졌다.
둘째, 고친 코드가 실제 런타임에 올라갔다.
둘 다 지나야 “현장 문제가 고쳐졌다”고 말할 수 있다. 첫 번째만 끝났으면 “수정은 들어갔고, 배포가 남았다”고 말해야 한다. 이 차이를 흐리면 사고 기록이 틀어진다. 사람은 해결됐다고 믿는데, 시스템은 아직 옛날 상태다.
이번 일이 딱 그랬다. 수정은 있었다. 배포는 없었다. 그래서 사용자 눈에는 계속 같은 문제가 보였다.
재시작은 귀찮은 의식이 아니다
서버나 브릿지를 재시작하는 일은 늘 조심스럽다. 대화 중인 세션이 있으면 끊길 수 있고, 큐가 밀려 있으면 메시지가 유실될 수 있고, 5대 노드가 얽혀 있으면 순서도 봐야 한다. 그래서 “코드는 고쳤는데 재시작은 나중에”라는 상태가 생긴다.
그 자체가 나쁜 건 아니다. 운영 중인 시스템을 무작정 재시작하지 않는 건 맞다. 문제는 그 상태를 명확히 표시하지 않을 때다.
재시작 전이면 아직 적용 전이다. 배포 전이면 아직 현장 반영 전이다. 그 상태를 “완료”라고 부르면 안 된다. “코드 완료, 런타임 미반영”이라고 써야 한다.
재시작은 귀찮은 의식이 아니다. 코드가 현실이 되는 순간이다. 물론 모든 변경이 즉시 재시작을 요구하지는 않는다. 설정 플래그처럼 런타임에 바로 먹는 것도 있다. 하지만 프로세스가 시작할 때 읽는 코드라면, 새 프로세스가 떠야 한다.
그걸 확인하는 질문은 단순하다.
지금 돌아가는 놈이 어느 버전인가?
이 질문 없이 main만 보면, 창고만 보고 현장을 판단하는 꼴이 된다.
태그 하나가 보여준 배포의 간극
이번 내부 봉투 표식은 작은 글자였다. 하지만 그 글자가 좋은 신호였다. 화면에 보이면 안 되는 게 보였기 때문이다. 덕분에 “수정은 있는데 런타임이 옛날”이라는 간극이 드러났다.
만약 더 조용한 버그였다면 어땠을까. 화면에 태그가 보이는 대신, 라우팅만 미묘하게 틀어졌다면. 중복 방지만 가끔 실패했다면. 그랬다면 저장소를 보며 “이미 고친 건데 왜 이러지” 하고 더 오래 헤맸을 것이다.
눈에 보이는 노출은 불쾌하지만, 디버깅에는 도움이 된다. 보이지 않는 런타임 drift가 가장 무섭다. 저장소는 최신인데 프로세스는 옛날. 문서에는 완료라고 적혀 있는데 실제 서비스는 옛날. 이런 상태는 겉으로 조용하다. 누군가 현장에서 증상을 볼 때까지 모른다.
그래서 배포 확인은 별도 단계가 되어야 한다. 머지 확인과 다르게. pull 확인과 다르게. 프로세스 재시작 확인과 다르게. 그리고 가능하면 실제 화면에서 증상이 사라졌는지까지 봐야 한다.
저장소의 초록불은 현장의 초록불이 아니다.
ep62의 자매편이었다
며칠 전에는 테스트 파일 하나가 있었다. 직접 실행하면 실패하는데, 표준 러너는 그 테스트를 못 찾았다. 그때 배운 건 “테스트를 쓰는 것과 테스트 스위트에 들어오게 하는 것은 다르다”였다.
이번 일은 그 자매편이다.
코드를 고치는 것과 고친 코드가 런타임에 올라가는 것은 다르다.
둘 다 같은 구조다. 어느 레이어에는 존재한다. 테스트 파일은 존재했고, 수정 코드도 존재했다. 하지만 실제 방어선이나 실제 현장에는 연결되지 않았다. 그래서 사람은 있다고 믿고, 시스템은 없는 것처럼 행동했다.
이런 간극은 자동화가 커질수록 자주 나온다. 저장소, 패키지, 프로세스, 화면, 보고서가 모두 다른 층이다. 한 층에서 완료됐다고 다른 층까지 완료된 게 아니다.
좋은 운영은 이 층들을 구분하는 일이다. 코드 완료. 테스트 포함. 배포 완료. 런타임 로드. 화면 검증. 단어는 길어지지만, 헷갈림은 줄어든다.
배운 것
main에 코드가 있다고 현장이 바뀐 게 아니다. 저장소의 진실과 런타임의 진실은 다르다. 프로세스가 새 코드를 읽고 다시 떠야, 그때부터 현장이 바뀐다.
“고쳤다”는 말을 쪼개야 한다. 코드 수정 완료인지, 배포 완료인지, 재시작 완료인지, 실제 증상 소멸까지 확인한 것인지. 이 구분을 안 하면 완료 보고가 너무 빨라진다.
그리고 배포 확인은 사치가 아니다. 현장에서 도는 놈이 어느 버전인지 확인하는 것. 그게 없으면 창고에 새 부품을 넣어놓고, 현장 기계가 고쳐졌다고 말하는 셈이다.
고친 코드가 저장소에 있는 것과, 고친 코드가 살아서 도는 것은 다른 사건이다.
— 강대종 / @ssamssae