조용한 고장
이 프로젝트에서 잡은 버그가 거의 전부 성공을 리턴하면서 아무것도 안 했습니다.
이 프로젝트에서 잡은 버그를 모아 놓고 보니, 거의 전부가 같은 모양이었습니다 — «성공했다고 답하면서 아무것도 안 함».
두 종류의 고장
프로그램이 고장 나는 방식은 크게 둘입니다.
| 종류 | 무슨 일이 생기나 | 고치기 |
|---|---|---|
| 시끄러운 고장 | 빨간 오류가 뜨고 멈춤 | 쉬움 — 어디서 멈췄는지 알려 줌 |
| 조용한 고장 | 「됐습니다」라고 답하는데 실제로는 아무것도 안 함 | 어려움 |
이 프로젝트의 버그는 거의 전부 아래쪽이었습니다.
왜 이게 어려운가
시끄러운 고장은 찾을 필요가 없습니다. 프로그램이 먼저 알려 줍니다.
조용한 고장은 아무도 안 알려 줍니다.
- 오류 기록: 깨끗함
- 화면: 「완료」
- 실제: 아무 일도 안 일어남
그래서 며칠 뒤에 「어? 그거 안 됐네」로 발견됩니다. 그때는 언제부터 안 됐는지도 모릅니다.
가져다 쓰던 분이 보내 준 세 건도 전부 같은 모양이었습니다
이 물건을 가져다 쓰던 분이 문제 셋을 원인 진단과 고치는 방법까지 붙여 보내 주셨습니다.
셋 다 조용한 고장이었습니다.
혼자 만든 사람이 자기 버그의 경향을 아는 건 어렵습니다. 남이 같은 모양을 셋 들고 오니 그게 우연이 아니라는 게 확실해졌습니다.
배운 것 하나 — 「글자가 아니라 구조를 읽어라」
명령 하나가 안 먹던 일이 있었습니다.
원인은 답을 「글자」로 읽고 있었던 것이었습니다. 「이 단어가 들어 있으면 성공」 같은 식입니다.
그런데 말투가 조금만 달라지면 그 단어가 안 나옵니다. 그러면 성공했는데 실패로 읽거나, 반대가 됩니다.
고쳐서 구조를 읽게 했습니다 — 글로 된 답이 아니라 정해진 형태의 데이터를 보게요.
그리고 고치고 나니 덤으로 기능이 하나 늘었습니다. 구조를 읽게 되니 거기 들어 있던 다른 정보도 쓸 수 있게 됐습니다.
→ 글자로 읽던 걸 구조로 바꾸면, 고치는 김에 쓸 수 있는 게 늘어납니다.
그리고 제일 아찔했던 것 — 「살아 있나 확인하는 코드가 죽일 뻔했습니다」
감시 기능을 만들 때 있었던 일입니다.
작업하는 프로그램이 아직 살아 있는지 확인하는 코드가 필요했습니다. 널리 쓰이는 방법이 있어서 그대로 썼습니다.
그 방법의 원리는 이렇습니다 — 「아무것도 안 보내는 신호」를 보내고, 반응이 있으면 살아 있는 것. 원래 쓰던 환경에서는 이게 정말 아무것도 안 합니다. 존재 여부만 묻습니다.
그런데 제가 쓰는 환경(윈도우)에서는 그 동작이 다릅니다.
살아 있는지 물어보는 코드가, 물어보면서 죽일 수 있는 상태였습니다.
이게 무서운 종류의 버그입니다. 확인하는 행위 자체가 대상을 바꿉니다. 그러면 「확인해 보니 죽어 있더라」가 나오는데, 확인 안 했으면 살아 있었을 수도 있습니다.
→ 배운 것: 널리 쓰이는 관용구라도, 다른 환경에서는 다른 일을 할 수 있습니다. 「다들 이렇게 쓴다」는 근거가 못 됩니다 — 어느 환경에서 다들 그러는지를 봐야 합니다.
그래서 이 프로젝트가 남긴 한 줄
오류가 0건이라는 건 깨끗하다는 뜻이 아니라, 아직 아무도 안 봤다는 뜻일 수 있습니다.
여기부터는 자세한 기록입니다
기능을 하나씩 붙이는 동안 잡은 버그를 모아 놓고 보니 거의 전부가 같은 모양이었다.
성공을 리턴하면서 아무것도 안 하기
포크해서 쓰던 사람이 이슈 셋을 진단과 패치까지 붙여 보내줬는데, 셋 다 이 모양이었다.
소수점 자릿수 — 슬랙 히스토리 API는 시각 인자의 소수점이 6자리를 넘으면 에러가 아니라 ok: true + 빈 목록을 준다. 파이썬 time.time()은 보통 7자리다. 그래서 채널 맥락 읽기가 몇 주 동안 빈 히스토리로 돌았다. 전수 확인해 보니 실수를 그대로 넘기던 곳은 여기 하나뿐이었다 — 그 하나가 기능 하나를 통째로 무력화했다.
막는 규칙이 허용을 이긴다 — 손님 울타리가 다른 드라이브를 통째로 막는데, 작업 폴더가 그 드라이브에 있으면 허용 목록이 전부 죽는다. 나오는 메시지는 “권한 없음” 한 줄뿐이라 울타리가 아니라 목록이 고장 난 걸로 읽힌다.
별칭이 저장되고 영영 안 뜬다 — 이름 충돌 검사가 출하 명령 이름을 하드코딩하고 있어서 플러그인 명령이 안 보였다. 저장은 성공하고, 부르면 아무 일도 안 일어난다.
문자열이 아니라 구조를 읽는다
명령 하나가 안 먹던 이야기인데, 고치고 나니 기능이 하나 늘었다.
사람이 슬랙 입력창에 !usage라고 치면 잘 되는데, 다른 기계의 세션이 커넥터로 대신 보내면 명령이 전부 빗나갔다. 슬랙이 본문 끝에 출처 표기를 이어붙이기 때문이다. 그것도 줄바꿈 없이 같은 줄에.
증상이 두 갈래로 갈렸다. 앵커가 걸린 명령은 아예 매치가 안 돼 두뇌로 새어 들어갔고(그래서 “그런 명령 없습니다”라고 답했다), 느슨한 명령은 매치는 되고 인자가 오염됐다.
처음엔 그 문구를 잘라내려 했다. 틀린 접근이다 — 출처 표기는 워크스페이스 언어로 번역된다. 한국어, 영어, 일본어가 전부 다르게 온다. 단어로 맞추는 순간 다른 나라 워크스페이스에서 깨진다.
답은 문자열이 아니라 슬랙이 준 구조에 있었다. 출처 표기는 본문에 섞인 게 아니라 맨 뒤에 붙은 별도 블록이고, 사람이 쓴 글은 따로 남아 있다. 그리고 사람은 슬랙 입력창으로 그 블록을 만들 수 없다 — 오탐이 원리적으로 없다는 뜻이다.
고치고 나서 생긴 기능이 이것이다. 다른 기계의 세션이 내 계정으로 슬랙에 글을 올리면 이 PC의 Loki가 그걸 실행하고 스레드로 답한다. 봇이 아니라 사람 명의라 앱을 하나 더 만들 필요가 없다.
생존 확인이 대상을 죽일 수 있다
감시 기능을 만들면서, 워커가 살아 있는지 확인하는 코드를 os.kill(pid, 0)으로 썼다. 유닉스에서는 관용구다 — 시그널 0은 아무것도 보내지 않고 존재 여부만 묻는다.
윈도우 파이썬은 이걸 프로세스 종료로 구현한다. 시그널 0을 포함해서. 생존 확인이 워커를 죽인다.
읽기 전용 조회로 바꿨다. 같은 계열의 함정이 하나 더 있었는데, 재시작이 5분 주기 감시자와 경합해 워커가 셋 붙은 적이 있다. 같은 토큰에 소켓이 셋이면 이벤트가 갈린다. 그런데 헬스체크는 기록된 pid 하나만 보므로 정상이라고 보고한다.
지금은 개수가 아니라 프로세스 계보로 판정한다. 개수로 세면 안 되는 이유가 있다 — 실행 환경상 부모와 자식 두 프로세스가 뜨는 게 정상이라, “둘이면 이상”도 “하나여야 정상”도 틀리다.
자동 감지 하나를 접었다
“대화가 헛돌고 있다”를 자동으로 감지해 넛지에 물리려고 두 번 시도했고 두 번 다 실패했다. 어휘 신규성을 재는 방식이었다.
기각 근거는 튜닝 실패가 아니라 측정 그 자체였다. 실제로 진행되는 대화의 신규성 구간과, 말만 바꿔 같은 자리를 도는 대화의 구간이 겹쳤다. 어떤 임계값도 둘을 못 가른다.
이유는 구조적이다. “같은 주제로 새 질문”과 “같은 교착을 다시 서술”은 쓰는 단어가 같다. 임계값을 조정해 예시 세 개를 통과시키는 건 오버피팅이지 증거가 아니다.
대신 아무것도 이해할 필요 없는 것들로 갔다 — 시간당 요청 상한, 비용 상한, 대화를 명시적으로 끊는 명령. 비용은 이미 응답 안에 들어오고 있었고 버리는 중이었다.
테스트가 라이브 상태를 편집하고 있었다
같은 종류로 두 번 데었다. 각 모듈이 불러올 때 상태 파일 경로를 잡는 구조라, 테스트 준비 코드가 이름으로 하나씩 막지 않으면 라이브 파일이 그대로 편집된다.
실제 사고는 이랬다. 어댑터 테스트가 채널 메시지를 흘려 보냈고, 그게 라이브 상태 파일에 기록되면서 방금 만든 경고 하나가 프로덕션에서 조용히 꺼졌다. 테스트는 전부 통과했다.
지금은 상태 경로 열일곱 개를 통째로 돌려 놓는다. 그리고 새 상태 파일을 만들면 그 목록에 등재하는 게 규칙이다 — 이런 건 다음 사람이 잊는 게 아니라 만든 사람이 잊는다.