[자바] 6. 시간 : java.time으로 Date 지옥 탈출 (Java 8 날짜·시간 API)

자바 버전 여행 6편 - java.time으로 Date 지옥 탈출

작성자

카테고리:

* 이 게시물은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

(자바 버전 여행 ⑥)

자바를 좀 해본 사람이면 Date와 Calendar에 한 번쯤 데였을 것이다. 나도 “1월이 왜 0이야?”에서 시작해, 멀티스레드에서 SimpleDateFormat이 값을 섞어버리는 것까지 골고루 당했다. 자바 8은 아예 새 API java.time을 들여와 이 문제들을 정리했다.

낡은 Date가 왜 문제였나

  • 가변(mutable) — 여기저기서 값을 바꿔 추적이 어렵다.
  • 월이 0부터 — 3월을 넣으려면 2를 쓴다. 버그의 단골.
  • 스레드 안전하지 않음 — SimpleDateFormat을 공유하면 값이 깨진다.
  • 타임존·기간 계산이 직관적이지 않다.

java.time의 핵심 클래스

용도에 따라 클래스가 딱 나뉜다. 이름만 봐도 뭘 담는지 보인다.

클래스담는 것예
LocalDate날짜만2026-08-04
LocalTime시간만21:30
LocalDateTime날짜+시간2026-08-04T21:30
ZonedDateTime타임존까지…+09:00 Asia/Seoul
Duration / Period시간 간격 / 날짜 간격2시간 / 3일

Before / After

// Before — Calendar, 월이 0부터라 8월인데 7
Calendar c = Calendar.getInstance();
c.set(2026, 7, 4);          // 2026-08-04 (헷갈림)
c.add(Calendar.DAY_OF_MONTH, 10);
Date result = c.getTime();
// After — 읽는 그대로
LocalDate day = LocalDate.of(2026, 8, 4);   // 8월은 8
LocalDate after = day.plusDays(10);         // 불변: 새 값을 반환
boolean isLeap = day.isLeapYear();

눈여겨볼 점 두 가지. 8월을 8로 그대로 쓴다는 것, 그리고 plusDays가 원본을 바꾸지 않고 새 객체를 돌려준다는 것(불변). 그래서 여러 곳에서 공유해도 안전하다.

포매팅·파싱과 간격

// 문자열 <-> 시간
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
String text = LocalDateTime.now().format(fmt);
LocalDate parsed = LocalDate.parse("2026-08-04");

// 두 시각 사이 간격
Duration d = Duration.between(start, end);   // 시/분/초
Period  p = Period.between(day1, day2);       // 년/월/일

DateTimeFormatter는 SimpleDateFormat과 달리 불변이라 스레드 안전하다. 상수로 하나 만들어 재사용해도 값이 섞이지 않는다.

직접 해보기 — 디데이 계산하고 불변 확인하기

오늘부터 특정 날짜까지 며칠 남았는지 구하고, java.time이 불변이라는 걸 확인하자.

  1. LocalDate로 오늘과 목표 날짜를 만든다.
  2. ChronoUnit.DAYS.between(...)으로 남은 일수를 구한다.
  3. plusDays 후 원본이 그대로인지 출력해 불변을 확인한다.
LocalDate today  = LocalDate.of(2026, 8, 1);
LocalDate target = LocalDate.of(2026, 12, 25);

long dday = ChronoUnit.DAYS.between(today, target);
System.out.println("D-" + dday);

LocalDate later = today.plusDays(10);
System.out.println(today + " / " + later);   // 원본은 안 바뀐다

실행해 보면 이렇게 나온다.

D-146
2026-08-01 / 2026-08-11

여기서 조금씩 바꿔 보자.

  • Period.between(today, target)로 “몇 개월 며칠” 형태로도 구해 본다.
  • DateTimeFormatter.ofPattern("yyyy년 M월 d일")로 오늘을 한국식으로 포매팅한다.

한 걸음 더 — SimpleDateFormat을 여러 스레드에서 공유하면 값이 깨지는 걸 재현해 보고, DateTimeFormatter는 안 그런지 비교하자.

✅ 확인 — LocalDate로 날짜 간격을 구하고, plusDays가 원본을 바꾸지 않는다(불변)는 걸 확인했으면 통과다.

어떤 클래스를 언제 — 지도

java.time은 클래스가 여럿이라 처음엔 헷갈린다. ‘무엇을 담느냐’로 나눠 보면 명확하다.

클래스담는 것
LocalDate날짜(2026-08-19)
LocalTime시각(14:30)
LocalDateTime날짜+시각(타임존 없음)
ZonedDateTime타임존 포함 날짜·시각
InstantUTC 타임스탬프(기계용)
Duration / Period시간 간격 / 날짜 간격

불변이라 안전하다 — 스레드도 안심

java.time 객체는 모두 불변이다. plusDays(1)는 원본을 바꾸지 않고 새 객체를 돌려준다. 그래서 SimpleDateFormat이 멀티스레드에서 값을 섞던 악명 높은 문제가 사라진다 — DateTimeFormatter는 불변이라 스레드 안전하다. 값이 안 바뀌니 여기저기 넘겨도 추적이 쉽다.

LocalDate today = LocalDate.now();
LocalDate tomorrow = today.plusDays(1);   // today는 그대로, 새 객체 반환
long dday = java.time.temporal.ChronoUnit.DAYS.between(today, target);

타임존을 제대로 — Instant vs ZonedDateTime

헷갈리는 지점. 저장·전송은 Instant(UTC 기준 절대 시각)로, 사람에게 보여줄 땐 ZonedDateTime으로 타임존을 입힌다. LocalDateTime은 타임존이 없어 ‘몇 시’만 있고 ‘어디의 몇 시’는 모른다 — 이걸 그대로 저장하면 나중에 애매해진다. ‘기계는 Instant, 사람은 Zoned‘로 외우면 된다.

옛 Date와 섞일 때

레거시 API가 Date를 요구하면 경계에서만 변환한다. Date.from(instant)·date.toInstant()로 오간다. 새 코드는 java.time으로 쓰되, 옛 API와 만나는 지점에서만 변환하는 게 정석이다. 예전에 Joda-Time을 쓰던 프로젝트도 이제 표준 java.time으로 옮기는 게 권장된다(Joda 저자 본인이 그렇게 안내한다).

Date legacy = Date.from(Instant.now());
Instant back = legacy.toInstant();

자바 8 여행의 마지막 편이 남았다. 다음 편은 눈에 잘 안 띄지만 자바 8이 문법과 내부를 함께 손본 지점 — 인터페이스의 default·static 메서드와 Metaspace다.

자주 묻는 질문 (FAQ)

java.util.Date를 아직 써도 되나요?

가능하면 java.time으로 바꾸세요. Date는 가변·스레드 비안전 문제가 있어 LocalDate·LocalDateTime이 더 안전합니다.

타임존까지 다루려면 뭘 쓰나요?

ZonedDateTime을 씁니다. 저장·계산은 UTC(Instant)로 하고 표시할 때만 지역 타임존으로 변환하는 게 정석입니다.

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다