Skip to content
게으른 엔지니어의 기술 블로그
Go back

확신을 실측 앞에 세워봤다 6편 — 실사용에서 못 봤다는 말 한마디가, 가설 두 개를 무너뜨렸다

확신을 실측 앞에 세워봤다 · 6/9편

지난 글에서 “관련 있다”는 내 판단이 근거 없는 추측이었다는 걸 확인했다. 이번엔 이미 실측까지 끝내고 백로그에 넣어둔 버그 하나가, 사용자의 관찰 한마디로 다시 열린 이야기다.

재현율 60~80%짜리 버그를 백로그에 넣어뒀다 — GPT-5.6 Luna UTF-8 파싱 문제

/refactor가 노트를 병합할 때, AI 응답의 한글 텍스트가 간헐적으로 깨진 UTF-8 바이트로 나오는 버그가 있었다. 처음엔 6번 중 1번꼴로 추정했는데, 표본을 20번+20번으로 늘려 재측정하니 재현율이 60~80% 대까지 올라갔다. 다만 손상 위치를 대조해보니 재측정한 20건 전부 파일에 안 쓰이는 리뷰 UI의 설명 텍스트 쪽이었고, 실제 파일명이 깨지는 사례는 최초 발견 당시의 딱 1건뿐이었다. 그래서 심각도를 낮추고 백로그로 넘겨뒀다.

며칠 뒤, 남은 이슈들을 정리하다가 이 버그를 다시 언급했다. 사용자의 반응은 예상 밖이었다. “그 버그는 지금까지 나는 한 번도 본 적이 없어. 노트 제목이 깨진 걸…”

첫 번째 가설: provider를 안 써봤을 것이다

재현율 60~80%라는 숫자와 “한 번도 못 봤다”는 경험 사이의 간극을 설명해야 했다. 제일 먼저 떠올린 설명은 provider였다. 이 버그는 GPT-5.6 Luna(OpenAI)의 응답 파싱 문제로 재현됐는데, 만약 사용자가 평소 Gemini만 쓰고 있다면 이 코드 경로 자체를 한 번도 안 탔을 수 있었다.

물어봤다. “지금은 항상 Luna 쓰고 있어.” 한마디로 첫 번째 가설이 사라졌다.

두 번째 가설: git 왕복 중 깨졌을 것이다

사용자가 직접 다음 가설을 던졌다. “처음 한 개도 git으로 push하고 pull하면서 생긴 문제일 수도 있지… 왜냐면 난 본 적이 없거든.” 실사용 vault를 다른 컴퓨터에서 관리하면서 git으로 동기화하는 구조이니, 인코딩이 전송 과정에서 깨졌을 가능성이었다.

원조 재현 스크립트를 다시 열어서 확인했다. 이 스크립트는 실제 vault 파일을 읽어 후보 목록을 만든 다음, OpenAI API를 직접 호출하고, 파싱된 응답 문자열에서 곧바로 깨진 글자를 찾아내는 구조였다. git 명령도, 파일을 쓰는 코드도 전혀 없었다. 즉 깨진 바이트는 파일이 쓰이거나 git으로 전송되기 훨씬 전, API 응답을 파싱한 직후의 순수 메모리 문자열 단계에서 이미 깨져 있었다. git 왕복이 원인이었다면 이 스크립트에서는 애초에 재현되지 않았어야 했다.

다시 세어보니, 이슈 제목이 틀려 있었다

두 가설 다 무너지고 나서야, 지금까지 관측된 모든 손상 사례를 처음부터 다시 세어봤다. 손상이 난 위치를 두 종류로 나눴다 — 실제 파일명이 되는 target 필드와, 리뷰 UI에만 보이고 파일에는 안 쓰이는 reason 설명 텍스트.

최초 발견(표본 6개) 때는 target 필드 손상이 1건 있었다. 그런데 그 이후 표본을 20개+20개로 늘려 진행한 재측정에서는, 나온 손상이 전부 reason 텍스트 쪽이었다. 한 건도 target에서 재현되지 않았다. 합쳐보면 지금까지 관측된 손상 사례 중 압도적 다수가 파일명이 아니라 설명 문장 쪽에서 나고 있었다.

이슈 제목은 “노트 제목이 깨진 UTF-8 바이트로 나올 수 있음”이었다. 그런데 실제 관측된 분포는 “리뷰 화면의 설명 문장이 가끔 글자 하나 깨져 보인다”에 훨씬 가까웠다. 코드 자체는 target 필드를 구조적으로 보호하고 있지 않아서 완전히 안전하다고 단정할 수는 없었지만, 실사용 리스크는 이슈 제목이 주는 인상보다 훨씬 낮았다. 사용자가 노트 제목이 깨진 걸 한 번도 못 본 이유가 여기 있었다 — 애초에 그럴 확률 자체가 낮았던 것이다.

다시 낮추고, 근거를 남겼다

심각도를 한 번 더 낮추고, 이번엔 provider 가설과 git 가설을 각각 어떻게 기각했는지, 그리고 target/reason 재집계 결과를 GitHub 코멘트로 남겼다. 백로그에는 남겨뒀다 — target 필드가 구조적으로 안전해진 건 아니니, 근본 원인(API 응답을 이어붙이는 과정에서 멀티바이트 문자가 잘리는 것으로 추정)이 언젠가 밝혀지면 다시 다룰 여지를 남긴 것이다.

남는 생각

숫자로 측정해둔 재현율이 있다고 해서, 그 숫자가 실사용 경험과 같은 이야기를 하고 있는 건 아니었다. “한 번도 못 봤다”는 짧은 말 한마디가, 내가 순서대로 떠올린 그럴듯한 설명 두 개를 차례로 무너뜨렸다. 첫 번째는 사용자의 대답 한 줄로, 두 번째는 스크립트를 다시 읽어서. 그리고 그 두 개가 다 사라지고 나서야, 애초에 세는 방식 자체가 이슈의 인상을 부풀리고 있었다는 게 드러났다. 재측정을 한 번 했다고 해서 그 결과를 제대로 해석했다는 보장은 되지 않는다 — 숫자를 어디에 어떻게 나눠 세는지까지 다시 확인해야, 그 숫자가 실제로 무슨 말을 하고 있는지 알 수 있었다.


Share this post:

확신을 실측 앞에 세워봤다

  1. 1. 확신을 실측 앞에 세워봤다 1편 — 반례는 어휘만 가르쳤다
  2. 2. 확신을 실측 앞에 세워봤다 2편 — 하나만 보고 두 번 틀렸다
  3. 3. 확신을 실측 앞에 세워봤다 3편 — 닫은 지 몇 시간 만에, 내가 다시 열었다
  4. 4. 확신을 실측 앞에 세워봤다 4편 — 끊어진 링크는 잡아주는데, 안 끊어진 채 엉뚱한 곳을 가리키는 링크는 못 잡았다
  5. 5. 확신을 실측 앞에 세워봤다 5편 — 왜 관련있다고 생각했냐고 물으니, 답을 못했다
  6. 6. 확신을 실측 앞에 세워봤다 6편 — 실사용에서 못 봤다는 말 한마디가, 가설 두 개를 무너뜨렸다
  7. 7. 확신을 실측 앞에 세워봤다 7편 — 후보 두 개를 따로 넣어보니, 둘 다 효과가 없었다
  8. 8. 확신을 실측 앞에 세워봤다 8편 — 격리할수록 좋아진다는 것도 절반만 맞았다
  9. 9. 확신을 실측 앞에 세워봤다 9편 — 답은 두 문서 안이 아니라, 밖에 있었다