์—ฌ๋Ÿฌ ๊ตญ๊ฐ€๋ฅผ ๋Œ€์ƒ์œผ๋กœ ํ•ด์•ผํ•˜๋Š” ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ค์–ด๊ฐ€๊ฒŒ ๋˜๋Š” ๊ฒฝ์šฐ ์–ธ์–ด ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์‹œ๊ฐ„์„ ๋‹ค๋ฃจ๋Š” ๊ฒƒ๋„ ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค. ๋‹จ์ˆœํ•˜๊ฒŒ ๋งํ•ด์„œ ํ•œ๊ตญ์—์„œ ์„œ๋น„์Šค๋ฅผ ์ œ๊ณตํ•œ๋‹ค๊ณ ํ•ด์„œ ํ•œ๊ตญ ์‹œ๊ฐ„์œผ๋กœ ๋ชจ๋“  ์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค๋ฃจ๋Š” ๊ฒƒ์€ ์ข‹์€ ๋ฐฉ์‹์ด ์•„๋‹™๋‹ˆ๋‹ค. ๊ธฐ๋ณธ์ ์œผ๋กœ๋Š” UTC๋ผ๊ณ  ํ•˜๋Š” ์„ธ๊ณ„ ํ˜‘์ • ์‹œ๊ฐ„์„ ๊ธฐ์ค€์œผ๋กœ ์‹œ๊ฐ„์„ ์ €์žฅํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค. PostgreSQL๊ณผ ๊ฐ™์€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ๋„ Timestamp๋ฅผ ์ €์žฅํ•˜๊ฒŒ ๋˜๋Š” ๊ฒฝ์šฐ ๋‚ด๋ถ€์ ์œผ๋กœ๋Š” UTC๋กœ ์ €์žฅ๋ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ, 2022๋…„ 3์›” 19์ผ 12์‹œ ๋ผ๋Š” ์‹œ๊ฐ„์€ 2022๋…„ 3์›” 19์ผ 03์‹œ๋กœ ์ €์žฅ๋˜๋Š”๊ฒƒ์ด์ฃ .

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์„œ๋ฒ„ ๊ทธ๋ฆฌ๊ณ  ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ชจ๋‘ UTC๋กœ ์„ค์ •ํ•˜์—ฌ ์‚ฌ์šฉํ•˜๋Š”๊ฒŒ ์ผ๋ฐ˜์ ์ž…๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์•„๋งˆ์กด ์›น ์„œ๋น„์Šค์˜ EC2๋Š” ํ•œ๊ตญ ๋ฆฌ์ „์ธ๋ฐ๋„ ๋ถˆ๊ตฌํ•˜๊ณ  ํ•œ๊ตญ ์‹œ๊ฐ„๋Œ€๊ฐ€ ์•„๋‹Œ UTC๋กœ ์„ค์ •๋˜์–ด์žˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  ์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ๋ฅผ ์ „๋‹ฌํ•  ๋•Œ๋Š” 2022-03-19 12:00๊ณผ ๊ฐ™์€ ๋ฌธ์ž์—ด ํ˜•ํƒœ๊ฐ€ ์•„๋‹Œ Unix Timestamp์˜ ๋ฐ€๋ฆฌ์ดˆ๊ฐ€ ๋ถ€์—ฌ๋œ ์ƒํƒœ๋กœ ์„œ๋ฒ„์™€ ํด๋ผ์ด์–ธํŠธ๊ฐ„ ์š”์ฒญ๊ณผ ์‘๋‹ต์— ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. ๊ฐ€๋”์”ฉ ๊ตญ๋‚ด ์‹œ๊ฐ„๋งŒ ๋‹ค๋ฃจ๋˜ ๊ฐœ๋ฐœ์ž๋“ค์ด UTC ๋˜๋Š” Unix Timestamp์— ๋Œ€ํ•ด์„œ ๋ชจ๋ฅด๋Š” ๊ฒฝ์šฐ๋„ ๊ฝค ๋งŽ์•˜์Šต๋‹ˆ๋‹ค. ๋ฐ€๋ฆฌ์ดˆ ํ˜•์‹์˜ Unix Timestamp๋ฅผ ์ „๋‹ฌ๋ฐ›์•˜์œผ๋‚˜ ์‹ค์ œ๋กœ ๋ณ€ํ™˜ํ•ด๋ณด๋‹ˆ 9์‹œ๊ฐ„์ด ๋น ์ง„๊ฒŒ ์•„๋‹ˆ๋ผ ์˜คํžˆ๋ ค 9์‹œ๊ฐ„์ด ๋”ํ•ด์ง„ ์ƒํƒœ๋กœ ์ „๋‹ฌํ•˜๋˜ ๊ฒฝํ—˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

Java Date Format

์„œ๋ฒ„์™€ ํด๋ผ์ด์–ธํŠธ ๊ฐ„์— ์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ๋ฅผ ์ „๋‹ฌํ•˜๊ฑฐ๋‚˜ ๊ฐœ๋ฐœ์ž ์‚ฌ์ด์— API๋ฅผ ์ œ๊ณตํ•  ๋•Œ์—๋Š” ์•ž์„œ ์ด์•ผ๊ธฐํ•œ Unix Timestamp๋กœ ์ „๋‹ฌํ•˜๋„๋ก ์ŠคํŽ™์„ ๋งž์ถœ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ๊ฐœ๋ฐœ์ž๊ฐ€ ์•„๋‹Œ ์ผ๋ฐ˜ ์‚ฌ์šฉ์ž๊ฐ€ ์„œ๋น„์Šค ๋‚ด์—์„œ CSV ๋˜๋Š” ์—‘์…€ ํŒŒ์ผ ํ˜•์‹์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์—…๋กœ๋“œํ•˜๊ฑฐ๋‚˜ ๋‹ค์šด๋กœ๋“œ ๋ฐ›์„๋•Œ์—๋Š” ์ˆซ์ž๊ฐ’์ธ Unix Timestamp๊ฐ€ ์•„๋‹Œ 2022-03-19 12:00:00๊ณผ ๊ฐ™์€ ์–ด๋– ํ•œ ๋ฌธ์ž ํ˜•ํƒœ์ด์–ด์•ผํ•ฉ๋‹ˆ๋‹ค. ์™œ๋ƒํ•˜๋ฉด ์ผ๋ฐ˜์ธ๋“ค์€ UTC๊ฐ€ ๋ฌด์—‡์ธ์ง€ Unix Timestamp๊ฐ€ ๋ฌด์—‡์ธ์ง€ ๋ชจ๋ฅด๋Š” ์‚ฌ๋žŒ์ด ๋งŽ์œผ๋‹ˆ๊นŒ์š”. ์‹ฌ์ง€์–ด๋Š” ๊ฐœ๋ฐœ์ž๋“ค ์ค‘์—์„œ๋„โ€ฆ

์ž๋ฐ” 8์˜ Date/Time API์— ๋Œ€ํ•ด์„œ๋Š” ๋„ค์ด๋ฒ„ D2์— ๊ณต์œ ๋œ Java์˜ ๋‚ ์งœ์™€ ์‹œ๊ฐ„ API๋ฅผ ์ฐธ๊ณ ํ•˜์‹œ๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

DateTimeFormatter

์ž๋ฐ” 8์˜ ๋‚ ์งœ ๋ฐ ์‹œ๊ฐ„ ํด๋ž˜์Šค์— ๋Œ€ํ•ด์„œ ๋‚ ์งœ ํฌ๋งท์„ ์ ์šฉํ•ด์•ผํ•˜๋Š” ๊ฒฝ์šฐ DateTimeFormatter๋ฅผ ์‚ฌ์šฉํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ผ๋ฐ˜์ ์œผ๋กœ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ฝ”๋“œ๋กœ ๋‚ ์งœ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ€ํ™˜ํ•  ์ˆ˜ ์žˆ์ฃ .

TimeZone tzSeoul = TimeZone.getTimeZone("Asia/Seoul");
String dateStr = "2022-03-19 12:00:00";
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(tzSeoul.toZoneId());
ZonedDateTime dateTime = ZonedDateTime.parse(dateStr, dateTimeFormatter);
System.out.printf("%s -> %s%n", dateStr, dateTime);

์œ„ ์ฝ”๋“œ๋Š” ์ดํ•ดํ•˜๊ธฐ์—๋Š” ์‰ฌ์šฐ๋‚˜ ํ•œ๊ฐ€์ง€ ๋ฌธ์ œ์ ์€ ์–ด๋–ค ํŠน์ •ํ•œ ํ˜•ํƒœ์˜ ๋ฌธ์ž์—ด๋กœ ๊ตฌ์„ฑ๋œ ๋ฐ์ดํ„ฐ๋งŒ ๋ณ€ํ™˜ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์ ์ž…๋‹ˆ๋‹ค.

๋งŒ์•ฝ, ์„œ๋น„์Šค ์‚ฌ์šฉ์ž๊ฐ€ 2022๋…„ 3์›” 19์ผ 0์‹œ์˜ ์‹œ๊ฐ„์„ ํ‘œํ˜„ํ•˜๊ณ ์ž ํ•œ๋‹ค๋ฉด 2022-03-19 00:00:00๊ณผ ๊ฐ™์ด ๋ถˆํ•„์š”ํ•˜๊ฒŒ 00:00:00์„ ๋ถ™์—ฌ์•ผํ•˜๋Š” ๋‹จ์ ์ด ์ƒ๊ธฐ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ๋ณด์™„ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ์—ฌ๋Ÿฌ๊ฐ€์ง€ ํ˜•์‹์˜ ๋‚ ์งœ ํฌ๋งท์„ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ์ฝ”๋“œ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

DateTimeFormatterBuilder

์—ฌ๋Ÿฌ๊ฐ€์ง€ ํ˜•ํƒœ์˜ ๋‚ ์งœ ํ˜•์‹์„ ์ง€์›ํ•ด์•ผํ•˜๋Š” ๊ฒฝ์šฐ์—๋Š” Optional ํŒจํ„ด์„ ์ ์šฉํ•ด์„œ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. DateTimeFormatter์— ๊ทธ๋Œ€๋กœ ์ ์šฉํ• ์ˆ˜๋„ ์žˆ์œผ๋‚˜ DateTimeFormatterBuilder๋ฅผ ํ†ตํ•ด ์—ฌ๋Ÿฌ๊ฐ€์ง€ ์ฒ˜๋ฆฌ ๋ฐฉ์‹์— ๋Œ€ํ•œ ์˜ต์…˜์ด ์ ์šฉ๋œ ํ˜•ํƒœ๋กœ DateTimeFormatter๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

TimeZone tzSeoul = TimeZone.getTimeZone("Asia/Seoul");
DateTimeFormatter dateTimeFormatter = new DateTimeFormatterBuilder()
        .appendPattern("[yyyy-MM-dd HH:mm:ss]")
        .appendPattern("[yyyy-MM-dd HH:mm]")
        .appendPattern("[yyyy-MM-dd]")
        .parseDefaulting(ChronoField.HOUR_OF_DAY, 0)
        .parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0)
        .parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0)
        .toFormatter()
        .withZone(tzSeoul.toZoneId());

String[] dateStrArr = new String[]{"2022-03-19 00:00:00", "2022-03-19 00:00", "2022-03-19"};

for (String dateStr : dateStrArr) {
    ZonedDateTime dateTime = ZonedDateTime.parse(dateStr, dateTimeFormatter);
    System.out.printf("%s -> %s%n", dateStr, dateTime);
}

// 2022-03-19 00:00:00 -> 2022-03-19T00:00+09:00[Asia/Seoul]
// 2022-03-19 00:00 -> 2022-03-19T00:00+09:00[Asia/Seoul]
// 2022-03-19 -> 2022-03-19T00:00+09:00[Asia/Seoul]

Unable to obtain LocalTime from TemporalAccessor

TimeZone tzSeoul = TimeZone.getTimeZone("Asia/Seoul");
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("[yyyy-MM-dd HH:mm:ss][yyyy-MM-dd HH:mm][yyyy-MM-dd]").withZone(tzSeoul.toZoneId());

String[] dateStrArr = new String[]{"2022-03-19 00:00:00", "2022-03-19 00:00", "2022-03-19"};

for (String dateStr : dateStrArr) {
    ZonedDateTime dateTime = ZonedDateTime.parse(dateStr, dateTimeFormatter);
    System.out.printf("%s -> %s%n", dateStr, dateTime);
}

์œ„์™€ ๊ฐ™์ด DateTimeFormatterBuilder๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ ์„œ๋„ DateTimeFormatter ํŒจํ„ด ์ž์ฒด๋กœ ๋‚ ์งœ ํฌ๋งท ์ฒด์ด๋‹์„ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜, 2022-03-19์™€ ๊ฐ™์ด ์‹œ๊ฐ„ ๋ถ€๋ถ„์ด ์—†๋Š” ํ˜•ํƒœ์˜ ๊ฒฝ์šฐ์—๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์—†๋‹ค๋Š” ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

java.time.format.DateTimeParseException: Text '2022-03-19' could not be parsed: Unable to obtain ZonedDateTime from TemporalAccessor: {},ISO,Asia/Seoul resolved to 2022-03-19 of type java.time.format.Parsed
	at java.base/java.time.format.DateTimeFormatter.createError(DateTimeFormatter.java:2017)
	at java.base/java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1952)
	at java.base/java.time.ZonedDateTime.parse(ZonedDateTime.java:598)
	at test.Main.main(Main.java:17)
Caused by: java.time.DateTimeException: Unable to obtain ZonedDateTime from TemporalAccessor: {},ISO,Asia/Seoul resolved to 2022-03-19 of type java.time.format.Parsed
	at java.base/java.time.ZonedDateTime.from(ZonedDateTime.java:566)
Caused by: java.time.DateTimeException: Unable to obtain ZonedDateTime from TemporalAccessor: {},ISO,Asia/Seoul resolved to 2022-03-19 of type java.time.format.Parsed

	at java.base/java.time.format.Parsed.query(Parsed.java:235)
	at java.base/java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1948)
	... 2 more
Caused by: java.time.DateTimeException: Unable to obtain LocalTime from TemporalAccessor: {},ISO,Asia/Seoul resolved to 2022-03-19 of type java.time.format.Parsed
Caused by: java.time.DateTimeException: Unable to obtain LocalTime from TemporalAccessor: {},ISO,Asia/Seoul resolved to 2022-03-19 of type java.time.format.Parsed

	at java.base/java.time.LocalTime.from(LocalTime.java:431)
	at java.base/java.time.ZonedDateTime.from(ZonedDateTime.java:561)
	... 4 more

๋”ฐ๋ผ์„œ, DateTimeFormatterBuilder๋กœ ๋ฌธ์ž์—ด๋กœ ๊ตฌ์„ฑ๋œ ๋‚ ์งœ ๋ฐ ์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ์— ๊ตฌ์„ฑ๋˜์ง€ ์•Š๋Š” ๋ถ€๋ถ„์„ ์–ด๋–ป๊ฒŒ ์ฒ˜๋ฆฌํ•  ๊ฒƒ์ธ๊ฐ€์— ๋Œ€ํ•œ ์˜ต์…˜์„ ์ ์šฉํ•˜์—ฌ DateTimeFormatter๋ฅผ ๋งŒ๋“ค์–ด์„œ ์‚ฌ์šฉํ•˜๋Š”๊ฒŒ ์ข‹์•„๋ณด์ž…๋‹ˆ๋‹ค.

๋งŒ์•ฝ, ์ด ๊ธ€์„ ๋ณด์‹œ๋Š” ์—ฌ๋Ÿฌ๋ถ„๋“ค์ด ์—ฌ๋Ÿฌ๊ฐ€์ง€ ํ˜•ํƒœ์˜ ๋‚ ์งœ ํฌ๋งท์„ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด์„œ ์•„๋ž˜์™€ ๊ฐ™์ด DateTimeFormattter๋ฅผ ์—ฌ๋Ÿฌ๊ฐœ ๋งŒ๋“ค์–ด์„œ ์ฒ˜๋ฆฌํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ–ˆ๋‹ค๋ฉด DateTimeFormatterBuilder๋กœ ํ•˜๋‚˜์˜ DateTimeFormatter๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์ข€ ๋” ๊น”๋”ํ•œ ํ˜•ํƒœ์˜ ์ฝ”๋“œ๋กœ ๋งŒ๋“ค์–ด๋ณด์‹œ๊ธฐ๋ฅผ ์ถ”์ฒœํ•ด๋“œ๋ฆฝ๋‹ˆ๋‹ค.

String dateStr = "2022-03-19";
ZonedDateTime dateTime = null;
try {
    dateTime = ZonedDateTime.parse(dateStr, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm").withZone(tzSeoul.toZoneId()));
} catch (DateTimeException e) {
    LocalDateTime localDateTime = LocalDate.parse(dateStr, DateTimeFormatter.ofPattern("yyyy-MM-dd").withZone(tzSeoul.toZoneId())).atStartOfDay();
    dateTime = ZonedDateTime.of(localDateTime, tzSeoul.toZoneId());
}
System.out.println(dateTime);

๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค.