Java 날짜와 시간 API 완전 정리 (java.time)
Java 8에서 도입된 java.time 패키지는 기존 Date와 Calendar의 고질적인 문제를 해결하고, 불변(Immutable) 설계와 직관적인 API를 제공합니다. 현대 Java 개발에서 날짜·시간 처리의 표준입니다.
1. Date, Calendar의 문제점
java.util.Date의 문제
// Java 1.0 시절 Date — 거의 모든 메서드가 deprecated
Date date = new Date(2026, 5, 1); // deprecated!
// 연도는 1900 기준, 월은 0 기준 → 직관에 어긋남
// 2026년 5월 1일을 만들려면:
Date date = new Date(126, 4, 1); // 126=2026-1900, 4=5월-1
// 문제점
date.getYear() // 126 (1900 기준)
date.getMonth() // 4 (0 기준, 5월이 4)
java.util.Calendar의 문제
Calendar cal = Calendar.getInstance();
cal.set(2026, Calendar.MAY, 1); // 월 상수 제공하지만 여전히 불편
// 1. 가변(Mutable) — 스레드 안전 X
cal.set(Calendar.YEAR, 2027); // 기존 객체 변경
// 2. 월이 0부터 시작 (여전히)
cal.get(Calendar.MONTH) // 4 (5월)
// 3. 요일이 1부터 (일요일=1, 월요일=2, ...)
cal.get(Calendar.DAY_OF_WEEK) // 헷갈림
// 4. 타입 안전 없음
cal.set(Calendar.MONTH, 99); // 컴파일 에러 없음!
// 5. 시간대 처리 복잡
문제점 요약
Date / Calendar 문제점:
┌────────────────────────────────────────────┐
│ 1. 가변(Mutable) → 스레드 안전 X │
│ 2. 월이 0 기준 (1월=0) → 직관성 없음 │
│ 3. 연도가 1900 기준 (Date) │
│ 4. 타입 안전 없음 (정수 상수) │
│ 5. 시간대 처리 어려움 │
│ 6. 날짜 연산 불편 │
│ 7. 포맷팅 API 분리 (SimpleDateFormat) │
│ → SimpleDateFormat은 스레드 안전 X │
└────────────────────────────────────────────┘
2. java.time 패키지 전체 구조
java.time 클래스 계층:
┌──────────────────────────────────────────────────────┐
│ 날짜/시간 클래스 │
│ │
│ 날짜만 LocalDate 2026-05-01 │
│ 시간만 LocalTime 14:30:00 │
│ 날짜+시간 LocalDateTime 2026-05-01T14:30:00 │
│ │
│ 시간대 포함 ZonedDateTime 2026-05-01T14:30:00+09:00[Asia/Seoul] │
│ 오프셋 포함 OffsetDateTime 2026-05-01T14:30:00+09:00 │
│ 머신 시간 Instant 1970-01-01T00:00:00Z 기준 나노초 │
│ │
│ 기간 Period 날짜 기반 (년/월/일) │
│ Duration 시간 기반 (초/나노초) │
│ │
│ 포맷 DateTimeFormatter │
│ 시간대 ZoneId, ZoneOffset │
└──────────────────────────────────────────────────────┘
3. LocalDate, LocalTime, LocalDateTime
LocalDate — 날짜만
// 생성
LocalDate today = LocalDate.now(); // 2026-05-01
LocalDate date = LocalDate.of(2026, 5, 1); // 2026-05-01
LocalDate date2 = LocalDate.of(2026, Month.MAY, 1); // Month 상수 사용
LocalDate date3 = LocalDate.parse("2026-05-01"); // 파싱
// 정보 조회
today.getYear() // 2026
today.getMonthValue() // 5 (1 기준!)
today.getMonth() // MAY (Month 열거형)
today.getDayOfMonth() // 1
today.getDayOfWeek() // FRIDAY (DayOfWeek 열거형)
today.getDayOfYear() // 121
today.lengthOfMonth() // 31 (5월)
today.isLeapYear() // false
// 불변 연산 — 새 객체 반환
LocalDate tomorrow = today.plusDays(1); // 2026-05-02
LocalDate nextMonth = today.plusMonths(1); // 2026-06-01
LocalDate nextYear = today.plusYears(1); // 2027-05-01
LocalDate yesterday = today.minusDays(1); // 2026-04-30
LocalDate lastMonday = today.with(DayOfWeek.MONDAY); // 이번 주 월요일
// 비교
LocalDate d1 = LocalDate.of(2026, 1, 1);
LocalDate d2 = LocalDate.of(2026, 12, 31);
d1.isBefore(d2) // true
d1.isAfter(d2) // false
d1.isEqual(d2) // false
d1.compareTo(d2) // 음수
LocalTime — 시간만
// 생성
LocalTime now = LocalTime.now();
LocalTime time = LocalTime.of(14, 30); // 14:30:00
LocalTime time2 = LocalTime.of(14, 30, 45); // 14:30:45
LocalTime time3 = LocalTime.of(14, 30, 45, 123_000_000); // 나노초 포함
LocalTime time4 = LocalTime.parse("14:30:45");
// 정보 조회
time.getHour() // 14
time.getMinute() // 30
time.getSecond() // 45
time.getNano() // 나노초
// 불변 연산
time.plusHours(2) // 16:30:45
time.minusMinutes(30) // 14:00:45
time.plusSeconds(15) // 14:31:00
// 상수
LocalTime.MIDNIGHT // 00:00
LocalTime.NOON // 12:00
LocalTime.MAX // 23:59:59.999999999
LocalTime.MIN // 00:00
LocalDateTime — 날짜 + 시간
// 생성
LocalDateTime now = LocalDateTime.now();
LocalDateTime dt = LocalDateTime.of(2026, 5, 1, 14, 30, 45);
LocalDateTime dt2 = LocalDateTime.of(LocalDate.now(), LocalTime.now());
LocalDateTime dt3 = LocalDateTime.parse("2026-05-01T14:30:45");
// 날짜/시간 분리
LocalDate date = dt.toLocalDate();
LocalTime time = dt.toLocalTime();
// 불변 연산
dt.plusDays(7).minusHours(2).withMinute(0)
// 비교
dt.isBefore(LocalDateTime.now())
dt.isAfter(LocalDateTime.now())
4. ZonedDateTime, OffsetDateTime, Instant
Instant — 머신 시간 (Unix epoch 기준)
// 1970-01-01T00:00:00Z 기준 나노초
Instant now = Instant.now();
Instant epoch = Instant.EPOCH; // 1970-01-01T00:00:00Z
Instant future = Instant.now().plusSeconds(3600);
now.getEpochSecond() // 초 (long)
now.getNano() // 나노초 부분
// 타임스탬프 변환
long millis = now.toEpochMilli();
Instant fromMillis = Instant.ofEpochMilli(millis);
// Date 변환 (레거시 연동)
Date legacyDate = Date.from(now);
Instant fromDate = legacyDate.toInstant();
ZoneId — 시간대
// 시간대 목록
ZoneId.getAvailableZoneIds() // 600개 이상
ZoneId seoul = ZoneId.of("Asia/Seoul");
ZoneId utc = ZoneId.of("UTC");
ZoneId tokyo = ZoneId.of("Asia/Tokyo");
ZoneId ny = ZoneId.of("America/New_York");
ZoneId system = ZoneId.systemDefault();
ZonedDateTime — 시간대 포함 날짜시간
ZonedDateTime seoulTime = ZonedDateTime.now(ZoneId.of("Asia/Seoul"));
// 2026-05-01T14:30:45+09:00[Asia/Seoul]
ZonedDateTime utcTime = ZonedDateTime.now(ZoneId.of("UTC"));
// 시간대 변환
ZonedDateTime nyTime = seoulTime.withZoneSameInstant(ZoneId.of("America/New_York"));
// 같은 순간, 다른 시간대 표현
// 생성
ZonedDateTime zdt = ZonedDateTime.of(
LocalDateTime.of(2026, 5, 1, 14, 30),
ZoneId.of("Asia/Seoul")
);
OffsetDateTime — UTC 오프셋 포함
// ZoneId(시간대 이름) 없이 오프셋만 포함
OffsetDateTime odt = OffsetDateTime.now(ZoneOffset.of("+09:00"));
// 2026-05-01T14:30:45+09:00
// DB 저장 시 권장 (시간대 정치적 변경에 영향 없음)
OffsetDateTime forDb = ZonedDateTime.now().toOffsetDateTime();
시간대 처리 Best Practice
// 1. 내부 처리는 Instant 또는 UTC
Instant eventTime = Instant.now();
// 2. 표시는 ZonedDateTime으로 변환
ZonedDateTime display = eventTime.atZone(ZoneId.of("Asia/Seoul"));
// 3. DB 저장은 UTC Instant 또는 OffsetDateTime
// TIMESTAMP WITH TIME ZONE 컬럼 권장
// 4. 사용자 입력은 명시적 시간대 포함
ZonedDateTime userInput = ZonedDateTime.parse("2026-05-01T14:30:00+09:00");
Instant stored = userInput.toInstant(); // UTC로 변환 후 저장
5. Period vs Duration
Period — 날짜 기반 기간 (년/월/일)
// 생성
Period period = Period.of(1, 6, 15); // 1년 6개월 15일
Period years = Period.ofYears(2);
Period months = Period.ofMonths(3);
Period days = Period.ofDays(10);
Period week = Period.ofWeeks(2); // 14일
// 두 날짜 사이의 기간
LocalDate start = LocalDate.of(2024, 1, 1);
LocalDate end = LocalDate.of(2026, 5, 1);
Period between = Period.between(start, end);
between.getYears() // 2
between.getMonths() // 4
between.getDays() // 0
between.toTotalMonths() // 28
// 날짜에 적용
LocalDate future = start.plus(period);
LocalDate past = end.minus(Period.ofMonths(6));
Duration — 시간 기반 기간 (초/나노초)
// 생성
Duration duration = Duration.ofHours(2).plusMinutes(30);
Duration d1 = Duration.of(90, ChronoUnit.MINUTES);
Duration d2 = Duration.ofDays(1); // 86400초
Duration d3 = Duration.ofHours(24);
Duration d4 = Duration.ofMinutes(60);
Duration d5 = Duration.ofSeconds(3600);
Duration d6 = Duration.ofMillis(1000);
Duration d7 = Duration.ofNanos(1_000_000_000L);
// 두 시간 사이
LocalTime t1 = LocalTime.of(9, 0);
LocalTime t2 = LocalTime.of(17, 30);
Duration workDay = Duration.between(t1, t2); // 8시간 30분
workDay.toHours() // 8
workDay.toMinutes() // 510
workDay.toSeconds() // 30600
// Instant 사이 경과 시간
Instant start = Instant.now();
// ... 작업 ...
Instant end = Instant.now();
Duration elapsed = Duration.between(start, end);
System.out.println("소요 시간: " + elapsed.toMillis() + "ms");
Period vs Duration 비교
┌──────────────┬──────────────────┬──────────────────┐
│ │ Period │ Duration │
├──────────────┼──────────────────┼──────────────────┤
│ 단위 │ 년/월/일 │ 초/나노초 │
│ 사용 대상 │ LocalDate │ LocalTime/Instant│
│ 윤년 고려 │ O │ X (고정 초) │
│ 서머타임 고려│ O (날짜 기반) │ X (절대 초) │
│ 적합한 상황 │ "3개월 후" │ "3시간 후" │
└──────────────┴──────────────────┴──────────────────┘
6. DateTimeFormatter — 포맷팅/파싱
미리 정의된 포맷터
LocalDate date = LocalDate.of(2026, 5, 1);
date.format(DateTimeFormatter.ISO_DATE) // 2026-05-01
date.format(DateTimeFormatter.ISO_LOCAL_DATE) // 2026-05-01
date.format(DateTimeFormatter.BASIC_ISO_DATE) // 20260501
date.format(DateTimeFormatter.ISO_ORDINAL_DATE) // 2026-121
LocalDateTime dt = LocalDateTime.now();
dt.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) // 2026-05-01T14:30:45
dt.format(DateTimeFormatter.ISO_DATE_TIME)
커스텀 포맷터
// 포맷 패턴
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy년 MM월 dd일 HH:mm:ss");
String formatted = LocalDateTime.now().format(formatter);
// "2026년 05월 01일 14:30:45"
// 파싱
LocalDateTime parsed = LocalDateTime.parse("2026년 05월 01일 14:30:45", formatter);
// 자주 쓰는 패턴
DateTimeFormatter.ofPattern("yyyy-MM-dd") // 2026-05-01
DateTimeFormatter.ofPattern("yyyyMMdd") // 20260501
DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm") // 2026/05/01 14:30
DateTimeFormatter.ofPattern("dd-MMM-yyyy") // 01-May-2026
DateTimeFormatter.ofPattern("E, dd MMM yyyy", Locale.ENGLISH) // Fri, 01 May 2026
포맷 패턴 문자 참조
패턴 문자:
y — 연도 (yyyy: 4자리, yy: 2자리)
M — 월 (MM: 숫자, MMM: 약자, MMMM: 전체)
d — 일 (dd: 2자리)
H — 시 (24시간, HH: 2자리)
h — 시 (12시간)
m — 분 (mm: 2자리)
s — 초 (ss: 2자리)
S — 밀리초 (SSS: 3자리)
E — 요일 (EEE: Mon, EEEE: Monday)
a — AM/PM
z — 시간대 이름 (KST)
Z — 시간대 오프셋 (+0900)
로케일 포맷터
// 지역화 포맷
DateTimeFormatter koFormatter = DateTimeFormatter
.ofLocalizedDate(FormatStyle.FULL)
.withLocale(Locale.KOREAN);
LocalDate.now().format(koFormatter); // 2026년 5월 1일 금요일
DateTimeFormatter enFormatter = DateTimeFormatter
.ofLocalizedDate(FormatStyle.LONG)
.withLocale(Locale.ENGLISH);
LocalDate.now().format(enFormatter); // May 1, 2026
SimpleDateFormat vs DateTimeFormatter
// SimpleDateFormat — 스레드 안전 X
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
// 여러 스레드에서 공유하면 버그 발생!
// DateTimeFormatter — 불변, 스레드 안전 O
private static final DateTimeFormatter FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd");
// 안전하게 static으로 공유 가능
7. 시간대(ZoneId) 처리
한국 시간 처리
ZoneId KOREA = ZoneId.of("Asia/Seoul"); // UTC+9, KST
// 현재 한국 시간
ZonedDateTime nowKorea = ZonedDateTime.now(KOREA);
// UTC → KST 변환
Instant utcInstant = Instant.now();
ZonedDateTime kst = utcInstant.atZone(KOREA);
// KST → UTC 변환
ZonedDateTime kstTime = ZonedDateTime.of(2026, 5, 1, 14, 30, 0, 0, KOREA);
Instant utc = kstTime.toInstant();
서머타임(DST) 처리
ZoneId ny = ZoneId.of("America/New_York");
// ZonedDateTime은 DST 자동 처리
ZonedDateTime before = ZonedDateTime.of(2026, 3, 8, 1, 0, 0, 0, ny);
ZonedDateTime after = before.plusHours(1);
// DST 시작 시 2:00가 3:00로 이동 → 자동 처리
// Duration은 절대 초 기준 (DST 무시)
Duration d = Duration.ofHours(25); // 항상 25*3600초
8. 불변 객체 설계와 날짜 API
java.time이 불변인 이유
// 모든 수정 메서드는 새 객체 반환
LocalDate date = LocalDate.of(2026, 5, 1);
LocalDate modified = date.plusDays(10); // date는 그대로
System.out.println(date); // 2026-05-01 (변경 없음)
System.out.println(modified); // 2026-05-11
// 스레드 안전: 여러 스레드가 같은 LocalDate 공유 가능
private static final LocalDate START_DATE = LocalDate.of(2026, 1, 1);
// 변경 불가이므로 안전하게 공유
날짜 유효성 검증
// 잘못된 날짜 — 즉시 예외
LocalDate.of(2026, 2, 30); // DateTimeException: Invalid date
LocalDate.of(2026, 13, 1); // DateTimeException: Invalid month
// 안전한 파싱
try {
LocalDate date = LocalDate.parse(input);
} catch (DateTimeParseException e) {
// 처리
}
레거시 Date 연동
// Date → LocalDateTime
Date legacyDate = new Date();
LocalDateTime ldt = legacyDate.toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDateTime();
// LocalDateTime → Date
LocalDateTime ldt = LocalDateTime.now();
Date legacyDate = Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant());
// Calendar → LocalDateTime
Calendar cal = Calendar.getInstance();
LocalDateTime ldt = cal.toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDateTime();
9. 자주 쓰는 날짜 연산 모음
// 이번 달 첫날 / 마지막날
LocalDate firstDay = LocalDate.now().withDayOfMonth(1);
LocalDate lastDay = LocalDate.now().with(TemporalAdjusters.lastDayOfMonth());
// 다음 달 첫날
LocalDate nextMonth = LocalDate.now().with(TemporalAdjusters.firstDayOfNextMonth());
// 이번 주 월요일
LocalDate monday = LocalDate.now().with(DayOfWeek.MONDAY);
// 다음 금요일
LocalDate nextFriday = LocalDate.now().with(TemporalAdjusters.next(DayOfWeek.FRIDAY));
// D-Day 계산
LocalDate target = LocalDate.of(2026, 12, 31);
long daysLeft = ChronoUnit.DAYS.between(LocalDate.now(), target);
System.out.println("D-" + daysLeft);
// 나이 계산
LocalDate birthday = LocalDate.of(1990, 8, 15);
int age = Period.between(birthday, LocalDate.now()).getYears();
// 두 날짜 사이 모든 날
LocalDate start = LocalDate.of(2026, 5, 1);
LocalDate end = LocalDate.of(2026, 5, 7);
start.datesUntil(end).forEach(System.out::println); // Java 9+
10. 전체 요약
java.time 선택 가이드:
┌────────────────┬──────────────────────────────────────┐
│ 클래스 │ 사용 시점 │
├────────────────┼──────────────────────────────────────┤
│ LocalDate │ 날짜만 (생일, 기념일) │
│ LocalTime │ 시간만 (업무 시간, 알람) │
│ LocalDateTime │ 시간대 불필요한 날짜시간 │
│ ZonedDateTime │ 시간대 포함 (글로벌 서비스) │
│ OffsetDateTime │ DB 저장, API 응답 (시간대 정보 포함) │
│ Instant │ 로그, 이벤트 타임스탬프 (UTC 절대값) │
│ Period │ 날짜 기반 기간 (D+30, 3개월 후) │
│ Duration │ 시간 기반 경과 (실행 시간, 타임아웃) │
└────────────────┴──────────────────────────────────────┘
핵심 원칙:
- Date/Calendar 신규 코드에서 사용 금지
- DateTimeFormatter는 static final로 공유
- DB 저장: UTC Instant 또는 OffsetDateTime
- 사용자 표시: ZonedDateTime + 시간대 변환