정적 SimpleDateFormat 때문에 가끔 엉뚱한 날짜가 나오고 예외까지 난 이유
문제 발생
포맷터를 상수로 빼서 재사용했습니다. 흔한 최적화라고 생각했습니다.
private static final SimpleDateFormat FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static String format(Date date) {
return FORMAT.format(date);
}평소에는 멀쩡하다가 트래픽이 몰리면 날짜가 뒤섞이고, 파싱 쪽에서는 이런 예외가 났습니다.
java.lang.NumberFormatException: For input string: ""원인 분석
SimpleDateFormat은 스레드 안전하지 않습니다. javadoc의 Synchronization 절이 짧고 분명합니다 — 날짜 포맷은 동기화되지 않습니다. 스레드마다 별도의 포맷 인스턴스를 만드는 것을 권장합니다. 여러 스레드가 하나의 포맷에 동시에 접근한다면 외부에서 동기화해야 합니다.
이유는 구현에 있습니다. DateFormat은 작업 중 내부 Calendar 필드를 변경하면서 포맷·파싱을 진행합니다. 두 스레드가 같은 인스턴스에 들어오면 한쪽이 세팅한 중간 상태를 다른 쪽이 읽습니다. 그래서 예외가 나기도 하고, 더 나쁘게는 예외 없이 다른 시각이 조용히 나오기도 합니다.
부하가 낮을 때 재현되지 않는 것도 이 때문입니다. 겹치는 순간이 있어야 드러납니다.
javadoc은 대안도 직접 지목합니다 — 불변이고 스레드 안전한 대안으로 DateTimeFormatter를 고려하라는 API 노트입니다.
해결 방안
java.time으로 옮깁니다. 지금 새로 쓰는 코드라면 이게 정답입니다.
private static final DateTimeFormatter FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.of("Asia/Seoul"));
public static String format(Instant instant) {
return FORMATTER.format(instant); // 불변, 공유 안전
}-
레거시를 당장 못 걷어내면 인스턴스를 공유하지 않습니다. 호출마다 새로 만드는 비용은 대부분의 서비스에서 문제가 되지 않습니다. 측정 없이 "느릴 것 같아서" 상수로 빼는 것이 애초에 이 버그의 출발점이었습니다.
-
스레드마다 하나가 필요하면
ThreadLocal을 쓰되 수명을 관리합니다. 스레드 풀에서ThreadLocal은 정리하지 않으면 그대로 남습니다. -
같은 성격의 다른 클래스도 함께 점검합니다.
Calendar,DateFormat계열은 모두 가변입니다. 반대로DateTimeFormatter,Instant,LocalDate는 불변이라 공유해도 됩니다. -
정적 필드에 가변 객체를 두는 습관 자체를 의심합니다. 이 버그는 코드 리뷰에서 "상수니까 괜찮다"로 통과하기 쉽습니다. 상수인 것은 참조이지 그 객체의 내부 상태가 아닙니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.