반응형

바쁜 분은 이것만

  1. LocalDate는 바뀌지 않는다 → d = d.plusDays(10);처럼 다시 받아야 한다
  2. 두 날짜 사이 총 일수는 ChronoUnit.DAYS.between(a, b) (Period.getDays() 아님)
  3. 연도 포맷은 yyyy — 대문자 YYYY는 연말에 다음 해로 찍힌다

자바에서 날짜만 다룰 때(시간 없이 "2026-10-26")는 java.time.LocalDate를 씁니다. 자주 쓰는 계산과, 결과가 예상과 다르게 나오는 경우를 OpenJDK 27에서 실행해 봤어요. 출력은 모두 실행 결과 그대로입니다.

import java.time.*;
import java.time.format.*;
import java.time.temporal.*;
import java.util.Locale;

1. 만들기와 더하기·빼기

LocalDate d = LocalDate.of(2026, 10, 26);
System.out.println("날짜: " + d);
System.out.println("요일: " + d.getDayOfWeek());
System.out.println("+30일: " + d.plusDays(30));
System.out.println("+1주: " + d.plusWeeks(1));
System.out.println("-1년: " + d.minusYears(1));
날짜: 2026-10-26
요일: MONDAY
+30일: 2026-11-25
+1주: 2026-11-02
-1년: 2025-10-26

오늘 날짜는 LocalDate.now()입니다. 월은 1부터 시작해서 10월은 그냥 10이에요(옛 Calendar처럼 0부터가 아님).

주의: 결과를 다시 받아야 한다

LocalDate x = LocalDate.of(2026, 1, 1);
x.plusDays(10);
System.out.println("plusDays 결과를 안 받으면: " + x);
x = x.plusDays(10);
System.out.println("다시 대입하면: " + x);
plusDays 결과를 안 받으면: 2026-01-01
다시 대입하면: 2026-01-11

LocalDate는 불변(immutable) 객체라 plusDays가 x를 바꾸지 않고 새 날짜를 돌려줍니다. 에러도 안 나서 놓치기 쉬워요. String의 replace와 같은 방식입니다.

2. 월 더하기와 말일

LocalDate jan31 = LocalDate.of(2026, 1, 31);
System.out.println("1/31 + 1개월: " + jan31.plusMonths(1));
System.out.println("1/31 + 1개월 + 1개월: " + jan31.plusMonths(1).plusMonths(1));
System.out.println("1/31 + 2개월: " + jan31.plusMonths(2));
System.out.println("2028 윤년 2/29 + 1년: " + LocalDate.of(2028, 2, 29).plusYears(1));
1/31 + 1개월: 2026-02-28
1/31 + 1개월 + 1개월: 2026-03-28
1/31 + 2개월: 2026-03-31
2028 윤년 2/29 + 1년: 2029-02-28

2월 31일은 없으니 그 달의 마지막 날(2/28)로 맞춰 줍니다. 문제는 그다음이에요. 한 달씩 두 번 더하면 2/28에서 출발하니 3/28이 되고, 한 번에 2개월을 더하면 3/31이 됩니다. 매달 말일 결제일처럼 반복 일정을 만들 때는 이전 결과에 계속 더하지 말고 시작일에 n개월을 더하세요.

3. 두 날짜 사이: Period와 ChronoUnit은 다르다

LocalDate a = LocalDate.of(2026, 3, 15);
LocalDate b = LocalDate.of(2026, 10, 26);
Period p = Period.between(a, b);
System.out.println("Period: " + p + " → " + p.getMonths() + "개월 " + p.getDays() + "일");
System.out.println("p.getDays()만 쓰면: " + p.getDays());
System.out.println("DAYS.between: " + ChronoUnit.DAYS.between(a, b));
System.out.println("MONTHS.between: " + ChronoUnit.MONTHS.between(a, b));
System.out.println("거꾸로: " + ChronoUnit.DAYS.between(b, a));
Period: P7M11D → 7개월 11일
p.getDays()만 쓰면: 11
DAYS.between: 225
MONTHS.between: 7
거꾸로: -225

가장 많이 틀리는 부분입니다. Period는 "7개월 11일"처럼 년·월·일로 나눠서 들고 있어서, getDays()는 총 일수가 아니라 나머지 일수(11)만 돌려줘요. D-day나 경과 일수처럼 총 며칠이 필요하면 ChronoUnit.DAYS.between을 씁니다. 순서를 바꾸면 음수가 나오고, 끝 날짜는 세지 않아요(3/15~3/16이면 1).

4. 비교

System.out.println("isBefore: " + a.isBefore(b) + ", isAfter: " + a.isAfter(b));
System.out.println("equals: " + a.equals(LocalDate.of(2026, 3, 15)));
isBefore: true, isAfter: false
equals: true

객체라서 == 대신 equals나 isEqual로 비교합니다. 같은 날짜를 따로 두 번 만들어 ==로 비교하면 false가 나와요.

LocalDate p = LocalDate.of(2026, 3, 15), q = LocalDate.of(2026, 3, 15);
System.out.println("== : " + (p == q) + ", equals: " + p.equals(q) + ", isEqual: " + p.isEqual(q));
== : false, equals: true, isEqual: true

5. 이번 달 첫날·말일, 다음 월요일

System.out.println("이번 달 첫날: " + b.withDayOfMonth(1));
System.out.println("이번 달 말일: " + b.with(TemporalAdjusters.lastDayOfMonth()));
System.out.println("2월 말일(2028): " + LocalDate.of(2028, 2, 10).with(TemporalAdjusters.lastDayOfMonth()));
System.out.println("다음 월요일: " + b.with(TemporalAdjusters.next(DayOfWeek.MONDAY)));
System.out.println("이번이거나 다음 월요일: " + b.with(TemporalAdjusters.nextOrSame(DayOfWeek.MONDAY)));
System.out.println("lengthOfMonth: " + b.lengthOfMonth() + ", isLeapYear: " + b.isLeapYear());
이번 달 첫날: 2026-10-01
이번 달 말일: 2026-10-31
2월 말일(2028): 2028-02-29
다음 월요일: 2026-11-02
이번이거나 다음 월요일: 2026-10-26
lengthOfMonth: 31, isLeapYear: false

2026-10-26은 월요일이라 next는 다음 주 월요일, nextOrSame은 그날 그대로입니다. 말일은 직접 계산하지 말고 lastDayOfMonth()를 쓰면 윤년도 알아서 맞춰요.

6. 문자열 ↔ 날짜

System.out.println("parse: " + LocalDate.parse("2026-10-26"));
DateTimeFormatter f = DateTimeFormatter.ofPattern("yyyy.MM.dd");
System.out.println("parse 패턴: " + LocalDate.parse("2026.10.26", f));
System.out.println("format: " + b.format(DateTimeFormatter.ofPattern("yyyy년 M월 d일 (E)", Locale.KOREAN)));
parse: 2026-10-26
parse 패턴: 2026-10-26
format: 2026년 10월 26일 (월)

DateTimeParseException: could not be parsed at index 8

LocalDate.parse("2026-10-5");
Exception in thread "main" java.time.format.DateTimeParseException: Text '2026-10-5' could not be parsed at index 8

패턴 없이 parse하면 2026-10-05처럼 월·일이 두 자리여야 합니다. index 8은 일(5)이 시작하는 위치예요. 한 자리 입력도 받으려면 패턴을 "yyyy-M-d"로 주면 됩니다.

yyyy-M-d 로 2026-10-5: 2026-10-05

없는 날짜: of는 에러, parse는 조용히 바꾼다

LocalDate.of(2026, 2, 30);
Exception in thread "main" java.time.DateTimeException: Invalid date 'FEBRUARY 30'

of는 바로 막아 주는데, 패턴을 준 parse는 기본 설정(SMART)에서 2/30을 2/28로 바꿔서 통과시킵니다.

DateTimeFormatter f = DateTimeFormatter.ofPattern("yyyy-MM-dd");
System.out.println("2026-02-30 (기본 SMART): " + LocalDate.parse("2026-02-30", f));

DateTimeFormatter g = DateTimeFormatter.ofPattern("uuuu-MM-dd")
        .withResolverStyle(ResolverStyle.STRICT);
LocalDate.parse("2026-02-30", g);
2026-02-30 (기본 SMART): 2026-02-28
Text '2026-02-30' could not be parsed: Invalid date 'FEBRUARY 30'

사용자 입력을 검증할 때는 ResolverStyle.STRICT를 씁니다. 이때 연도를 yyyy로 두면 정상 날짜도 실패해요. yyyy는 연호 기준 연도(year-of-era)라서, STRICT에서는 연호(서기) 정보가 없으면 날짜를 만들지 않습니다.

DateTimeFormatter y = DateTimeFormatter.ofPattern("yyyy-MM-dd")
        .withResolverStyle(ResolverStyle.STRICT);
LocalDate.parse("2026-02-28", y);
Text '2026-02-28' could not be parsed: Unable to obtain LocalDate from TemporalAccessor: {MonthOfYear=2, DayOfMonth=28, YearOfEra=2026},ISO of type java.time.format.Parsed

그래서 STRICT에서는 uuuu를 씁니다. uuuu-MM-dd + STRICT로는 2026-02-28이 정상으로 읽혔어요.

7. YYYY를 쓰면 연말에 연도가 1 늘어난다

System.out.println("기본 로케일: " + Locale.getDefault());
for (String s : new String[]{"2026-12-26", "2026-12-27", "2026-12-31"}) {
    LocalDate d = LocalDate.parse(s);
    System.out.println(s + " " + d.getDayOfWeek()
        + " → YYYY(기본): " + d.format(DateTimeFormatter.ofPattern("YYYY-MM-dd"))
        + " / YYYY(UK): " + d.format(DateTimeFormatter.ofPattern("YYYY-MM-dd", Locale.UK)));
}
기본 로케일: ko_KR
2026-12-26 SATURDAY → YYYY(기본): 2026-12-26 / YYYY(UK): 2026-12-26
2026-12-27 SUNDAY → YYYY(기본): 2027-12-27 / YYYY(UK): 2026-12-27
2026-12-31 THURSDAY → YYYY(기본): 2027-12-31 / YYYY(UK): 2026-12-31

대문자 YYYY는 달력 연도가 아니라 주 기준 연도(week-based year)입니다. 2027년 1월 1일이 들어 있는 주에 속한 날은 2027로 찍혀요. 한국 로케일은 주가 일요일에 시작해서 2026-12-27(일)부터 연도가 2027로 바뀌었고, 영국 로케일(주 시작 월요일, 다른 규칙)에선 같은 날이 2026으로 나왔습니다. 1년 내내 멀쩡하다가 연말 며칠만 틀리는 버그라 테스트로도 잘 안 잡혀요. 연도는 항상 소문자 yyyy(또는 uuuu)로 쓰세요.

정리

하고 싶은 것 코드
며칠 뒤 d = d.plusDays(n) (다시 대입)
매달 같은 날 반복 start.plusMonths(i) (시작일 기준)
총 며칠 차이 ChronoUnit.DAYS.between(a, b)
○년 ○개월 ○일 Period.between(a, b)
이번 달 말일 d.with(TemporalAdjusters.lastDayOfMonth())
입력 날짜 엄격 검사 ofPattern("uuuu-MM-dd").withResolverStyle(ResolverStyle.STRICT)
연도 포맷 yyyy (YYYY 금지)

참고: Oracle Java SE API 문서 — java.time.LocalDate, Period, ChronoUnit, TemporalAdjusters, DateTimeFormatter(Patterns for Formatting and Parsing, ResolverStyle)

반응형

+ Recent posts