날짜 조회·조작·서식
날짜 파싱 실패를 재현하고 TemporalAdjusters와 DateTimeFormatter로 조회·조정·출력 규칙을 익힙니다.
java.time 값은 불변이므로 조회 메서드는 필드를 읽고 조작 메서드는 새 값을 반환합니다.
문자열 경계에서는 DateTimeFormatter가 문자 위치와 의미를 정의합니다.
파싱 형식과 출력 형식을 분리하고, 지원하지 않는 필드·잘못된 날짜·반환값 누락을 경계에서 처리해야 합니다.
날짜 서식과 파싱 실패
입력은 점으로 연·월·일을 나누지만 formatter는 하이픈을 기대합니다.
LocalDate는 어디까지 읽었는지를 포함한 DateTimeParseException을 던집니다.
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
public final class DatePatternMismatchFailure {
public static void main(String[] args) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
LocalDate date = LocalDate.parse("2025.03.10", formatter);
System.out.println(date);
}
}Exception in thread "main" java.time.format.DateTimeParseException사용자 입력 양식과 formatter를 같은 경계에 두고 예시를 제공합니다.
여러 입력 형식을 허용해야 하면 무작정 문자열 replace를 하기보다 허용 formatter 목록을 순서대로 시도하고 모호한 날짜를 거부합니다.
편의 조회와 동적 필드 조회
고정된 업무 코드에서는 getYear, getMonth, getDayOfWeek가 의도를 잘 드러냅니다.
범용 표 작성기처럼 필드가 데이터로 전달될 때는 ChronoField를 사용합니다.
import java.time.LocalDateTime;
import java.time.temporal.ChronoField;
public final class DateTimeFieldQuery {
public static void main(String[] args) {
LocalDateTime value = LocalDateTime.of(2025, 3, 10, 14, 25, 30);
System.out.println("year=" + value.getYear());
System.out.println("day-of-week=" + value.getDayOfWeek());
System.out.println("hour=" + value.get(ChronoField.HOUR_OF_DAY));
System.out.println("day-range=" + value.range(ChronoField.DAY_OF_MONTH));
System.out.println("offset-supported=" + value.isSupported(ChronoField.OFFSET_SECONDS));
}
}year=2025
day-of-week=MONDAY
hour=14
day-range=1 - 31
offset-supported=falseLocalDateTime에는 오프셋이 없으므로 OFFSET_SECONDS를 지원하지 않습니다.
get을 바로 호출하기보다 isSupported를 확인하거나 ZonedDateTime으로 정보 모델을 바꿉니다.
타입에 없는 정보를 기본값 0으로 꾸며 내지 않습니다.
plus와 with의 차이
plus는 현재 값에서 상대적인 양을 더하고, with는 특정 필드를 절대값으로 교체합니다.
둘 다 원본을 유지합니다.
import java.time.LocalDateTime;
import java.time.Period;
public final class DateTimeManipulation {
public static void main(String[] args) {
LocalDateTime original = LocalDateTime.of(2025, 1, 31, 9, 0);
LocalDateTime plusMonth = original.plusMonths(1);
LocalDateTime nextYear = original.plus(Period.ofYears(1));
LocalDateTime changedHour = original.withHour(18);
System.out.println("original=" + original);
System.out.println("plus-month=" + plusMonth);
System.out.println("next-year=" + nextYear);
System.out.println("changed-hour=" + changedHour);
}
}original=2025-01-31T09:00
plus-month=2025-02-28T09:00
next-year=2026-01-31T09:00
changed-hour=2025-01-31T18:001월 31일에 한 달을 더하면 대상 월의 유효한 마지막 날로 조정됩니다.
“30일 뒤”가 필요하면 plusDays(30)이고 “다음 달 같은 청구일”은 별도 청구 정책이 필요합니다.
메서드 이름만 비슷하다고 업무 의미가 같지 않습니다.
DateTimeManipulation의 같은 원본에서 계산한 세 반환값과 변경되지 않은 원본을 비교한다.
같은 원본에서 이동하거나 필드를 교체한 새 값을 만듭니다
DateTimeManipulation의 세 연산은 모두 original = 2025-01-31T09:00에서 각각 시작합니다. 앞 연산의 결과에 이어 더하는 체인이 아닙니다.
| 조회·연산 | 원문 프로그램 출력값 | 의미 |
|---|---|---|
original | 2025-01-31T09:00 | 원본은 그대로 |
original.plusMonths(1) | 2025-02-28T09:00 | 한 달 이동 후 유효한 월말로 조정 |
original.plus(Period.ofYears(1)) | 2026-01-31T09:00 | 같은 원본에서 1년 이동 |
original.withHour(18) | 2025-01-31T18:00 | 시 필드를 18로 교체 |
original- 원문 프로그램 출력값:
2025-01-31T09:00의미: 원본은 그대로 original.plusMonths(1)- 원문 프로그램 출력값:
2025-02-28T09:00의미: 한 달 이동 후 유효한 월말로 조정 original.plus(Period.ofYears(1))- 원문 프로그램 출력값:
2026-01-31T09:00의미: 같은 원본에서 1년 이동 original.withHour(18)- 원문 프로그램 출력값:
2025-01-31T18:00의미: 시 필드를 18로 교체
plusMonths(1)은 고정 30일을 더하는 연산이 아닙니다. 반환값을 받아야 변경된 값을 사용할 수 있으며, 별도 청구일 정책까지 이 연산이 결정하지는 않습니다.
달력 규칙과 TemporalAdjusters
마지막 날, 다음 월요일, 이번 달 마지막 금요일 같은 규칙은 직접 반복 계산하지 않고 표준 adjuster를 사용할 수 있습니다.
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.temporal.TemporalAdjusters;
public final class BoardDateAdjusters {
public static void main(String[] args) {
LocalDate base = LocalDate.of(2025, 3, 10);
LocalDate endOfMonth = base.with(TemporalAdjusters.lastDayOfMonth());
LocalDate nextMonday = base.with(TemporalAdjusters.next(DayOfWeek.MONDAY));
LocalDate lastFriday = base.with(TemporalAdjusters.lastInMonth(DayOfWeek.FRIDAY));
System.out.println("end=" + endOfMonth);
System.out.println("next-monday=" + nextMonday);
System.out.println("last-friday=" + lastFriday);
}
}end=2025-03-31
next-monday=2025-03-17
last-friday=2025-03-28next는 기준일이 이미 월요일이어도 다음 주 월요일을 반환하고, nextOrSame은 기준일을 그대로 허용합니다.
일정 요구 문구와 일치하는 adjuster를 선택합니다.
2025년3월10일 월요일을 기준으로 한 달력조정 결과와 nextOrSame의 기준일 포함 경계를 비교한다.
next는 기준일을 제외하고 nextOrSame은 포함합니다
기준일은 2025-03-10, 이미 월요일입니다.
| 달력 규칙 | 반환 날짜 | 기준일과의 관계 |
|---|---|---|
lastDayOfMonth() | 2025-03-31 | 같은 달의 마지막 날 |
next(MONDAY) | 2025-03-17 | 기준일을 제외한 다음 월요일 |
lastInMonth(FRIDAY) | 2025-03-28 | 같은 달의 마지막 금요일 |
nextOrSame(MONDAY) | 2025-03-10 | 기준일도 허용하는 경계 비교 |
lastDayOfMonth()- 반환 날짜:
2025-03-31기준일과의 관계: 같은 달의 마지막 날 next(MONDAY)- 반환 날짜:
2025-03-17기준일과의 관계: 기준일을 제외한 다음 월요일 lastInMonth(FRIDAY)- 반환 날짜:
2025-03-28기준일과의 관계: 같은 달의 마지막 금요일 nextOrSame(MONDAY)- 반환 날짜:
2025-03-10기준일과의 관계: 기준일도 허용하는 경계 비교
앞의 세 항목은 BoardDateAdjusters의 출력입니다. 마지막 항목은 본문의 ‘이미 월요일이면 유지’ 설명을 같은 기준일에 적용한 추가 경계 비교입니다. 연습 코드의 입력은 별도로 2025-03-12입니다.
문자 경계와 DateTimeFormatter
ISO_LOCAL_DATE 같은 표준 formatter를 우선하고, 업무 형식이 필요할 때 패턴을 만듭니다.
MM은 월이고 mm은 분이며, HH는 24시간제 시간입니다.
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
public final class BoardDateFormatting {
public static void main(String[] args) {
DateTimeFormatter input = DateTimeFormatter.ofPattern("yyyy년 MM월 dd일 HH:mm", Locale.KOREAN);
DateTimeFormatter output = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
LocalDateTime parsed = LocalDateTime.parse("2025년 03월 10일 14:25", input);
String formatted = parsed.format(output);
System.out.println("parsed=" + parsed);
System.out.println("formatted=" + formatted);
}
}parsed=2025-03-10T14:25
formatted=2025-03-10 14:25입력의 구분자가 맞는 것과 달력 날짜가 유효한 것은 두 검사입니다. DateTimeFormatter의 기본 해석 정책은 SMART이며, ResolverStyle은 달력 필드 조합의 해석을 정합니다. 존재하지 않는 날짜를 거부하려는 입력 경계는 STRICT를 명시합니다.
예시
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.format.ResolverStyle;
public final class StrictDateInput {
public static void main(String[] args) {
DateTimeFormatter smart = DateTimeFormatter.ofPattern("yyyy-MM-dd");
DateTimeFormatter strict = DateTimeFormatter.ofPattern("uuuu-MM-dd")
.withResolverStyle(ResolverStyle.STRICT);
System.out.println("smart=" + LocalDate.parse("2025-02-30", smart));
try {
LocalDate.parse("2025-02-30", strict);
} catch (DateTimeParseException error) {
System.out.println("strict=rejected");
}
System.out.println("leap=" + LocalDate.parse("2024-02-29", strict));
}
}smart=2025-02-28
strict=rejected
leap=2024-02-29SMART는 이 입력의 30일을 2월 마지막 날로 조정합니다. STRICT는 조합을 거부합니다. u는 연도, y는 연호 내 연도이므로 여기서는 연호 없이 연·월·일을 엄격히 읽도록 uuuu를 사용합니다. 원 예제의 yyyy 출력 패턴을 무조건 잘못된 패턴이라고 볼 필요는 없으며 입력 해석의 목적에 맞춰 선택합니다.
답안을 바꾸기 전에 STRICT에 2025-02-29를 넣으면 어떻게 되는지 예상해 보세요. 2025년은 평년이므로 거부되고, 위 2024년 입력은 윤년이라 허용됩니다. 출력 모양이 같아도 날짜 규칙을 별도로 확인해야 하는 이유입니다.
formatter는 불변이며 공유해도 안전합니다.
패턴 문자열을 매 호출마다 만들 필요 없이 상수로 재사용할 수 있습니다.
외부 프로토콜은 사람이 읽는 지역 형식보다 ISO 표준 형식을 우선합니다.
파싱의 입력 계약과 출력 형식을 분리한다
문자열 parse→LocalDateTime→format의 실제 변환과 별도 SMART/STRICT 입력 판정을 보여 줍니다. 정상 왕복 단계와 별도 거부 지점을 분리해 타입과 출력 변화를 따라갈 수 있습니다.
패턴 구분자 불일치 실패와 정상 한국어 날짜 파싱, 출력 포맷을 서로 다른 단계의 계약으로 비교한다.
parse는 입력을 읽고, format은 출력 형식을 정합니다
parse는 문자열을 값으로 읽고, format은 값을 문자열로 표현합니다.
| 경계·원본 | 입력과 형식 | 관찰 결과 |
|---|---|---|
실패 입력 · DatePatternMismatchFailure | 문자열 2025.03.10패턴 | 점과 하이픈이 달라 · DateTimeParseException |
정상 입력 · BoardDateFormatting | 2025년 03월 10일 14:25
한국어 로케일 | 2025-03-10T14:25 |
| 출력 표현 · 같은 정상 프로그램 | 파싱한 값에 적용
| 2025-03-10 14:25 |
- 실패 입력 ·
DatePatternMismatchFailure - 입력과 형식: 문자열
2025.03.10· 패턴yyyy-MM-dd관찰 결과: 점과 하이픈이 달라 ·DateTimeParseException - 정상 입력 ·
BoardDateFormatting - 입력:
2025년 03월 10일 14:25패턴:yyyy년 MM월 dd일 HH:mm로케일: 한국어관찰 결과:2025-03-10T14:25 - 출력 표현 · 같은 정상 프로그램
- 입력과 형식: 파싱한 값에 ·
yyyy-MM-dd HH:mm적용관찰 결과:2025-03-10 14:25
MM은 월, mm은 분입니다. ofPattern의 기본 날짜 해석은 SMART이므로, 구분자 실패 한 사례가 모든 잘못된 달력 날짜의 엄격한 거부를 증명하지는 않습니다.
이 예의 조건
- 02-30은 정상 왕복과 독립 입력이다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 원래 입력 2025년 03월 10일 14:25→LocalDateTime→출력 2025-03-10 14:25. 별도 02-30 입력은 SMART의 02-28/STRICT의 예외로 형식과 달력 유효성을 구분. |
| 8 | 2025-03-10 |
| 구분 | 내용 |
|---|---|
| 설명 | 원래 입력 2025년 03월 10일 14:25→LocalDateTime→출력 2025-03-10 14:25. 별도 02-30 입력은 SMART의 02-28/STRICT의 예외로 형식과 달력 유효성을 구분. |
| 전체 읽기 정보 | 파싱의 입력 계약과 출력 형식을 분리한다 파싱의 입력 계약과 출력 형식을 분리한다 패턴 구분자 불일치 실패와 정상 한국어 날짜 파싱, 출력 포맷을 서로 다른 단계의 계약으로 비교한다. parse는 입력을 읽고, format은 출력 형식을 정합니다 parse 는 문자열을 값으로 읽고, format 은 값을 문자열로 표현합니다. 파싱의 입력 계약과 출력 형식을 분리한다 경계·원본 입력과 형식 관찰 결과 실패 입력 · DatePatternMismatchFailure 문자열 2025.03.10 패턴 yyyy-MM-dd 점과 하이픈이 달라 · DateTimeParseException 정상 입력 · BoardDateFormatting 2025년 03월 10일 14:25 yyyy년 MM월 dd일 HH:mm 한국어 로케일 2025-03-10T14:25 출력 표현 · 같은 정상 프로그램 파싱한 값에 적용 yyyy-MM-dd HH:mm 2025-03-10 14:25 실패 입력 · DatePatternMismatchFailure 입력과 형식: 문자열 2025.03.10 · 패턴 yyyy-MM-dd 관찰 결과: 점과 하이픈이 달라 · DateTimeParseException 정상 입력 · BoardDateFormatting 입력: 2025년 03월 10일 14:25 패턴: yyyy년 MM월 dd일 HH:mm 로케일: 한국어 관찰 결과: 2025-03-10T14:25 출력 표현 · 같은 정상 프로그램 입력과 형식: 파싱한 값에 · yyyy-MM-dd HH:mm 적용 관찰 결과: 2025-03-10 14:25 MM 은 월, mm 은 분입니다. ofPattern 의 기본 날짜 해석은 SMART 이므로, 구분자 실패 한 사례가 모든 잘못된 달력 날짜의 엄격한 거부를 증명하지는 않습니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 문자 입력 원 형식의 날짜/시각을 받습니다. |
|
|
| parse 값 LocalDateTime을 만들며 별도 달력 오류도 판정합니다. |
|
|
| format 출력 정상 값을 다른 형식의 문자열로 만듭니다. |
|
|
파싱의 입력 계약과 출력 형식을 분리한다
문자열 parse→LocalDateTime→format의 실제 변환과 별도 SMART/STRICT 입력 판정을 보여 줍니다. 정상 왕복 단계와 별도 거부 지점을 분리해 타입과 출력 변화를 따라갈 수 있습니다.
패턴 구분자 불일치 실패와 정상 한국어 날짜 파싱, 출력 포맷을 서로 다른 단계의 계약으로 비교한다.
parse는 입력을 읽고, format은 출력 형식을 정합니다
parse는 문자열을 값으로 읽고, format은 값을 문자열로 표현합니다.
| 경계·원본 | 입력과 형식 | 관찰 결과 |
|---|---|---|
실패 입력 · DatePatternMismatchFailure | 문자열 2025.03.10패턴 | 점과 하이픈이 달라 · DateTimeParseException |
정상 입력 · BoardDateFormatting | 2025년 03월 10일 14:25
한국어 로케일 | 2025-03-10T14:25 |
| 출력 표현 · 같은 정상 프로그램 | 파싱한 값에 적용
| 2025-03-10 14:25 |
- 실패 입력 ·
DatePatternMismatchFailure - 입력과 형식: 문자열
2025.03.10· 패턴yyyy-MM-dd관찰 결과: 점과 하이픈이 달라 ·DateTimeParseException - 정상 입력 ·
BoardDateFormatting - 입력:
2025년 03월 10일 14:25패턴:yyyy년 MM월 dd일 HH:mm로케일: 한국어관찰 결과:2025-03-10T14:25 - 출력 표현 · 같은 정상 프로그램
- 입력과 형식: 파싱한 값에 ·
yyyy-MM-dd HH:mm적용관찰 결과:2025-03-10 14:25
MM은 월, mm은 분입니다. ofPattern의 기본 날짜 해석은 SMART이므로, 구분자 실패 한 사례가 모든 잘못된 달력 날짜의 엄격한 거부를 증명하지는 않습니다.
이 예의 조건
- 02-30은 정상 왕복과 독립 입력이다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 원래 입력 2025년 03월 10일 14:25→LocalDateTime→출력 2025-03-10 14:25. 별도 02-30 입력은 SMART의 02-28/STRICT의 예외로 형식과 달력 유효성을 구분. |
| 8 | 2025-03-10 |
| 구분 | 내용 |
|---|---|
| 설명 | 원래 입력 2025년 03월 10일 14:25→LocalDateTime→출력 2025-03-10 14:25. 별도 02-30 입력은 SMART의 02-28/STRICT의 예외로 형식과 달력 유효성을 구분. |
| 전체 읽기 정보 | 파싱의 입력 계약과 출력 형식을 분리한다 파싱의 입력 계약과 출력 형식을 분리한다 패턴 구분자 불일치 실패와 정상 한국어 날짜 파싱, 출력 포맷을 서로 다른 단계의 계약으로 비교한다. parse는 입력을 읽고, format은 출력 형식을 정합니다 parse 는 문자열을 값으로 읽고, format 은 값을 문자열로 표현합니다. 파싱의 입력 계약과 출력 형식을 분리한다 경계·원본 입력과 형식 관찰 결과 실패 입력 · DatePatternMismatchFailure 문자열 2025.03.10 패턴 yyyy-MM-dd 점과 하이픈이 달라 · DateTimeParseException 정상 입력 · BoardDateFormatting 2025년 03월 10일 14:25 yyyy년 MM월 dd일 HH:mm 한국어 로케일 2025-03-10T14:25 출력 표현 · 같은 정상 프로그램 파싱한 값에 적용 yyyy-MM-dd HH:mm 2025-03-10 14:25 실패 입력 · DatePatternMismatchFailure 입력과 형식: 문자열 2025.03.10 · 패턴 yyyy-MM-dd 관찰 결과: 점과 하이픈이 달라 · DateTimeParseException 정상 입력 · BoardDateFormatting 입력: 2025년 03월 10일 14:25 패턴: yyyy년 MM월 dd일 HH:mm 로케일: 한국어 관찰 결과: 2025-03-10T14:25 출력 표현 · 같은 정상 프로그램 입력과 형식: 파싱한 값에 · yyyy-MM-dd HH:mm 적용 관찰 결과: 2025-03-10 14:25 MM 은 월, mm 은 분입니다. ofPattern 의 기본 날짜 해석은 SMART 이므로, 구분자 실패 한 사례가 모든 잘못된 달력 날짜의 엄격한 거부를 증명하지는 않습니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 문자 입력 원 형식의 날짜/시각을 받습니다. |
|
|
| parse 값 LocalDateTime을 만들며 별도 달력 오류도 판정합니다. |
|
|
| format 출력 정상 값을 다른 형식의 문자열로 만듭니다. |
|
|
날짜 API 선택 기준
- 고정 필드 조회는 편의 getter
- 동적 필드 도구는 TemporalField와 isSupported
- 상대적 이동은 plus/minus
- 특정 필드 교체는 withX
- 달력 규칙은 TemporalAdjusters
- 문자열→시간은 parse
- 시간→문자열은 format
불변 반환값을 변수에 받지 않는 실수를 조심하고, 월말과 DST 같은 경계 입력을 반드시 예제에 포함합니다.
연습 문제
입력 날짜가 월요일이면 그대로, 아니면 다음 월요일을 선택하고 yyyy/MM/dd E 형식으로 출력하세요.
해설 보기
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAdjusters;
import java.util.Locale;
public final class NextBoardMondayExercise {
public static void main(String[] args) {
LocalDate input = LocalDate.of(2025, 3, 12);
LocalDate monday = input.with(TemporalAdjusters.nextOrSame(DayOfWeek.MONDAY));
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd E", Locale.ENGLISH);
System.out.println(monday.format(formatter));
}
}2025/03/17 MonnextOrSame이 “이미 월요일이면 유지”라는 요구를 직접 표현합니다.
직접 요일 숫자를 빼는 계산보다 월 경계를 안전하게 넘습니다.