java.time 제대로 쓰기: 타임존/포맷 실수 7가지와 안전한 사용법
java.time 패키지는 시간대를 포함한 날짜·시간을 타입으로 구분합니다. 문제는 “어떤 타입을 언제 써야 하는지”를 헷갈리기 쉽다는 점입니다. 아래에서 현업에서 자주 발생하는 실수 7가지를 정리하고, 각 상황에서의 안전한 코드를 제시합니다.
Instant, 표시는 ZonedDateTime, 연산은 상황에 따라 Period/Duration을 고르세요.자주 하는 실수 7가지
1) Date/Calendar/SimpleDateFormat 계속 사용
SimpleDateFormat은 스레드 안전하지 않습니다. 병렬 환경에서 포맷터를 공유하면 간헐적 오류가 납니다. DateTimeFormatter는 불변이므로 재사용해도 안전합니다.
// 권장
private static final DateTimeFormatter ISO =
DateTimeFormatter.ISO_OFFSET_DATE_TIME;
2) LocalDateTime으로 “순간(Instant)”을 표현
LocalDateTime은 시간대 정보가 없습니다. 서버 지역이 바뀌면 같은 값이 다른 실제 시각을 의미할 수 있습니다. 저장/전송에는 Instant 또는 OffsetDateTime을 쓰세요.
// 저장/전송
Instant now = Instant.now(); // UTC instant
String iso = now.toString(); // 2025-09-28T05:21:00Z
// 사용자 표시
ZonedDateTime view = now.atZone(ZoneId.of("Asia/Seoul"));
3) 시스템 기본 시간대(System default)에 암묵 의존
컨테이너/서버마다 기본 시간대가 다르면 결과가 달라집니다. ZoneId를 항상 명시하세요.
ZonedDateTime departure = LocalDateTime.parse("2025-10-18T21:00")
.atZone(ZoneId.of("America/New_York"));
4) DST를 무시한 시간 더하기/빼기
써머타임 시작/종료 구간에서는 “겹침/누락”이 생깁니다. 비즈니스 규칙이 “벽시계 기준”이면 ZonedDateTime에 plusHours를 적용하고, 절대적인 경과 시간을 다루면 Instant에 Duration을 적용하세요.
// 벽시계 기준(일정 이동)
ZonedDateTime next = meeting.plusHours(1); // DST 규칙 반영
// 절대 경과 시간(타이머/만료)
Instant expireAt = issued.plus(Duration.ofHours(1));
5) 포맷 토큰 혼동: MM vs mm, HH vs hh, yyyy vs YYYY
MM=월,mm=분HH=시(24h),hh=시(12h)yyyy=연도,YYYY=주(week) 기반 연도(연말 주의!)
DateTimeFormatter f = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
6) 오프셋/타임존 포맷 혼동
Z는 UTC(+00:00), XXX는 +09:00 형태, VV는 Asia/Seoul 같은 이름 있는 타임존입니다.
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mmXXX [VV]");
7) DB에 로컬 문자열로 저장
DB에는 UTC Instant 또는 timestamp with time zone 유형을 쓰세요. 문자열 저장은 비교/정렬/타임존 변환에서 문제를 일으킵니다.
안전한 변환 레시피
| 목표 | 코드 |
|---|---|
| Instant → 사용자의 현지 시간 |
|
| 현지 시간 → Instant |
|
| 문자열 파싱(오프셋 포함) |
|
| 커스텀 포맷 |
|
타입 선택 치트시트(이미지)

스케줄링 팁(DST 안전)
- 매일 “오전 9시” 같은 벽시계 반복은
ZonedDateTime+ZoneId기준으로 계산. - “24시간 후 만료” 같은 절대 시간은
Instant+Duration사용. - 서버와 DB의 타임존을 고정(UTC)하고, 클라이언트에서 표시 변환.
요약
- 스레드 안전한
DateTimeFormatter를 재사용. - 보관/전송은
Instant, 표시/일정은ZonedDateTime. - 포맷 토큰(
MM/mm,yyyy/YYYY) 혼동 주의.
FAQ
Q1. 한국(Asia/Seoul)은 DST가 없는데 왜 ZonedDateTime이 필요한가요?
A. 사용자 기반이 글로벌이거나 서버가 다른 지역에 있을 수 있습니다. 또한 정책 변경에 대비하려면 규칙을 포함한 타입이 안전합니다.
Q2. OffsetDateTime과 ZonedDateTime 중 무엇을 저장하나요?
A. 장기 보관·비교가 목적이면 Instant가 가장 단순합니다. API 스펙상 오프셋이 필요하면 OffsetDateTime, 일정/규칙은 ZonedDateTime을 사용하세요.
관련 글
👉 1편: ArrayList vs LinkedList: 언제 무엇을 쓰나 (+O(1) 착각 5가지)
👉 2편: 제네릭 와일드카드 완전정복(PECS 암기팁 포함)
👉 3편: Stream API: for→stream 리팩터링 10가지(성능/가독 균형)
👉 4편: Optional.orElse vs orElseGet 차이와 NPE 방지
👉 5편: java.time 제대로 쓰기(타임존/포맷 실수 7가지)
👉 6편: Java Record vs Lombok DTO 선택 기준
👉 7편: NIO.2로 폴더 스캔/감시 구현(FileVisitor/WatchService)
👉 8편: ExecutorService 스레드풀 사이즈/큐 전략
'Java & Spring' 카테고리의 다른 글
| NIO.2로 폴더 스캔/감시 구현(FileVisitor/WatchService) (0) | 2025.09.29 |
|---|---|
| Java Record vs Lombok DTO 선택 기준: 언제 무엇을 쓸까 (0) | 2025.09.28 |
| Optional.orElse vs orElseGet 차이와 NPE 방지: 실무 규칙 7가지 (0) | 2025.09.28 |
| Stream API: for→stream 리팩터링 10가지(성능/가독 균형) (0) | 2025.09.28 |
| 제네릭 와일드카드 완벽 마스터: PECS(Producer Extends, Consumer Super) 암기팁 (0) | 2025.09.28 |