[연재] 검사 228가지를 통과한 코드가 10분마다 화면을 멈추고 있었습니다 — 내가 만든 건 내가 못 봅니다
지난 편에서 감사 인원을 따로 안 두고, 모으는 자리에 있는 사람이 직접 검사한다는 얘기를 했습니다. 그런데 정작 제가 검사하는 자리에 있으면서도 놓친 일이 있었습니다. 이번 편은 그 얘기입니다.
"테스트 228개 통과"라는 보고를 받았습니다
여러 일이 어떻게 돌아가는지 한눈에 보는 현황판이 있습니다. 여러 조수가 지금 뭘 하고 있는지, 어디서 막혔는지를 한 화면에서 보려고 만든 거예요. 거기에 새 칸 하나를 붙였어요. 3초마다 상태를 확인해서 화면에 뿌려주는 칸이었습니다.
만들고 나서 조수한테 검사를 시켰더니 "테스트 228개 전부 통과"라는 보고가 왔습니다. 빨간불이 하나도 없었어요. 저도 흡족해서 넘어갔습니다. 새로 붙인 칸이 며칠 동안 잘 돌아가는 것도 눈으로 봤고요. 숫자가 맞게 뜨고, 새로고침도 잘 되고, 무엇 하나 걸리는 게 없었습니다.
그날 저녁, 버릇처럼 그 코드를 반대편 창에 옮겨 붙여봤습니다. 딱히 문제가 있을 거라 생각해서는 아니었어요. 그냥 습관이었습니다. 뭘 만들면 일단 반대편에도 한 번 보여주는, 앞선 편들에서 계속 해온 그 버릇이요. 11초 뒤에 답이 왔는데 첫 줄이 이랬습니다.
"이 줄이 화면 전체를 최대 90초 멈춥니다."
읽자마자 손이 멈췄습니다. 방금까지 잘 돌아간다고 믿고 있던 화면이었으니까요.
실제로 그랬습니다
찾아보니 맞았습니다. 새로 붙인 칸은 10분에 한 번씩 바깥에 상태를 물어보게 돼 있었는데, 그 물어보는 동안 응답을 기다리며 화면 전체가 멈추는 구조였습니다. 서버가 그 순간엔 다른 요청을 하나도 못 받았어요. 저장해둔 값이 오래됐으면 새로 물어보고, 그 물어보는 동안 다른 누가 화면을 열어도 응답이 없었던 겁니다. 평소엔 저장해둔 값을 그대로 보여주니 빠릅니다. 문제는 그 저장이 비는 딱 그 순간이었고, 하필 그 순간이 10분마다 규칙적으로 돌아왔습니다.
감시하려고 만든 화면이 10분마다 스스로 눈을 감는 꼴이었습니다. 3초마다 상태를 확인하는 화면인데, 정작 10분에 한 번은 90초 동안 아무것도 안 보여줬던 거예요. 하필 감시용으로 만든 칸이 감시가 제일 필요한 순간에 멈춘다는 게 우습기도 하고 아찔하기도 했습니다.
제 검사 228가지는 왜 못 잡았을까요
이게 제일 이상했습니다. 검사를 228가지나 걸어놨는데 왜 하나도 안 걸렸을까요. 숫자만 보면 어지간한 결함은 다 잡힐 것 같은 규모였습니다.
답은 단순했습니다. 그 검사들은 전부 값이 맞는지만 봤습니다. 상태 확인 결과가 맞게 나오는지, 화면에 맞는 숫자가 뜨는지요. 값 자체는 다 맞았습니다. 문제는 값이 아니라 그 값을 가져오는 동안 화면이 멈추는 시간이었는데, 시간을 재는 검사는 하나도 없었어요.
228개라는 숫자만 보면 꽤 촘촘하게 짠 그물처럼 들립니다. 저도 그렇게 믿었고요. 그런데 촘촘함과 폭은 다른 얘기였습니다. 그물코를 아무리 촘촘하게 짜도, 애초에 그 그물을 어디에 펼쳐 놓을지를 잘못 정하면 옆에 있는 큰 구멍은 그대로 뚫려 있습니다. 228개는 전부 "값이 맞는가"라는 한 방향으로만 펼쳐진 그물이었고, "시간이 얼마나 걸리는가"라는 방향에는 애초에 그물이 없었던 거예요.
그리고 무엇을 검사할지 정한 사람도 저였습니다. 제가 "이 정도면 됐다"고 생각한 항목들로만 그물을 짰으니, 제가 생각 못 한 자리는 애초에 그물 밖이었던 거예요. 검사를 228번 돌린 게 문제가 아니라, 228번 다 같은 질문(값이 맞나요)만 반복해서 물었던 게 문제였습니다.
왜 만든 제가 못 봤을까요
코드를 쓸 때 저는 이렇게 생각했습니다. "10분에 한 번이니까 이 정도는 싸다." 그리고 나중에 다시 읽을 때도 같은 생각을 들고 읽었습니다. "10분 캐시니까 싸다"는 전제를 깔고 보니, 캐시가 비는 순간에 정확히 무슨 일이 벌어지는지는 눈에 안 들어왔어요.
반면 처음 보는 쪽은 그 전제가 없었습니다. 왜 10분마다 물어보게 했는지, 그게 싸다고 판단한 근거가 뭔지 모르니까, 그냥 적힌 글자를 문자 그대로 읽었습니다. 코드에 적힌 대로 "응답이 올 때까지 최대 90초 기다린다"를요. 제가 못 본 게 아니라, 볼 필요가 없다고 이미 판단해 버린 채로 보고 있었던 겁니다.
집을 지은 사람이 자기 집 하자를 못 보는 것과 비슷합니다. 여기 이렇게 만든 이유를 아니까, 그 이유가 문제라는 생각 자체가 안 드는 거예요.
이게 제일 무서운 부분이었습니다. 제가 게을러서 못 본 게 아니라는 것 말이에요. 오히려 저는 그 코드를 꽤 여러 번 들여다봤습니다. 그런데 볼 때마다 "10분 캐시니까 싸다"는 생각이 먼저 켜졌고, 그 생각이 켜진 채로는 아무리 여러 번 봐도 같은 곳을 지나쳤습니다. 처음 보는 쪽은 그 생각의 스위치 자체가 없었으니, 코드에 적힌 숫자(90초)를 그냥 숫자 그대로 읽었을 뿐이었어요. 저는 숫자를 안 읽은 게 아니라, 읽으면서 동시에 "괜찮다"는 결론을 같이 읽고 있었던 겁니다.
앞 편의 규칙에 단서를 하나 붙여야 했습니다
지난 편에서, 인원을 빼서 검사조를 따로 두지 말고 모으는 자리에 있는 사람이 직접 검사하면 결함을 2배 넘게 더 찾는다는 얘기를 했습니다. 그런데 그 실험은 전부 남이 만든 것을 검사한 경우였습니다.
이번 일로 단서가 하나 더 생겼습니다. 내가 방금 만든 것에는 그 효과가 안 납니다. 저는 이 현황판을 만든 사람이자 검사한 사람이었고, 둘 다 저였기 때문에 못 봤어요. 잡은 건 코드를 태어나서 처음 본 쪽이었습니다. 같은 화면을 두고 여러 번 다시 봤는데도 저는 못 잡았고, 처음 보는 쪽은 단 한 번 만에 잡았습니다.
앞 편에서 "모으는 자리에 있는 사람이 직접 검사하면 잘 잡는다"고 했던 건 틀린 말이 아닙니다. 다만 그 실험에 쓰인 결함들은 전부 다른 사람이 만든 코드에 심어져 있었어요. 저는 그 실험 결과를 보고 "그럼 내가 직접 검사하면 되겠구나"로 넘겨짚었는데, 정작 제가 만든 것 앞에서는 저 자신이 그 '다른 사람'이 아니었습니다. 같은 사람이 만들고 같은 사람이 검사하면, 아무리 꼼꼼해도 의도가 시야를 가립니다.
언제 외부 눈을 붙여야 하나
이 일을 겪고 나서 든 생각은 "그럼 매번 다른 눈을 붙이면 되겠다"였습니다. 그런데 그것도 답은 아니었습니다. 매번 다른 눈을 붙이는 건 그 자체로 비용입니다. 작은 수정 하나마다 반대편 창을 켜고 기다리면, 그 기다리는 시간이 쌓여서 오히려 손해가 됩니다. 그래서 아래 셋 중 하나에 해당할 때만 붙이기로 정했습니다.
1. 다른 것의 발목을 잡을 수 있는 변경. 반복해서 도는 것에 얹는 호출, 바깥에 묻는 동기 호출, 여럿이 같이 쓰는 서버·잠금·캐시를 만지는 경우입니다. 2. "이 정도면 싸다"는 판단이 들어간 자리. 이번이 정확히 이거였습니다. 싸다고 여긴 근거가 어떤 조건(캐시가 살아있을 때)에 걸려 있다면, 그 조건이 깨지는 순간을 남이 봐야 합니다. 3. 되돌릴 수 없는 일. 배포, 외부 제출, 삭제처럼요.
셋 다 아니면 그냥 넘어갑니다. 매번 외부 눈을 붙이면 그 자체가 새로운 고정비용이 되고, 작은 수정 하나에도 반대편 창을 켜는 건 배보다 배꼽이 큽니다. 그런데 이 셋 중 하나라도 걸리면, 그때는 "나중에 봐도 되겠지"가 아니라 그 자리에서 바로 붙입니다. 이번 일을 겪고 나서 정한 순서예요.
값을 따져보니 비교가 안 됐습니다
같은 날 재본 값이 있습니다. 처음 보는 쪽에 물어보는 데 든 건 11초, 토큰 2만여 개, 5시간짜리 사용 한도의 1%도 안 되는 양이었습니다. 그걸로 화면 전체를 멈추던 결함 하나를 잡았습니다. 비용 대비로 따지면 비교할 대상이 없는 수준이었어요.
숫자를 나란히 놓고 보면 더 분명해집니다. 저는 코드를 짜는 데 훨씬 오랜 시간을 썼고, 그 뒤에 228가지 검사를 돌리는 데도 적잖은 시간을 썼습니다. 그런데 정작 결함을 잡은 건 11초였습니다. 시간을 많이 들였다고 결함이 잡히는 게 아니라는 걸, 숫자로 보고 나서야 받아들였습니다.
그리고 어떻게 물어야 잘 잡히는지도 알게 됐습니다. "조용히 깨질 자리를 먼저 대라"고 물으면 잠복해 있는 결함을 캐냅니다. "오늘 처리할 수 있는 크기로 셋만 골라라"고 붙이면, 큰 개편안을 늘어놓느라 정작 봐야 할 걸 놓치는 일을 막습니다. 그리고 파일은 통째로 옮겨 붙이지 않고 경로만 알려줍니다. 통째로 옮기면 그게 제 쪽 비용으로 잡히고, 상대는 어차피 요약본만 보게 되거든요. 세 문장을 정리해 두고 나니, 이제는 물을 때마다 다시 고민할 게 없어졌습니다.
이번에 사람이 붙잡은 자리
- 버릇처럼 반대편 창에 옮겨본 것. 의심이 있어서가 아니라 그냥 습관이었는데, 그 습관이 결함을 잡았습니다. 습관을 들이기로 정한 것도 결국 저였고요.
- 제 검사 228가지가 왜 못 잡았는지 원인을 끝까지 따진 것. "통과했으니 됐다"에서 멈추지 않고, 무엇을 검사 안 했는지를 되짚은 게 저였습니다. 그물의 개수가 아니라 그물이 향한 방향을 따진 것도 그때였습니다.
- 언제 외부 눈을 붙일지 기준 세 가지를 정한 것. 매번 붙이면 비용이고, 안 붙이면 이번 같은 일이 또 납니다. 그 경계를 긋는 건 결국 사람 몫이었습니다.
세 편에 걸쳐 반박도 시켜보고, 합치는 것도 시켜보고, 검사조도 나눠봤습니다. 이번 편에서 결국 남은 건 제일 단순한 사실이었습니다. 만든 사람은 자기가 만든 걸 못 봅니다. 조수도 그렇고, 저도 그렇습니다. 그러니 매번은 아니어도, 정말 중요한 자리에서는 꼭 처음 보는 눈 하나를 붙여야 한다는 것, 그리고 그 붙일 때를 정하는 기준을 미리 세워둬야 한다는 것. 이 연재를 시작하면서 처음 잡았던 "사람 몫 세 자리" 중 하나가, 결국 여기로 다시 돌아왔습니다.