CATEGORY

카테고리 (607)
AI (12)
Language & Specs (259)
FrameWork (34)
Library (19)
App (41)
Git (8)
Build & Dependency (2)
AWS (15)
DataBase (45)
OS (33)
Tool (16)
IT (120)
반응형
SEEMINGLY ONLINE

Seemingly
Online

이모저모 방방곡곡 두루두루 개발지식 저장소

RECENT POSTS

Language & Specs/Java

SimpleDateFormat thread safe? 멀티스레드 날짜 깨짐 원인과 해결

반응형

SimpleDateFormat은 thread safe하지 않습니다. 하나의 인스턴스를 여러 스레드가 공유하면(특히 static 필드로 두면) 날짜가 엉뚱한 값으로 뒤섞이거나 NumberFormatException·ArrayIndexOutOfBoundsException간헐적으로 터집니다. 해결책은 세 가지입니다 — ① java.timeDateTimeFormatter로 교체(권장), ② ThreadLocal로 스레드마다 분리, ③ 호출할 때마다 새로 생성. 아래에서 Java 21로 직접 재현해 확인합니다. (기준: Java 21, java.time은 Java 8부터)

 

먼저 결론 — Java 21로 직접 돌린 결과

같은 날짜 문자열 "2026-07-19"스레드 16개가 각각 5,000번씩(총 80,000회) parse → format 하도록 돌렸습니다. 포매터를 어떻게 공유했느냐만 다릅니다.

포매터 공유 방식 잘못된 결과 예외 판정
공유 SimpleDateFormat 1개 37,599회 18,883회 ❌ 깨짐
공유 DateTimeFormatter 1개 0회 0회 ✅ 정상
ThreadLocal로 스레드마다 분리 0회 0회 ✅ 정상

공유 SimpleDateFormat은 8만 번 중 절반 이상이 잘못되거나 예외였습니다. 실제로 나온 깨진 값들입니다.

기대값: 2026-07-19
깨진 값: 64557200-04-01, 199070-11-12, 0013-07-01, 4120-07-01, 9192-07-18 ...
예외:   NumberFormatException, ArrayIndexOutOfBoundsException
⚠️ 수치는 실행할 때마다 달라집니다(경합은 확률적이라). 하지만 공유하면 반드시 깨지고, 안 공유하면 절대 안 깨진다는 결론은 변하지 않습니다.

왜 이 버그가 무서운가 — 테스트는 통과하고 운영에서만 터진다

단위 테스트는 보통 스레드 하나로 돕니다. 그래서 SimpleDateFormatstatic으로 공유해도 테스트는 멀쩡히 통과합니다. 문제는 웹 서버처럼 여러 요청이 동시에 같은 인스턴스를 두드릴 때만 나타납니다. "가끔 날짜가 이상하게 찍힌다", "로그에 뜬금없는 NumberFormatException이 하루에 몇 번 보인다" 같은 재현이 안 되는 증상이라 원인 찾기가 특히 까다롭습니다.

뿌리는 하나입니다 — 가변(mutable) 상태를 여러 스레드가 공유하는 것. 같은 이유로 싱글톤 객체를 멀티스레드에서 안전하게 만들 때도 자바 싱글톤의 이중 체크락과 volatile 같은 장치가 필요합니다.

원인 — SimpleDateFormat은 내부 Calendar를 재사용한다

SimpleDateFormatformat()·parse()를 호출할 때 내부에 들고 있는 Calendar 필드를 매번 덮어쓰며 계산합니다. 이 Calendar가 인스턴스에 공유돼 있어서, 스레드 A가 값을 채우는 도중 스레드 B가 끼어들면 서로의 중간 계산 상태가 뒤섞입니다. 그래서 존재할 수 없는 연도(64557200년)나 배열 인덱스 예외가 나옵니다.

이건 우연이 아니라 공식 문서에 명시된 설계입니다. Oracle Java 21 API 문서의 SimpleDateFormat "Synchronization" 항목은 날짜 포맷이 동기화되어 있지 않다("not synchronized")고 못 박고, 스레드마다 별도 인스턴스를 만들라고 권고합니다. 예전 자바 Date 타입 시절부터 함께 쓰던 클래스라 레거시 코드에 특히 많이 숨어 있습니다.

해결책 3가지

① DateTimeFormatter로 교체 (권장)

Java 8부터 들어온 java.timeDateTimeFormatter불변(immutable) 객체입니다. 상태를 바꾸지 않으니 여러 스레드가 하나를 공유해도 안전합니다. 공식 문서도 "This class is immutable and thread-safe"라고 명시합니다. 새 코드라면 이걸 쓰면 끝입니다.

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

// ✅ static 으로 공유해도 안전하다 (불변 객체)
private static final DateTimeFormatter FMT =
        DateTimeFormatter.ofPattern("yyyy-MM-dd");

String out = LocalDate.parse("2026-07-19", FMT).format(FMT); // 항상 정확

② ThreadLocal로 스레드마다 분리

레거시 사정으로 SimpleDateFormat을 꼭 유지해야 한다면, 공유하지 말고 스레드마다 자기 인스턴스를 갖게 합니다. 공식 문서가 권고하는 "스레드별 별도 인스턴스"가 바로 이것입니다.

import java.text.SimpleDateFormat;

// ✅ 스레드마다 별도 인스턴스를 만들어 준다
private static final ThreadLocal<SimpleDateFormat> SDF =
        ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

String out = SDF.get().format(new java.util.Date());
💡 톰캣 같은 스레드 풀 환경에서는 스레드가 재사용되므로 ThreadLocal에 담긴 값이 오래 남습니다. 특별한 사정이 없으면 ①번(DateTimeFormatter)이 더 깔끔합니다.

③ 호출할 때마다 새로 생성 / synchronized

메서드 안에서 new SimpleDateFormat(...)매번 새로 만들어 지역 변수로 쓰면 공유가 없으니 안전합니다. 가장 단순하지만 호출마다 객체가 생깁니다. 굳이 하나를 공유하고 싶다면 synchronized로 감쌀 수도 있는데, 모든 포맷 호출이 한 줄로 줄서기(직렬화) 때문에 트래픽이 몰리면 병목이 됩니다.

어떤 걸 써야 하나 — 한눈에 비교

방법 안전 비고
DateTimeFormatter (java.time) 권장. 불변·공유 안전. Java 8+
ThreadLocal<SimpleDateFormat> 레거시 유지용. 스레드 풀 누수 주의
매번 new SimpleDateFormat 단순. 호출마다 객체 생성 비용
공유 + synchronized 락으로 직렬화 → 동시성 병목
static 공유 (그냥) 데이터 깨짐·간헐적 예외

자주 묻는 질문 (FAQ)

Q. 로컬·테스트에서는 멀쩡한데 운영에서만 날짜가 깨집니다. 왜죠?
단일 스레드에서는 경합이 없어 문제가 안 보입니다. 웹 서버처럼 여러 요청이 동시에 같은 SimpleDateFormat을 쓰는 순간부터 깨집니다. "재현이 안 되는 날짜 버그"의 단골 원인입니다.

Q. 그냥 synchronized로 감싸면 되는 것 아닌가요?
동작은 안전해지지만 모든 포맷 호출이 순서를 기다리게 되어 성능이 떨어집니다. 같은 안전을 성능 손해 없이 얻으려면 불변인 DateTimeFormatter가 낫습니다.

Q. DateTimeFormatterSimpleDateFormat과 패턴이 같나요?
대부분 비슷하지만 다릅니다. 특히 연도에서 y(연)와 Y(주 기준 연도, week-based-year)가 서로 다르게 동작하니 yyyy를 쓰세요. 타임존이 필요하면 LocalDateTime 대신 ZonedDateTime과 함께 씁니다.

Q. ThreadLocal을 쓰면 메모리 누수가 있다던데요?
스레드 풀에서 스레드가 오래 살아남으면 ThreadLocal 값도 함께 남습니다. 보통은 문제되지 않지만, 민감한 환경이면 사용 후 remove()를 고려하거나 ①번으로 가는 편이 안전합니다.

마무리

정리하면, SimpleDateFormatjava.util.Date공유하는 순간 thread safe하지 않습니다. 새 코드는 처음부터 java.timeDateTimeFormatter·LocalDate를 쓰고, 레거시는 ThreadLocal이나 지역 변수로 공유를 끊으면 됩니다. 날짜를 더하고 빼는 등 실제 날짜 연산이 궁금하다면 자바 날짜 더하고 빼는 계산 방법도 함께 보세요.


📚 참고 출처 (2026년 7월 19일 확인)
· Oracle Java 21 API — SimpleDateFormat (Synchronization)
· Oracle Java 21 API — DateTimeFormatter
· 실측: Java 21.0.4에서 스레드 16 × 5,000회(총 80,000회) 직접 실행

반응형

COMMENTS