pile·
백엔드·마켓컬리마켓컬리 Hello World·

하이버네이트의 시간은 거꾸로 간다

Spring Boot 3 / Hibernate 6 업그레이드 후 자정 무렵 배송 매니저 앱 로그인이 막힌 장애를, "저장은 되는데 조회 시 음수 나노초로 DateTimeException"이라는 현상부터 Hibernate의 LocalTime↔Time 변환 로직 변경까지 추적한 RCA다.

핵심 포인트
  • 에러는 DateTimeException: Invalid value for NanoOfSecond ... -843000000으로, DB TIME 컬럼을 LocalTime으로 변환하는 조회 과정에서 음수 나노초가 나와 발생했다.
  • Hibernate 5.6→6.5 업그레이드로 LocalTime을 Time으로 변환할 때 이전엔 버리던 밀리초(나노초)를 반영하게 바뀌어, 배포 후 데이터에 나노초가 생겼다.
  • KST(UTC+9) 타임존에서 0~8시 시각을 epoch(1970-01-01 UTC) 기준으로 계산하면 fastTime이 음수가 되고, ms를 1000으로 나눈 나머지가 음수가 돼 예외가 터진다.
  • 버그 수정 버전이 아직 릴리스 전이라, LocalTime의 나노초를 truncatedTo로 제거하는 AttributeConverter를 문제 컬럼에 적용해 임시 조치했다.
상세 정리
  • 발단: AWS MSK 대응으로 Spring Boot를 3.x로 올렸고 QA·배포 후 정상 같았으나, 다음 날 자정 직전부터 로그인 불가 알림이 발생해 신속 롤백 후 원인 분석에 들어갔다.
  • 데이터 관찰: DB엔 음수 나노초가 없고 모두 0/양수인데, 배포 전엔 나노초가 없다가 배포 후 데이터엔 나노초가 생긴 차이를 발견했다.
  • 의심1(Java 11→17): LocalTime 로직 변경이 없었고 저장 시 나노초를 버리는 동작도 배포 전부터 동일해 제외했다.
  • 의심2(Hibernate): LocalTimeJavaType.unwrap가 5.6.5에선 ms를 0으로 고정했으나 6.5.5에선 나노초에서 ms를 추출해 Time에 반영하도록 바뀐 것을 확인했다.
  • 저장 경로: TimeJdbcType.doBind에서 unwrap을 거쳐 new Time(... + roundToPrecision(nano,3)/1000000)로 ms가 정상 양수로 저장된다.
  • 조회 경로: doExtract에서 LocalTimeJavaType.wrap으로 Time을 LocalTime으로 되돌릴 때 ms 계산이 음수가 되는 문제가 있었다.
  • 근본 원인: LocalTime엔 날짜가 없어 epoch를 임의 기준으로 쓰는데, KST는 UTC를 맞추려 9시간을 빼면서 0~8시 구간의 fastTime이 음수가 되고 그 나머지 연산이 음수 ms를 만든다.
  • 재현 조건: KST(UTC+ 타임존) + 0~8시 + 나노초 존재(소수점 3자리)일 때 발생하고, UTC나 9시 이후·나노초 없음이면 발생하지 않았다.
  • 조치: 릴리스된 수정본이 없어 나노초 제거 Converter를 적용해 재발을 차단하고, 추후 수정 버전에서 업그레이드할 예정이다.
  • 회고: 인수·통합 테스트가 있었다면 큰 버전업에서 조기 발견했을 것이고, 가설-검증식 디버깅이 원인 규명에 도움이 됐다는 교훈을 남겼다.
왜 읽나Spring Boot 3 / Hibernate 6 업그레이드 후 시간·타임존 관련 장애를 겪는 백엔드 개발자에게 LocalTime 변환 내부 동작과 임시 우회를 알려주는 RCA.
마켓컬리
마켓컬리 Hello World 블로그
원문은 여기서 이어서 읽을 수 있어요
원문 읽기
읽음 (0)

이 글과 비슷한

  1. 백엔드·github-engGitHub Engineering·

    조기 종료를 없애야 벡터화된다 — 메모리 속도 소스 코드 케이스 폴딩

    GitHub의 코드 검색 엔진 Blackbird는 480TB 이상의 소스 코드를 인덱싱하기 전 모든 바이트에 case folding을 적용한다. 이 글은 Rust로 구현한 case folding을 메모리 대역폭 한계(45+ GiB/s)까지 끌어올린 두 가지 반직관적 최적화를 상세히 다룬다. 핵심은 루프 조기 종료(break) 제거로 LLVM 벡터화를 유도하고, UTF-8을 디코딩하지 않고 바이트 공간 산술만으로 fold를 수행하는 것이다.

    #rust#unicode#simd+2
  2. 백엔드·여기어때 (GC컴퍼니)여기어때 (GC컴퍼니)·

    트랜잭션 스크립트에서 숙소 메타 + 가격 계산 모듈로 — 전시 아키텍처 개선기 (2/3)

    여기어때 전시개발팀이 숙소 상세(PDP) API를 해부한 결과, 코드상으로는 DB 호출 3번처럼 보이던 요청이 실제로는 MongoDB $lookup 체인으로 컬렉션을 19회 접근하는 구조였다. 이 트랜잭션 스크립트 방식의 핵심 문제는 "aggregation이 I/O를 가린다"는 점으로, 독립적인 쿼리 10개가 단일 파이프라인에 직렬화되어 병렬화 기회를 잃고, 가격 때문에 거의 안 바뀌는 이미지까지 매 요청마다 읽어야 하는 읽기 증폭이 발생했다. V3에서는 "조회 시점 조립"을 "쓰기 시점 사전 조립"으로 전환하고, 화면별로 복제되던 가격 계산 로직을 goodsprice 단일 모듈로 수렴했다. 4개 API(PLP/PDP/RDP/ILP)의 반복 마이그레이션은 Claude Code skill로 절차를 고정하고 쉐도잉 + 동일성 검증으로 안전망을 마련하는 방식으로 진행됐다.

    #architecture#migration#caching+2