Back

“간헐적 에러”를 디버깅하는 방법

고객사에서 문의가 왔다. “API로 목록 조회만 하는데, 간헐적으로 에러가 발생합니다.” 첨부된 건 로그 스크린샷 한 장.

1단계: 증상과 코드가 안 맞는다

스크린샷의 에러는 DataIntegrityViolationException — DB에 쓸 때 나는 에러다. 그런데 고객 코드를 보니 정말로 SELECT만 한다. 읽기만 하는 코드에서 쓰기 에러가 나는 건 말이 안 된다.

여기서 첫 번째 판단이 필요했다. 고객이 “내 코드에서 에러가 난다”고 했지만, 스크린샷을 다시 보니 이건 고객 코드의 로그가 아니라 대상 워크플로우의 내부 실행 로그였다. 고객의 폴링 코드(GET)와 워크플로우의 실행 경로(POST/INSERT)는 완전히 다른 코드 경로다.

이 분리를 안 했으면 고객 코드를 계속 분석하면서 시간을 낭비했을 것이다.

2단계: 가설 3개, 검증 3개

WRITE 경로에서 DataIntegrityViolationException이 나는 원인 후보를 세 가지로 좁혔다.

  1. DB charset (utf8mb3) — 대상 테이블이 3바이트 UTF-8만 지원. 이모지 같은 4바이트 문자가 들어오면 터질 수 있다
  2. 컬럼 길이 초과 — 특정 컬럼들에 길이 제한이 있는데 입력값 검증이 없음
  3. 동시성 — 3개 경로가 같은 row를 트랜잭션 없이 업데이트

테스트 환경에서 세 가지를 전부 돌렸다.

  • 가설 1: 이모지를 직접 INSERT → ERROR 1366: Incorrect string value재현 성공
  • 가설 2: 길이 제한 초과로 POST → ERROR 1406: Data too long → 에러가 나긴 하지만, 발생 시점이 고객 스크린샷과 다르다 → 패턴 불일치로 배제
  • 가설 3: 동시 UPDATE → lost update는 가능하지만 DataIntegrityViolationException이 아니다 → 배제

결정적 재현: 워크플로우에서 print("Hello 😀")를 실행하고, 폴링 중에 에러가 터지는 것까지 확인. 고객 스크린샷과 정확히 같은 패턴.

3단계: “간헐적”의 조건을 특정하기

“간헐적”이라는 건 매번 안 터진다는 뜻이고, 매번 안 터진다는 건 조건이 있다는 뜻이다. 같은 워크플로우 코드인데 왜 어떤 날은 터지고 어떤 날은 안 터지는가? 처리하는 입력 데이터에 따라 로그에 들어가는 내용이 달라지기 때문이었다. 이모지가 포함된 데이터가 들어오는 날만 터진다.

“간헐적”이 “입력 데이터에 4바이트 유니코드가 포함된 경우”로 바뀐 순간, 문제는 더 이상 간헐적이지 않았다.

돌아보며

이 케이스에서 내가 쓴 접근은 세 단계다.

  1. 에러 경로를 분리한다 — 고객이 보고한 증상과 실제 에러 발생 위치가 다를 수 있다. 코드를 읽어서 확인해야 한다
  2. 가설을 세우고 전부 검증한다 — 3개 가설 중 1개만 맞았고, 나머지 2개는 재현 결과로 배제했다. “아마 이거겠지”로 넘어가면 나중에 재발한다
  3. “간헐적”의 조건을 찾는다 — 간헐적이라는 건 조건부라는 뜻이다. 조건을 특정하면 재현이 되고, 재현이 되면 원인이 확정된다