빌드노트
Java에서 연말 날짜를 YYYY로 출력했더니 다음 해가 됐다
Java 날짜 출력에서 YYYY는 주차 기준 연도다. 미국·영국 Locale의 연말·연초 고정 날짜를 비교하고, 날짜 키에는 ISO_LOCAL_DATE·uuuu를, 주차 키에는 주차 연도와 번호를 선택하는 기준을 설명한다.
Java의 DateTimeFormatter로 파일명이나 조회 키를 만들 때, 2020-12-27이 2021-12-27로 출력될 수 있다. 패턴이 YYYY-MM-dd라면 대문자 Y부터 확인한다. Y는 그 날짜가 속한 주차의 기준 연도다. 달력의 연·월·일이 필요하면 date.format(DateTimeFormatter.ISO_LOCAL_DATE)를 쓰거나 패턴을 uuuu-MM-dd로 바꾼다.
YYYY-MM-dd는 주차 기준 연도에 달력의 월·일을 붙인다. 그래서 겉으로는 정상적인 날짜 문자열인데, 입력 날짜와 연도가 다를 수 있다. Java SE 17 문서는 Y를 주차 기준 연도, u를 연도로 구분한다. DateTimeFormatter의 패턴 정의 (새 탭에서 열림)
같은 날짜를 미국·영국 Locale로 출력했다
아래 결과는 실제 파일명 장애 기록이 아니라, 고정 날짜로 따로 실행한 비교다. Windows 11의 OpenJDK 17.0.2에서 미국 Locale.US와 영국 Locale.UK를 formatter에 각각 지정했다. 시스템 시각과 기본 Locale은 바꾸지 않았다.
직접 확인하려면 JDK 17 이상이 필요하다. 터미널에서 java -version으로 버전을 확인하고, jshell --execution local --feedback concise를 실행한 뒤 다음 코드를 붙여 넣는다. 별도 프로젝트나 라이브러리는 필요 없다. Y는 YYYY-MM-dd, u는 uuuu-MM-dd, ISO는 비교 기준인 ISO_LOCAL_DATE의 출력이다.
import java.time.*;
import java.time.format.*;
import java.time.temporal.*;
import java.util.*;
for (var locale : List.of(Locale.US, Locale.UK)) {
var weeks = WeekFields.of(locale);
System.out.printf("%s first=%s minimum=%d%n", locale.toLanguageTag(),
weeks.getFirstDayOfWeek(), weeks.getMinimalDaysInFirstWeek());
var y = DateTimeFormatter.ofPattern("YYYY-MM-dd", locale);
var u = DateTimeFormatter.ofPattern("uuuu-MM-dd", locale);
for (var s : List.of("2020-06-15", "2020-12-26", "2020-12-27", "2021-01-01", "2021-01-04")) {
var d = LocalDate.parse(s);
System.out.printf("%s -> Y=%s u=%s ISO=%s%n", s,
d.format(y), d.format(u), d.format(DateTimeFormatter.ISO_LOCAL_DATE));
}
}
JShell의 입력 안내를 제외한 실제 출력이다. en-GB는 Locale.UK의 언어 태그다. 종료할 때는 /exit를 입력한다.
en-US first=SUNDAY minimum=1
2020-06-15 -> Y=2020-06-15 u=2020-06-15 ISO=2020-06-15
2020-12-26 -> Y=2020-12-26 u=2020-12-26 ISO=2020-12-26
2020-12-27 -> Y=2021-12-27 u=2020-12-27 ISO=2020-12-27
2021-01-01 -> Y=2021-01-01 u=2021-01-01 ISO=2021-01-01
2021-01-04 -> Y=2021-01-04 u=2021-01-04 ISO=2021-01-04
en-GB first=MONDAY minimum=4
2020-06-15 -> Y=2020-06-15 u=2020-06-15 ISO=2020-06-15
2020-12-26 -> Y=2020-12-26 u=2020-12-26 ISO=2020-12-26
2020-12-27 -> Y=2020-12-27 u=2020-12-27 ISO=2020-12-27
2021-01-01 -> Y=2020-01-01 u=2021-01-01 ISO=2021-01-01
2021-01-04 -> Y=2021-01-04 u=2021-01-04 ISO=2021-01-04
미국 Locale에서는 12월 26일까지 같다가 27일에 Y만 다음 연도로 바뀌었다. 영국 Locale에서는 그 12월 날짜가 유지됐지만, 1월 1일의 Y가 이전 연도로 나왔다. Locale만 영국으로 바꾸는 것은 날짜 출력의 수정안이 될 수 없다. 연초에는 반대 방향의 차이가 생긴다. u와 ISO는 두 Locale에서 모두 입력 날짜를 유지했다.
주차의 첫 주는 1월 1일에 시작하지 않을 수 있다
WeekFields는 한 주의 시작 요일과 첫 주에 필요한 최소 일수로 주차를 정한다. 주차 기준 연도에서는 한 주 전체를 하나의 연도에 넣으므로, 연말·연초의 날짜가 달력 연도와 다른 주차 연도에 속할 수 있다. ISO 규칙은 월요일 시작·최소 4일이다. WeekFields의 주차 연도와 ISO 규칙 (새 탭에서 열림)
이번 환경에서 미국 Locale은 일요일 시작·최소 1일이었다. 2021년 1월 1일이 포함된 주는 2020년 12월 27일에 시작한다. 그 주 전체가 2021년의 첫 주가 되어, 아직 12월인 날짜도 YYYY로는 2021이 나온다.
영국 Locale은 월요일 시작·최소 4일이었다. 2020년 12월 28일에 시작한 주에는 2021년 날짜가 1월 1~3일, 세 개뿐이다. 그래서 그 주는 2020년의 마지막 주로 남고, 2021년 첫 주는 1월 4일에 시작한다. 위 두 출력의 경계를 이 규칙으로 설명할 수 있다.
숫자만 출력하는 패턴에도 Locale이 관여한다. Y는 지역별 WeekFields를 사용하는 필드이며, Locale을 생략한 ofPattern(pattern)은 기본 FORMAT Locale을 쓴다. 내 코드에서는 패턴 문자열과 함께 formatter.getLocale()을 확인한다. 위 코드의 Locale을 그 값으로 바꾸면 해당 환경의 주차 규칙도 볼 수 있다. Y의 지역별 필드 매핑 (새 탭에서 열림), ofPattern의 기본 Locale (새 탭에서 열림)
이 비교에는 시각이나 시간대 변환이 없다. LocalDate 자체가 시간과 시간대를 담지 않기 때문이다. 같은 LocalDate에서 u와 ISO는 맞고 Y만 다르다면, 이번처럼 formatter의 연도 필드를 먼저 확인한다. u와 ISO부터 기대 날짜와 다르다면 입력 LocalDate를 만드는 경로를 조사해야 한다. 시각을 날짜로 바꾸는 앞 단계의 시간대 문제까지 이 실험이 배제해 주지는 않는다. LocalDate의 표현 범위 (새 탭에서 열림)
날짜 키인가, 주차 키인가
날짜별 파일명과 조회 키라면 달력 연도가 필요하다. ISO_LOCAL_DATE는 연·월·일을 출력하므로 패턴을 직접 조합할 필요가 없다. 구분자를 바꾸는 등 패턴이 필요하면 uuuu-MM-dd를 사용한다. 키를 만드는 쪽과 조회하는 쪽이 같은 규칙을 쓰는지도 함께 확인한다.
소문자 yyyy도 이번 서기 날짜에서는 uuuu와 같은 결과였다. 그러나 y는 연호 안의 연도, u는 연속된 연도 값이다. 별도로 LocalDate.of(0, 1, 1)을 출력했을 때 uuuu-MM-dd는 0000-01-01, yyyy-MM-dd G는 0001-01-01 BC였다. 여기서 G는 연호다. 연말의 주차 문제와 연호의 차이를 섞어 판단하지 않는다. u와 y가 참조하는 연도 필드 (새 탭에서 열림)
주차별 식별자가 목적이면 Y를 쓸 이유가 있다. 이때는 YYYY-'W'ww처럼 주차 번호와 묶고 Locale도 명시한다. 실제로 2021-01-01의 이 패턴 출력은 미국 Locale에서 2021-W01, 영국 Locale에서 2020-W53이었다. ISO 주차 날짜가 필요하면 ISO_WEEK_DATE를 쓸 수 있다. 같은 날짜의 실제 출력은 2020-W53-5였으며, 마지막 5는 월요일을 1로 센 금요일이다. ISO_WEEK_DATE의 필드 구성 (새 탭에서 열림)
전체 비교는 일반 날짜 하나와 2020년 말·2021년 초의 고정 날짜 일곱 개를 두 Locale에서 실행했다. 다른 JDK 버전·지역 데이터 제공자, Locale 확장, 파싱 동작은 실행 검증하지 않았다. 자기 코드의 날짜 키를 확인할 때도 연중 날짜 한 건으로 끝내지 말고, 12월 26·27일과 1월 1·4일처럼 이번 차이를 드러낸 입력을 함께 비교한다.