* 이 게시물은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
(자바 버전 여행 ⑥)
자바를 좀 해본 사람이면 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이 불변이라는 걸 확인하자.
LocalDate로 오늘과 목표 날짜를 만든다.ChronoUnit.DAYS.between(...)으로 남은 일수를 구한다.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 | 타임존 포함 날짜·시각 |
| Instant | UTC 타임스탬프(기계용) |
| 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)로 하고 표시할 때만 지역 타임존으로 변환하는 게 정석입니다.

답글 남기기