예외를 던지지 않는 실패들
아침 첫 대화에서만 취향 기억이 0건이었다. 에러 로그는 없었다. 이런 결함을 여섯 개 찾았고, 공통점이 하나 있었다.
버그 리포트는 이렇게 왔다.
아침에 처음 말 걸면 에이전트가 내 취향을 하나도 모른다. 두 번째부터는 멀쩡하다.
에러 로그는 없었다. 5xx도 없었다. 응답은 200이었고, 기억 목록이 빈 배열이었을 뿐이다. 재현하려고 낮에 아무리 호출해도 잘 나왔다.
원인을 찾고 나서, 같은 성질의 결함이 다섯 개 더 있다는 걸 알았다.
밤새 유휴, 그리고 첫 요청
벡터 저장소 커넥션 풀에 대여 전 검사가 없었다. 몇 시간 유휴 상태로 두면 서버 쪽에서 커넥션이 끊기는데, 풀은 그걸 모른 채 죽은 커넥션을 내준다. 첫 요청이 그걸 물고 실패한다.
01아침 첫 턴. 어젯밤 이후로 이 경로에는 아무 요청도 없었다.
설계 자체는 옳았다. 기억을 못 불러왔다고 대화를 끊을 수는 없다. 문제는 삼킨 실패가 어디에도 보고되지 않은 것이다. 빈 배열과 "기억이 없는 사용자"가 구분되지 않았다.
고친 것은 두 줄이다. 풀에 대여 전 검사를 걸고, 삼킨 실패를 빈 결과와 다른 신호로 남겼다.
필드가 없는 행은 조건에 걸리지 않는다
검색에서 특정 대화를 제외하려고 $not 조건을 걸었다. 그런데 모델이 직접 저장한 기억이 검색에 영영 나오지 않았다.
NOT NULL은 TRUE가 아니라 NULL이다. 아는 사람에겐 당연하고, 잊은 사람에겐 며칠이 걸린다. 무서운 건 이 조건이 에러를 내지 않는다는 점이다. 쿼리는 성공하고, 결과가 조용히 줄어든다.
COALESCE(NOT (…), TRUE)로 감쌌다. 그리고 애초에 필드가 비지 않도록, 자동 적재분에도 분류를 반드시 붙이게 했다.
새벽에 나눈 대화는 어제 일이 된다
기억에는 날짜가 붙는다. "다음 주 화요일에 반차"라는 문장을 저장하려면 기준일이 필요하다.
벤더 코드는 기준일을 UTC로 계산하고 있었다.
파드 타임존을 Asia/Seoul로 맞추면 되지 않느냐는 이야기가 나왔다. 그렇게 하지 않았다. 그 설정이 되어 있는지를 배포마다 확인해야 한다는 것 자체가 사고 지점이기 때문이다. 어딘가에서 한 번 누락되면 같은 버그가 조용히 돌아온다.
기준일을 UTC+9로 코드에 고정했다. 한국은 1988년 이후 서머타임이 없어서, 이 값은 언제나 정확하다. 환경에 의존하지 않는 상수가 환경에 의존하는 설정보다 낫다.
나머지 셋
같은 성격의 결함이 세 개 더 있었다. 전부 배포 전에 잡았다.
세 번째가 특히 나쁘다. 페이지를 넘길 때마다 어떤 항목은 두 번 나오고 어떤 항목은 영영 안 나온다. 그런데 한 번만 조회하면 아무 이상이 없어 보인다.
공통점
여섯 개를 늘어놓고 보니 하나로 묶였다.
전부 예외를 던지지 않는다. 죽은 커넥션은 삼켜진 실패가 됐고, NOT NULL은 조용히 행을 줄였고, UTC 기준일은 그럴듯한 날짜를 만들었고, 풀스캔은 그냥 느렸고, 문자열 시각은 정렬만 어긋났고, 정렬 없는 LIMIT은 매번 다른 답을 줬다.
전부 200을 반환한다. 전부 그럴듯한 값을 돌려준다.
에러를 내는 버그는 발견이 쉽다. 스택 트레이스가 위치를 알려주고, 알림이 울리고, 재현이 된다. 틀린 값을 조용히 돌려주는 버그는 사용자가 대신 발견해 준다. 그것도 "아침에만 이상하다" 같은 형태로.
배포 전에 잡은 값
여섯 중 넷은 아직 배포되지 않은 상태에서 잡았다. 코드를 읽다가 발견한 것이지 장애로 발견한 게 아니다.
배포 후였다면 각각 데이터 마이그레이션이 필요했을 것이다. 문자열 시각은 이미 쌓인 원장을 변환해야 하고, UTC 기준일은 이미 저장된 상대 날짜를 되돌릴 방법이 없다. 되돌릴 방법이 없다는 게 핵심이다. 「내일」이 하루 밀린 채 저장된 기억은, 나중에 타임존을 고쳐도 복구되지 않는다.
남은 것
조용한 실패를 만들지 않으려면 "없음"과 "못 가져옴"을 구분해야 한다. 빈 배열 하나로 두 상태를 표현하면, 장애가 정상 응답으로 위장한다. 이건 타입의 문제이지 로깅의 문제가 아니다.
환경 설정에 의존하는 정확성은 정확성이 아니다. 파드 타임존이 맞게 설정돼 있어야만 옳은 코드는, 그 설정을 확인하는 절차를 영원히 안고 간다. 상수로 못 박을 수 있으면 그렇게 하는 편이 낫다.
언어의 3값 논리는 잊을 때만 물린다. NOT NULL이 NULL인 것은 SQL 을 아는 사람 모두가 아는 사실이고, 그래서 아무도 검토에서 지적하지 않는다. 아는 것과 그 순간에 떠올리는 것은 다르다.