본문 바로가기
Java & Spring

java.time 제대로 쓰기: 타임존/포맷 실수 7가지와 안전한 사용법

by yamoojin83 2025. 9. 28.

 

java.time 제대로 쓰기: 타임존/포맷 실수 7가지와 안전한 사용법

java.time 패키지는 시간대를 포함한 날짜·시간을 타입으로 구분합니다. 문제는 “어떤 타입을 언제 써야 하는지”를 헷갈리기 쉽다는 점입니다. 아래에서 현업에서 자주 발생하는 실수 7가지를 정리하고, 각 상황에서의 안전한 코드를 제시합니다.

한 줄 요약: 보관/전송은 UTC의 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를 무시한 시간 더하기/빼기

써머타임 시작/종료 구간에서는 “겹침/누락”이 생깁니다. 비즈니스 규칙이 “벽시계 기준”이면 ZonedDateTimeplusHours를 적용하고, 절대적인 경과 시간을 다루면 InstantDuration을 적용하세요.


// 벽시계 기준(일정 이동)
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 형태, VVAsia/Seoul 같은 이름 있는 타임존입니다.


DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mmXXX [VV]");
  

7) DB에 로컬 문자열로 저장

DB에는 UTC Instant 또는 timestamp with time zone 유형을 쓰세요. 문자열 저장은 비교/정렬/타임존 변환에서 문제를 일으킵니다.

 

안전한 변환 레시피

목표 코드
Instant → 사용자의 현지 시간

ZonedDateTime view = instant.atZone(ZoneId.of(userZone));
        
현지 시간 → Instant

Instant utc = local.atZone(ZoneId.of(userZone)).toInstant();
        
문자열 파싱(오프셋 포함)

OffsetDateTime t = OffsetDateTime.parse("2025-10-18T21:00:00+09:00");
        
커스텀 포맷

DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss VV");
String s = zoned.format(fmt);
        

타입 선택 치트시트(이미지)

java.time cheat sheet image
저장/전송은 Instant(UTC), 사용자 표시/일정은 ZonedDateTime, API 오프셋 전달은 OffsetDateTime.

 

 

 

스케줄링 팁(DST 안전)

  • 매일 “오전 9시” 같은 벽시계 반복은 ZonedDateTime + ZoneId 기준으로 계산.
  • “24시간 후 만료” 같은 절대 시간은 Instant + Duration 사용.
  • 서버와 DB의 타임존을 고정(UTC)하고, 클라이언트에서 표시 변환.

 

요약

  • 스레드 안전한 DateTimeFormatter를 재사용.
  • 보관/전송은 Instant, 표시/일정은 ZonedDateTime.
  • 포맷 토큰(MM/mm, yyyy/YYYY) 혼동 주의.

 

FAQ

Q1. 한국(Asia/Seoul)은 DST가 없는데 왜 ZonedDateTime이 필요한가요?
A. 사용자 기반이 글로벌이거나 서버가 다른 지역에 있을 수 있습니다. 또한 정책 변경에 대비하려면 규칙을 포함한 타입이 안전합니다.

Q2. OffsetDateTimeZonedDateTime 중 무엇을 저장하나요?
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 스레드풀 사이즈/큐 전략

👉 9편: CompletableFuture allOf/anyOf로 외부 API 병렬화

👉 10편: 예외 처리 베스트 프랙티스(체크/언체크, 경계 설계)