Quick Comparison
// 오늘 날짜
LocalDate today = LocalDate.now();
LocalDate birthday = LocalDate.of(1990, 3, 15);
// 날짜 연산 — 새 객체 반환 (불변)
LocalDate nextWeek = today.plusDays(7);
LocalDate lastMonth = today.minusMonths(1);
// 포매팅
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("uuuu-MM-dd");
String formatted = today.format(fmt); // "2026-03-27"| 저장하거나 표현할 대상 | 먼저 쓸 타입 | 빠뜨리면 생기는 문제 |
|---|---|---|
| 생일·마감 날짜 | LocalDate | 시간대를 붙여 날짜가 바뀜 |
| 지역 wall-clock 일정 | LocalDateTime + ZoneId | DST gap/overlap 정책 누락 |
| 로그·API의 같은 순간 | Instant | 서버 zone에 따라 시각이 달라짐 |
| 표시용 지역 시각 | ZonedDateTime | zone 없이 화면마다 다르게 해석 |
갈리는 기준
어떤 날짜/시간 타입을 먼저 떠올리면 되나
| 상황 | 먼저 떠올릴 선택 |
|---|---|
| 날짜만 필요 | LocalDate |
| 날짜와 시간 필요 | LocalDateTime |
| 시간대까지 필요 | ZonedDateTime |
| 타임스탬프 시점 표현 | Instant |
| 문자열 ↔ 날짜 변환 | DateTimeFormatter |
LocalDate / LocalDateTime / ZonedDateTime — 선택 기준
java.time 패키지는 사용 목적에 따라 세 가지 주요 타입을 제공합니다.
LocalDate는 시간대 없이 날짜만 표현합니다. 생일, 예약 날짜처럼 "어느 시간대에서도 같은 날짜"를 나타낼 때 씁니다. LocalDateTime은 날짜 + 시간이지만 여전히 시간대 정보가 없습니다. 로컬 이벤트 시각처럼 시간대가 중요하지 않을 때 씁니다. ZonedDateTime은 시간대까지 포함합니다. 서버 로그, 국제 일정, API 통신처럼 같은 순간이 시간대에 따라 다르게 표시되는 상황에 씁니다.
LocalDate date = LocalDate.of(2026, 3, 27); // 날짜만
LocalDateTime dateTime = LocalDateTime.of(2026, 3, 27, 9, 30); // 날짜 + 시각
ZonedDateTime zoned = ZonedDateTime.now(ZoneId.of("Asia/Seoul")); // 시간대 포함
// Instant — 절대 시각 (유닉스 타임스탬프 기반)
Instant now = Instant.now(); // 시간대 무관한 순간불변 설계 — 날짜 연산이 안전한 이유
java.time의 주요 타입은 불변입니다. 날짜를 수정하는 메서드는 기존 객체를 바꾸지 않고 새 객체를 반환합니다. 이 덕분에 날짜를 여러 곳에서 공유해도 누군가가 값을 바꿔 다른 코드에 영향을 주는 일이 줄어듭니다. (구버전 java.util.Date와 Calendar는 가변이어서 이런 버그가 잦았습니다.)
LocalDate start = LocalDate.of(2026, 1, 1);
LocalDate end = start.plusMonths(3); // 새 객체 반환
// start는 여전히 2026-01-01
// 날짜 비교
System.out.println(start.isBefore(end)); // true
System.out.println(end.isAfter(start)); // true
// 두 날짜 간 차이 — 날짜 단위
long days = ChronoUnit.DAYS.between(start, end); // 90
Period period = Period.between(start, end); // P3M (3개월)
// 두 시각 간 차이 — 시간 단위 (Duration)
LocalDateTime from = LocalDateTime.of(2026, 3, 27, 9, 0);
LocalDateTime to = LocalDateTime.of(2026, 3, 27, 11, 30);
Duration duration = Duration.between(from, to);
long minutes = duration.toMinutes(); // 150
long seconds = duration.toSeconds(); // 9000
// Period: 날짜 기반 (년/월/일), Duration: 시간 기반 (시/분/초/나노초)DateTimeFormatter — 파싱과 포매팅
DateTimeFormatter는 날짜를 문자열로 변환하거나 문자열을 날짜로 파싱할 때 씁니다. 스레드 안전하므로 상수로 만들어 재사용할 수 있습니다. (구버전 SimpleDateFormat은 스레드 안전하지 않아 공유 사용이 위험했습니다.)
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy년 MM월 dd일");
// 날짜 → 문자열
String s = LocalDate.of(2026, 3, 27).format(fmt); // "2026년 03월 27일"
// 문자열 → 날짜
LocalDate d = LocalDate.parse("2026년 03월 27일", fmt);
// ISO 8601 포맷은 상수로 제공됨
String iso = LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE); // "2026-03-27"
// ZonedDateTime → String (API 응답 등)
String timestamp = ZonedDateTime.now(ZoneId.of("UTC"))
.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);LocalDateTime vs ZonedDateTime
로컬 화면 표시나 단일 시스템 내부 값이면 LocalDateTime으로 충분할 때가 많습니다. 반대로 서버 간 전달, 로그, 국제 일정처럼 "같은 순간"을 다뤄야 하면 시간대가 들어간 ZonedDateTime이나 Instant를 먼저 떠올리는 편이 안전합니다.
// 같은 사무실 내부 예약 시각
LocalDateTime meeting = LocalDateTime.of(2026, 4, 6, 14, 0);
// 여러 지역에서 같은 순간을 공유해야 하는 로그/이벤트 시각
ZonedDateTime seoul = ZonedDateTime.of(2026, 4, 6, 14, 0, 0, 0, ZoneId.of("Asia/Seoul"));
Instant eventTime = seoul.toInstant();LocalDateTime을 ZoneId에 붙일 때 DST gap/overlap이 있는 지역에서는 존재하지 않는 시각 또는 두 번 나타나는 시각이 생길 수 있습니다. 예약처럼 local calendar time이 원본인 도메인은 zone과 overlap 선택 정책을 함께 저장하고, 로그·이벤트처럼 같은 순간이 중요한 데이터는 Instant를 저장한 뒤 표시할 때 zone을 적용합니다.
DateTimeFormatter의 yyyy는 year-of-era이고 uuuu는 proleptic year입니다. 일반적인 ISO-style 기계 포맷에는 uuuu-MM-dd를 쓰고, 사용자 표시는 Locale을 명시합니다.
선택 기준
| 상황 | 선택 |
|---|---|
| 날짜만 (생일, 예약일) | LocalDate |
| 날짜 + 시각 (시간대 불필요) | LocalDateTime |
| 시간대가 중요한 일정, 로그 | ZonedDateTime |
| 절대 시각 (타임스탬프) | Instant |
| 두 날짜 간 차이 (일 단위) | ChronoUnit.DAYS.between() |
| 두 날짜 간 차이 (년/월/일) | Period.between() |
| 두 시각 간 차이 (시간/분/초) | Duration.between() |
주의할 점
LocalDate.parse()는 기본적으로 ISO 8601 형식(yyyy-MM-dd)만 파싱합니다. 다른 형식의 문자열을 파싱하려면 반드시 DateTimeFormatter를 명시해야 하며, 형식이 맞지 않으면 DateTimeParseException이 발생합니다.
// ❌ 기본 파싱 — ISO 8601 형식 아니면 예외
LocalDate.parse("27/03/2026"); // DateTimeParseException
// ✅ 포매터 명시
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd/MM/yyyy");
LocalDate date = LocalDate.parse("27/03/2026", fmt); // 정상
// ⚠ 구버전 Date와 혼용 주의 — API 호환이 필요하면 변환
Date legacy = Date.from(LocalDate.now()
.atStartOfDay(ZoneId.systemDefault()).toInstant());시간대가 중요한 값을 LocalDateTime으로만 저장하면 서버 위치가 달라졌을 때 같은 순간을 복원하기 어렵습니다.
// ❌ 시간대 없이 저장
LocalDateTime eventTime = LocalDateTime.now();
// ✅ 절대 시각 또는 시간대 포함 값으로 저장
Instant eventTime = Instant.now();
ZonedDateTime seoulTime = ZonedDateTime.now(ZoneId.of("Asia/Seoul"));참고 링크
2 sources