Skip to Content

12장. 포맷과 파싱의 함정 — DateTimeFormatter, 로케일, withZone

만화로 보는 요약 — 먼저 읽어보세요

만화로 보는 요약 — 먼저 읽어보세요

환경에 따라 달라지는 로케일 → zone 없는 포맷의 정보 손실 → 정확한 패턴까지 명시하는 문자열 계약.

면접 실전 질문: ① DateTimeFormatter가 SimpleDateFormat과 달리 스레드 안전한 이유는? ② yyyy와 YYYY는 무엇이 다르고 언제 갈리는가? ③ Instant를 포맷할 때 withZone()은 무엇을 해결하는가?


같은 순간을 같은 포맷터로 찍어도 로케일과 zone이 다르면 다른 문자열이 나온다 — 예외는 던져지지 않고, 틀린 텍스트만 조용히 남는다.

면접 실전 질문: ① DateTimeFormatterSimpleDateFormat과 달리 스레드 안전한 이유는? ② yyyyYYYY는 무엇이 다르고, 언제 서로 다른 값을 내놓는가? ③ Instant를 zone 없는 포맷터로 포맷하면 어떻게 되며, withZone()은 정확히 무엇을 해결하는가?


배경 — 텍스트가 되는 순간, 검증이 끝난다

11장은 질문 하나를 남기고 끝났습니다. 시간을 정확하게 표현하고(9~10장) 안전하게 측정하는 법(11장)을 다졌는데, 그 값은 결국 어딘가에 찍혀야 합니다. 로그 한 줄, API 응답의 문자열 한 필드, 화면에 뜨는 날짜 하나로요. Instant를 문자열로 바꾸는 이 경계가 이번 장의 무대입니다.

지금까지 본 함정들과 이 장의 함정은 결이 다릅니다. 6장의 존재하지 않는 지역 시각은 예외를 던졌습니다. 11장의 음수 DurationThread.sleep()을 즉시 깨뜨렸습니다. 실패가 소리를 냈다는 공통점이 있었습니다. 그런데 포맷은 다릅니다. 틀린 포맷도 보통은 문자열을 뱉어냅니다. 컴파일도 되고, 실행도 되고, 화면에도 찍힙니다. 그 문자열이 의도한 값인지 아닌지는 사람이 눈으로 확인하기 전엔 아무도 모릅니다. 이 장에서 다룰 세 가지 함정 — 로케일, zone, 패턴 문자 — 이 전부 이 성질을 공유합니다. 아무것도 깨지지 않은 채로 버그가 배포됩니다.

먼저 좋은 소식부터 짚습니다. 8장에서 SimpleDateFormatstatic 필드로 공유했다가 스레드 50개가 10,000번 호출하는 사이 4,873번 잘못된 결과와 4,045번의 예외(ClassCastException 포함)를 본 적이 있습니다. SimpleDateFormatformat()parse()가 내부 Calendar 상태를 스레드 간 조율 없이 공유해 썼기 때문입니다. DateTimeFormatter는 이 문제를 정면으로 고칩니다. 불변(immutable)이고 스레드 안전(thread-safe)합니다. 자바독이 클래스 선언 바로 아래에 명시한 계약이고, 8장의 사고를 겪은 다음 읽으면 왜 이 한 줄이 그렇게 중요한지 바로 와닿습니다. static final 필드로 얼마든지 공유해도 됩니다 — 이 장에서 실측으로 확인합니다. 문제는 스레드 안전성이 아닙니다. 그 대신 로케일과 zone이라는, 컴파일러도 런타임도 잡아주지 않는 두 개의 새로운 함정이 기다리고 있습니다.

스토리 — 안전해진 것과, 여전히 조용히 틀리는 것

스레드는 안전하다 — 직접 확인

먼저 8장과 똑같은 조건으로 DateTimeFormatterstatic final 필드에 담아 스레드 50개가 200번씩, 총 10,000번 포맷하게 만들어봤습니다.

import java.time.Instant; import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; public class FormatterThreadSafe { // 8장의 SimpleDateFormatRace와 똑같은 모양으로 static 필드 공유. static final DateTimeFormatter SHARED = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneOffset.UTC); public static void main(String[] args) throws InterruptedException { int threadCount = 50; int iterationsPerThread = 200; Instant fixed = Instant.ofEpochMilli(1_700_000_000_000L); String expected = SHARED.format(fixed); System.out.println("기대하는 포맷 결과 = " + expected); AtomicInteger wrongResult = new AtomicInteger(0); AtomicInteger exceptionCount = new AtomicInteger(0); CountDownLatch latch = new CountDownLatch(threadCount); for (int t = 0; t < threadCount; t++) { new Thread(() -> { try { for (int i = 0; i < iterationsPerThread; i++) { try { if (!expected.equals(SHARED.format(fixed))) wrongResult.incrementAndGet(); } catch (Exception e) { exceptionCount.incrementAndGet(); } } } finally { latch.countDown(); } }).start(); } latch.await(); System.out.println("스레드 수 = " + threadCount + ", 스레드당 반복 = " + iterationsPerThread); System.out.println("잘못된(기대와 다른) 결과 횟수 = " + wrongResult.get()); System.out.println("예외 발생 횟수 = " + exceptionCount.get()); } }
기대하는 포맷 결과 = 2023-11-14 22:13:20 스레드 수 = 50, 스레드당 반복 = 200 잘못된(기대와 다른) 결과 횟수 = 0 예외 발생 횟수 = 0

10,000번 중 오류 0, 예외 0입니다. 8장의 SimpleDateFormat이 같은 조건에서 오류 4,873회, 예외 4,045회를 냈던 것과 정확히 대비됩니다. DateTimeFormatter는 내부에 가변 상태를 두지 않고 매 호출마다 새 파싱 컨텍스트를 만들기 때문에, 여러 스레드가 하나의 인스턴스를 static final로 공유해도 안전합니다. 스레드 안전성은 이제 걱정할 대상이 아닙니다. 걱정할 대상은 따로 있습니다.

함정 ① 로케일 — 같은 코드, 다른 서버, 다른 문자열

패턴에 월 이름이나 요일 이름을 넣는 순간, 출력은 로케일에 따라 달라집니다. 문제는 그 로케일을 코드 어디에서도 명시하지 않았을 때입니다.

import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.Locale; public class LocaleSilentSwitch { public static void main(String[] args) { LocalDate d = LocalDate.of(2024, 3, 5); DateTimeFormatter base = DateTimeFormatter.ofPattern("EEEE, MMMM d, yyyy"); System.out.println("JVM 기본 로케일 = " + Locale.getDefault()); System.out.println("base(로케일 미지정) = " + d.format(base)); System.out.println("base.withLocale(Locale.KOREAN) = " + d.format(base.withLocale(Locale.KOREAN))); System.out.println("base.withLocale(Locale.US) = " + d.format(base.withLocale(Locale.US))); } }
JVM 기본 로케일 = en_US base(로케일 미지정) = Tuesday, March 5, 2024 base.withLocale(Locale.KOREAN) = 화요일, 3월 5, 2024 base.withLocale(Locale.US) = Tuesday, March 5, 2024

(JVM 기본 로케일에 따라 갈리는 값입니다 — 이 컨테이너는 en_US라 로케일 미지정 결과가 Locale.US와 같게 나왔습니다. 기본 로케일이 ko_KR인 서버였다면 미지정 결과가 첫 줄과 같은 값이 아니라 세 번째 줄처럼 나왔을 겁니다.) DateTimeFormatter.ofPattern(String)은 로케일을 명시하지 않으면 Locale.getDefault(Locale.Category.FORMAT)를 조용히 가져다 씁니다. 개발자 노트북이 ko_KR이고 배포 서버가 en_US(또는 그 반대)면, 같은 코드가 환경마다 다른 문자열을 찍습니다. 8장에서 본 Date.toString()이 JVM 기본 타임존을 몰래 빌려 쓰던 함정과 정확히 같은 모양입니다. 이번엔 타임존이 아니라 로케일이 그 자리를 차지했을 뿐입니다.

더 위험한 방향은 파싱입니다. 로케일이 다른 두 서버가 같은 패턴으로 날짜를 주고받는데 한쪽은 요일·월 이름을 영어로, 다른 쪽은 한국어로 기대한다면, 포맷은 몰라도 파싱은 DateTimeParseException으로 즉시 죽습니다. 다행히 이건 그나마 시끄러운 실패입니다. 더 조용한 쪽은 숫자만 쓰는 패턴입니다. yyyy-MM-dd처럼 이름이 안 들어간 패턴은 로케일이 달라도 숫자 서식(자릿수 채움, 구분자)이 그대로라 대부분 로케일에서 같은 문자열을 냅니다 — 로케일이 숫자 자체의 표기 체계(아라비아 숫자가 아닌 다른 자릿수 기호)까지 바꾸려면 별도의 확장 태그가 필요하고, 흔히 쓰는 Locale.US·Locale.KOREA 조합에서는 자릿수가 그대로 유지됩니다. 로케일 함정은 대부분 이름(월·요일)에서 터지지, 숫자 자체에서 터지는 일은 드뭅니다. 그래서 패턴에 이름이 없으면 안전하다고 착각하기 쉽지만, 다음 절에서 볼 패턴 문자 함정은 숫자 패턴 안에서도 일어납니다.

규칙은 하나입니다. 로케일을 요구하는 출력이라면 Locale을 항상 명시적으로 지정합니다. DateTimeFormatter.ofPattern(pattern, Locale.US)처럼 생성 시점에 박아 넣거나, API 응답처럼 고정된 포맷이 필요하면 DateTimeFormatter.ISO_INSTANT류의 로케일-불변 상수를 씁니다(7장). “서버 기본값이 알아서 맞겠지”는 가정이 아니라 도박입니다.

함정 ② zone — Instant는 zone을 모른다

9장에서 확정한 규칙을 다시 씁니다. Instant는 시간축 위의 점이고, 지역 시각(연·월·일·시·분)은 zone을 적용해야만 나옵니다. 그런데 yyyy-MM-dd HH:mm:ss 같은 패턴은 정확히 그 지역 시각 필드(연도·월·일·시·분)를 요구합니다. Instant 자체엔 그 필드가 없습니다. 실제로 부딪혀 봤습니다.

import java.time.Instant; import java.time.ZoneId; import java.time.format.DateTimeFormatter; public class InstantNoZone { public static void main(String[] args) { Instant instant = Instant.parse("2024-03-15T09:15:00Z"); DateTimeFormatter localPattern = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); try { System.out.println(localPattern.format(instant)); } catch (Exception e) { System.out.println("localPattern.format(instant) 실패: " + e.getClass().getName() + " - " + e.getMessage()); } DateTimeFormatter withZone = localPattern.withZone(ZoneId.of("Asia/Seoul")); System.out.println("withZone(Asia/Seoul).format(instant) = " + withZone.format(instant)); } }
localPattern.format(instant) 실패: java.time.temporal.UnsupportedTemporalTypeException - Unsupported field: YearOfEra withZone(Asia/Seoul).format(instant) = 2024-03-15 18:15:00

zone 없는 지역 시각 패턴을 Instant에 그대로 쓰면 예외를 던집니다. 이건 사실 반가운 실패입니다 — 배포 전에 걸립니다. withZone(ZoneId.of("Asia/Seoul"))을 붙이면 그 zone의 규칙을 적용해 지역 시각을 계산한 뒤 포맷합니다. 9장의 Instant.atZone()이 항상 성공하는 함수였던 것과 같은 이유로, withZone()이 붙은 포맷터도 Instant에 대해 항상 성공합니다.

문제는 이 실패가 항상 일어나지는 않는다는 데 있습니다. ZonedDateTime처럼 이미 지역 시각 필드를 갖고 있는 타입을 zone 없는 패턴으로 포맷하면, 예외 없이 조용히 성공합니다. 다만 그 문자열에서 zone 정보는 사라집니다.

import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; public class SilentZoneLoss { public static void main(String[] args) { Instant instant = Instant.parse("2024-03-15T09:15:00Z"); ZonedDateTime seoul = instant.atZone(ZoneId.of("Asia/Seoul")); ZonedDateTime utc = instant.atZone(ZoneId.of("UTC")); DateTimeFormatter noZoneField = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); System.out.println("seoul.format(noZoneField) = " + noZoneField.format(seoul)); System.out.println("utc.format(noZoneField) = " + noZoneField.format(utc)); System.out.println("두 ZonedDateTime이 같은 순간인가? = " + seoul.toInstant().equals(utc.toInstant())); } }
seoul.format(noZoneField) = 2024-03-15 18:15:00 utc.format(noZoneField) = 2024-03-15 09:15:00 두 ZonedDateTime이 같은 순간인가? = true

같은 순간을 가리키는 두 값인데도, 패턴이 zone 필드를 요구하지 않으니 결과 문자열만 보면 완전히 다른 두 시각처럼 보입니다. 어느 zone에서 찍었는지 이 문자열 안엔 아무 흔적도 없습니다. 로그 두 줄이 서로 다른 서버에서, 서로 다른 zone으로 이 값을 찍었다면, 읽는 사람은 시각이 9시간이나 어긋난 별개의 사건이라고 오인하기 쉽습니다. 이게 이 장 첫머리에서 말한 “예외 없이 틀린 텍스트만 남는” 정확한 사례입니다. Instant가 관여하는 패턴엔 항상 withZone()을 붙이고, 사람이 읽을 로그·API 응답엔 오프셋이나 zone 정보를 문자열 자체에 남기는 ISO_OFFSET_DATE_TIME·ISO_ZONED_DATE_TIME(7장)을 쓰는 편이 안전합니다.

함정 ③ 패턴 문자 — 대소문자 하나가 다른 필드를 가리킨다

DateTimeFormatter의 패턴 문자는 대소문자로 완전히 다른 필드를 가리킵니다. 자바독과 실제 동작을 코드로 맞춰봤습니다.

import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Locale; public class LetterCheck { public static void main(String[] args) { LocalDateTime dt = LocalDateTime.of(2024, 2, 9, 15, 7, 30); // 연중 40번째 날 DateTimeFormatter f = DateTimeFormatter.ofPattern( "'mm(분)='mm' MM(월)='MM' HH(0-23)='HH' hh(1-12,AM/PM 필요)='hh a' dd(일)='dd' DD(연중 몇째 날)='DD", Locale.US); System.out.println(dt.format(f)); } }
mm(분)=07 MM(월)=02 HH(0-23)=15 hh(1-12,AM/PM 필요)=03 PM dd(일)=09 DD(연중 몇째 날)=40

mm은 분, MM은 월입니다. HH는 023시, hh는 112시라 a(AM/PM) 없이는 오전·오후를 구분할 방법이 없습니다. dd는 그 달의 며칠째, DD는 그 해의 며칠째입니다. 이 중 실무에서 가장 조용히 사고를 내는 조합이 hh입니다. a 표기를 빼먹으면 무슨 일이 벌어지는지 확인했습니다.

import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Locale; public class HHTrap { public static void main(String[] args) { LocalDateTime morning = LocalDateTime.of(2024, 2, 9, 3, 7, 0); LocalDateTime afternoon = LocalDateTime.of(2024, 2, 9, 15, 7, 0); DateTimeFormatter noAmPm = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss", Locale.US); System.out.println("오전 3시07분 = " + morning.format(noAmPm)); System.out.println("오후 3시07분 = " + afternoon.format(noAmPm)); System.out.println("두 문자열이 같은가? = " + morning.format(noAmPm).equals(afternoon.format(noAmPm))); } }
오전 3시07분 = 2024-02-09 03:07:00 오후 3시07분 = 2024-02-09 03:07:00 두 문자열이 같은가? = true

예외는 없습니다. 컴파일도, 실행도, 저장도 다 됩니다. 그런데 새벽 3시와 오후 3시가 완전히 같은 문자열로 찍힙니다. a를 빠뜨린 hh 패턴은 12시간의 정보를 조용히 지워버립니다. 이 문자열을 다시 파싱하면 항상 오전으로 해석되고, 오후에 벌어진 이벤트는 전부 새벽으로 뒤바뀐 채 기록됩니다. 리뷰에서 걸러내기도 어렵습니다 — 코드만 봐서는 hh가 틀렸는지 알 수 없고, 실제로 오후 시각이 입력될 때까지 드러나지 않습니다.

yyyyYYYY의 차이는 더 미묘합니다. yyyy는 연호 기준 연도(year-of-era), YYYY는 주 기준 연도(week-based-year)입니다. 평소엔 같은 값을 내다가, 연말연시 근처 며칠에서만 갈립니다. 실제로 갈리는 날짜를 찾아봤습니다.

import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.Locale; public class YearPatternTrap { public static void main(String[] args) { DateTimeFormatter calendarYear = DateTimeFormatter.ofPattern("yyyy-MM-dd", Locale.US); DateTimeFormatter weekBasedYear = DateTimeFormatter.ofPattern("YYYY-MM-dd", Locale.US); LocalDate[] days = { LocalDate.of(2024, 12, 29), // 일요일 LocalDate.of(2024, 12, 30), // 월요일 LocalDate.of(2024, 12, 31), // 화요일 LocalDate.of(2025, 1, 1), // 수요일 }; System.out.println("포맷에 사용한 로케일 = " + Locale.US); for (LocalDate d : days) { System.out.println(d + " yyyy=" + d.format(calendarYear) + " YYYY=" + d.format(weekBasedYear)); } } }
포맷에 사용한 로케일 = en_US 2024-12-29 yyyy=2024-12-29 YYYY=2025-12-29 2024-12-30 yyyy=2024-12-30 YYYY=2025-12-30 2024-12-31 yyyy=2024-12-31 YYYY=2025-12-31 2025-01-01 yyyy=2025-01-01 YYYY=2025-01-01

어긋나는 날짜 자체가 로케일에 달려 있습니다. 위 결과는 en_US·ko_KR처럼 일요일 시작·최소 1일 규칙을 쓰는 로케일 기준입니다. Locale.GERMANY처럼 ISO 방식 (월요일 시작·최소 4일)을 쓰는 로케일로 같은 코드를 돌리면 12월 29일은 갈리지 않고 30~31일만 갈립니다. “연말 며칠”이라는 감각은 맞지만, 정확히 어느 날인지는 로케일마다 다릅니다.

12월 2931일 사흘 동안 yyyy는 여전히 2024를 가리키는데 YYYY는 이미 2025입니다. 이유는 주(週) 계산 규칙에 있습니다. Y는 그 날짜가 속한 가 몇 년도 소속인지를 봅니다. 확인해 보니 Locale.USLocale.KOREA 둘 다 firstDayOfWeek=SUNDAY, minimalDaysInFirstWeek=1입니다(java.time.temporal.WeekFields.of(locale)로 실측). 그러니까 새해 첫날을 단 하루만 포함해도 그 주 전체를 “새해의 첫 주”로 칩니다. 2024-12-29는 일요일이라 그날부터 시작하는 주(12/291/4)가 이미 2025년의 첫 주고, 그래서 YYYY가 2025를 가리킵니다. yyyy는 이런 주 계산과 무관하게 그냥 그 날짜가 달력상 몇 년도인지만 봅니다.

여기서 한 겹 더 있습니다. Y가 따르는 “week-based-year”는 로케일의 WeekFields를 따르는 것이지, ISO 8601이 정의하는 week-based-year(java.time.temporal.IsoFields.WEEK_BASED_YEAR, firstDayOfWeek=MONDAY·minimalDaysInFirstWeek=4)와도 다릅니다. 같은 2024-12-29를 IsoFields.WEEK_BASED_YEAR로 읽으면 2024가 나옵니다(실측 확인) — Y 패턴이 낸 2025와 정반대입니다. “주 기준 연도” 안에서도 어느 규칙을 따르는지에 따라 답이 갈리는 셈입니다. 실무에서 가장 안전한 선택은 명확합니다. 달력 연도가 필요하면 yyyy 또는 uuuu(연호 없는 연도, 자바독 권장)를 씁니다. ISO 8601의 주 번호 표기(2024-W15-3, 7장)처럼 정말 주 기준 연도가 필요한 게 아니라면 YYYY를 쓸 이유가 없습니다.

핵심 — 포맷터가 지키는 계약과, 개발자가 지켜야 할 계약

세 함정을 하나로 묶으면 이렇게 정리됩니다. DateTimeFormatter가 스스로 보장하는 건 불변성과 스레드 안전성뿐입니다. 로케일과 zone은 호출한 쪽이 명시하지 않으면 실행 환경이 대신 정합니다. 그리고 실행 환경은 개발 머신과 배포 서버가 다르고, 서버끼리도 다를 수 있습니다.

함정증상안전장치
로케일 미지정월·요일 이름이 실행 환경마다 다르게 찍힘ofPattern(pattern, Locale)로 항상 명시, API 응답엔 로케일-불변 상수 사용
Instant + zone 없는 지역 시각 패턴UnsupportedTemporalTypeException(즉시 실패라 그나마 안전)withZone(ZoneId)
ZonedDateTime + zone 필드 없는 패턴예외 없이 zone 정보만 조용히 증발패턴에 VV(zone id)·XXX(오프셋) 포함, 또는 ISO_ZONED_DATE_TIME
yyyy vs YYYY연말연시 며칠간 연도가 1 어긋남달력 연도는 yyyy/uuuu, 주 기준 연도가 필요할 때만 YYYY
hh (a 없음)오전/오후가 같은 문자열로 찍힘, 파싱하면 항상 오전으로 복원HH 사용, hh를 쓸 거면 반드시 a 동반
mm/MM, dd/DD분/월, 일/연중일 혼동패턴 작성 후 실제 값으로 한 번 찍어 눈으로 확인

패턴 문자 표는 자바독 기준으로 다시 한번 못 박습니다. y는 연호 기준 연도, Y는 주 기준 연도, M은 월, m은 분, H는 023시, h는 112시(AM/PM 필요), d는 그 달의 날짜, D는 그 해의 날짜. 이 표를 외우는 것보다 중요한 습관은, 새 패턴을 쓸 때마다 실제 값을 한 번 찍어 눈으로 확인하는 것입니다. 컴파일러도 테스트도 hhHH여야 했는지는 알려주지 않습니다.

정리

  • DateTimeFormatter는 불변이고 스레드 안전하다. 8장의 SimpleDateFormat static 공유 사고(10,000회 중 오류 4,873·예외 4,045)와 정반대로, 같은 조건에서 오류 0·예외 0을 실측했다. 스레드 안전성은 더 이상 걱정할 대상이 아니다.
  • 로케일을 지정하지 않으면 JVM 기본 로케일이 조용히 개입한다. 같은 패턴 객체가 실행 환경에 따라 다른 문자열을 낸다(실측: en_US 기본 로케일에서 withLocale(KOREAN)과 미지정 결과가 다르게 나옴). 숫자만 있는 패턴은 대개 안전하지만, 이름(월·요일)이 들어가는 순간 로케일은 필수 인자다.
  • Instant는 지역 시각 필드가 없다. zone 없는 지역 시각 패턴에 Instant를 넣으면 UnsupportedTemporalTypeException을 던진다(실측). withZone(ZoneId)를 붙이면 해결된다. 반대로 ZonedDateTime을 zone 없는 패턴으로 찍으면 예외 없이 zone 정보만 조용히 사라진다(실측 — 같은 순간이 다른 문자열로 찍힘).
  • 패턴 문자는 대소문자로 완전히 다른 필드다. mmMM, HHhh, ddDD가 특히 흔한 혼동이다. hha를 빠뜨리면 오전·오후가 같은 문자열로 찍히는 걸 실측으로 확인했다 — 예외 없는 정보 손실의 대표 사례다.
  • yyyyYYYY는 연말연시 며칠에서만 갈린다. 실측: en_US·ko_KR 모두 firstDayOfWeek=SUNDAY·minimalDaysInFirstWeek=1이라, 2024-12-29~31이 yyyy로는 2024지만 YYYY로는 2025다. ISO의 IsoFields.WEEK_BASED_YEAR(월요일 시작, 최소 4일)로 같은 날짜를 읽으면 또 다른 값(2024)이 나온다 — “주 기준 연도” 안에서도 규칙이 갈린다. 달력 연도가 필요하면 yyyy/uuuu를 쓴다.
  • 제3부를 관통한 흐름을 다시 보면 — 8장은 레거시 API(Date·Calendar·SimpleDateFormat)가 개념을 나눌 이름조차 갖지 못해 무너진 자리를 보여줬고, 9장은 InstantLocalDateTime으로 “언제”라는 개념을 둘로 쪼갰고, 10장은 그 사이를 오프셋·zone까지 품은 타입으로 이었고, 11장은 “언제”와 “얼마나”가 서로 다른 시계를 요구한다는 걸 보여줬다. 12장은 그 모든 정확한 값이 마지막으로 통과하는 문 — 텍스트 경계에서, 예외 없이도 틀릴 수 있는 자리들을 짚었다.

생각해볼 질문: 여기까지 다진 모든 규칙은 메모리 안, 그러니까 JVM 안에서 유효합니다. InstantZonedDateTime이든, 문자열로 올바르게 찍었든, 결국 이 값은 애플리케이션이 재시작되면 사라집니다. 실무 시스템은 이 값을 데이터베이스 컬럼에 넣어야 살아남습니다. 그런데 MySQL은 DATETIMETIMESTAMP라는, 이름은 비슷해도 완전히 다르게 동작하는 두 개의 시간 타입을 갖고 있습니다. 자바 쪽에서 그토록 애써 구분한 “순간”과 “지역 시각”이라는 개념은, 이 두 컬럼 타입 중 어느 쪽에 어떻게 대응할까요?

13장 · DATETIME vs TIMESTAMP — MySQL은 시간을 어떻게 저장하나

Last updated on