자바

java time convert (시간 변환)

알쓸개잡 2023. 9. 17.

애플리케이션 개발을 하다 보면 종종 문자열 타입의 시간 정보를 자바의 LocalDateTime 혹은 LocalDate, LocalTime과 같은 인스턴스로 변환을 하거나 역으로 원하는 시간 형식으로 문자열로 변환을 해야 하는 경우가 있다. 또한 지역 시간(timezone)에 맞도록 시간을 변경해야 하는 경우도 있을 것이다. 이번 포스팅에서는 자바에서 시간 변환과 관련된 내용을 기록한다.

 

기본으로 제공되는 시간 관련 클래스

java에서는 기본적으로 제공하는 몇 가지 시간 관련 클래스가 있다. 해당 클래스의 목록은 아래와 같다.

클래스 now() 호출 형식 기본 timezone
LocalDateTime 2023-09-17T13:22:51.003196 시스템에 적용된 timezone의 날짜와 시간 표시
LocalDate 2023-09-17 시스템에 적용된 timezone의 날짜 표시
LocalTime 13:26:02.674767 시스템에 적용된 timezone의 시간 표시
OffsetDateTime 2023-09-17T13:26:42.987772+09:00 시스템에 적용된 timezone을 UTC 기준으로 timezone offset 표시
(날짜 + 시간 + timezone offset)
OffsetTime 13:29:37.716176+09:00 시스템에 적용된 timezone을 UTC 기준으로 timezone offset 표시 (시간 + timezon offset)
ZonedDateTime 2023-09-17T13:30:21.927436+09:00[Asia/Seoul] 시스템에 적용된 timezone을 UTC 기준으로 timezone offset과 지역 정보 표시
(날짜 + 시간 + timezone offset + 지역)
Instant 2023-09-17T05:00:09.049776Z UTC timezone (날짜 + 시간 )
Z는 UTC를 표현하는 suffix 정보

클래스의 이름에서 알 수 있듯이 XXXLocalTime은 날짜와 시간을 모두 다루고 XXXDate는 날짜만, XXXTime은 시간만 다룬다.

 

DateTimeFormatter

DateTimeFormatter는 java 시간 관련 인스턴스를 출력하고자 하는 시간 포맷으로 변환할 수 있도록 지원하거나 String 타입의 시간 정보 형식을 파싱 하기 위해서 제공되는 시간 형식 분석기이다.

 

몇 가지 대표 심볼

시간 정보 파싱을 위한 몇 가지 대표 형식은 아래와 같다. 더 자세한 형식은 DateTimeFormatter 클래스의 주석을 확인해 보면 된다.

symbol meaning example
y year-of-era (서기년도) 2004; 04
M month-of-year 7; 07
D day-of-year 189
d day-of-month 09
H hour-of-day (0-23) 14
m minute-of-hour 30
s second-of-minute 55
S fraction-of-second 978
n nano-of-second 987654321
V time-zone ID Asia/Seoul
v generic time-zone name Pacific Time; PT
z time-zone name Pacific Standard Time; PST
O localized zone-offset GMT+8; UTC-08:00; GMT+08:00
X zone-offset ('Z' for zero) Z;-08; -0830;-08:30;-083015;-08:30:15
x zone-offset +0000; -08; -0830; -08:30; -083015; -08:30:15
Z zone-offset +0000; -0800; -08:00

DateTimeFormatter example

String localDateTimeStr = "2023-09-17 14:08:23";
//datetime foramt
//y-M-d H:m:s 와 yyyy-MM-dd HH:mm:ss 는 동일하다
DateTimeFormatter dateTimeFormatter1 = 
	DateTimeFormatter.ofPattern("y-M-d H:m:s");
DateTimeFormatter dateTimeFormatter2 = 
	DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime localDateTime1 = LocalDateTime.parse(localDateTimeStr, dateTimeFormatter1);
LocalDateTime localDateTime2 = LocalDateTime.parse(localDateTimeStr, dateTimeFormatter2);
System.out.println("instance time1: " + localDateTime1);
System.out.println("instance time2: " + localDateTime2);
-----------------------------------------------------------------
결과
instance time1: 2023-09-17T14:08:23
instance time2: 2023-09-17T14:08:23

 

DateTimeFormatter에서 제공하는 기본 형식

DateTimeFormatter클래스에서 기본적으로 몇가지 시간 형식을 미리 정의하여 제공한다.

DateTimeFormatter클래스에서 기본으로 제공하는 시간 형식

위 시간 형식들은 static으로 미리 정의되어 있는 시간 형식들이다. 아래 코드는 ISO_LOCAL_DATE_TIME형식의 코드이다.

public static final DateTimeFormatter ISO_LOCAL_DATE_TIME;
static {
    ISO_LOCAL_DATE_TIME = new DateTimeFormatterBuilder()
            .parseCaseInsensitive()
            .append(ISO_LOCAL_DATE)
            .appendLiteral('T')
            .append(ISO_LOCAL_TIME)
            .toFormatter(ResolverStyle.STRICT, IsoChronology.INSTANCE);
}
...
public static final DateTimeFormatter ISO_LOCAL_DATE;
static {
    ISO_LOCAL_DATE = new DateTimeFormatterBuilder()
            .appendValue(YEAR, 4, 10, SignStyle.EXCEEDS_PAD)
            .appendLiteral('-')
            .appendValue(MONTH_OF_YEAR, 2)
            .appendLiteral('-')
            .appendValue(DAY_OF_MONTH, 2)
            .toFormatter(ResolverStyle.STRICT, IsoChronology.INSTANCE);
}
...
public static final DateTimeFormatter ISO_LOCAL_TIME;
static {
    ISO_LOCAL_TIME = new DateTimeFormatterBuilder()
            .appendValue(HOUR_OF_DAY, 2)
            .appendLiteral(':')
            .appendValue(MINUTE_OF_HOUR, 2)
            .optionalStart()
            .appendLiteral(':')
            .appendValue(SECOND_OF_MINUTE, 2)
            .optionalStart()
            .appendFraction(NANO_OF_SECOND, 0, 9, true)
            .toFormatter(ResolverStyle.STRICT, null);
}

ISO_LOCAL_DATE_TIME은 ISO_LOCAL_DATE와 ISO_LOCAL_TIME을 조합하여 시간 형식을 미리 정의하는 것을 알 수 있다.

ISO_LOCAL_TIME에 정의된 형식을 보면. optionalStart()가 호출되는 부분을 볼 수 있는데 .optionalStart()가 호출된 다음 형식은 생략이 가능하다는 의미로 해석하면 된다. 즉 SECOND_OF_MINUTE(초), NANO_OF_SECOND(나노초) 형식은 생략 가능하다는 것이다.

ChronoField enum 클래스에 정의된 몇 가지 타입들을 살펴보면 아래와 같다.

MILLI_OF_SECOND("MilliOfSecond", MILLIS, SECONDS, ValueRange.of(0, 999)),
...
SECOND_OF_MINUTE("SecondOfMinute", SECONDS, MINUTES, ValueRange.of(0, 59), "second"),
...
MINUTE_OF_HOUR("MinuteOfHour", MINUTES, HOURS, ValueRange.of(0, 59), "minute"),

타입 이름에서 알 수 있듯이 MILLI_OF_SECOND는 0 - 999 범위의 밀리초 형식을 의미하고 SECOND_OF_MINUTE는 0 - 59의 초 형식을 의미한다. 이 처럼 각 정의된 필드를 따라가 보면서 그 의미를 확인하는 것도 DateTimeFormatter를 이해하는데 도움이 될 것이다.

DateTimeFormatter.ISO_LOCAL_DATE_TIME을 이용하여 String 타입의 시간 형식을 파싱 하는 코드를 살펴보자.

//T의 의미는 날짜 뒤에 시간이 표시된다는 의미의 리터럴 문자이다.
String localDateTimeStr = "2023-09-17T14:08:23.123";
LocalDateTime localDateTime = LocalDateTime.parse(localDateTimeStr, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
System.out.println(localDateTime);

//ISO_LOCAL_DATE_TIME은 y-M-dTH:m[:s][.n] ([]는 생략가능 하다는 의미)
//형식이므로 아래 시간 형식은 y-M-dTH:m 형식이므로 ISO_LOCAL_DATE_TIME으로 파싱이 가능하다.
localDateTimeStr = "2023-09-17T14:08";
localDateTime = LocalDateTime.parse(localDateTimeStr, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
System.out.println(localDateTime);

 

DateTimeFormatterBuilder를 이용한 커스텀 DateTimeFormatter 생성

주어진 시간 형식에 맞게 formatter를 커스텀하게 생성하여 시간 정보를 파싱 할 수 있다.

예를 들어 날짜와 시간 사이를 구분 짓는 리터럴 문자 'T'가 있지만 DateTimeFormatter에서 기본적으로 제공하는 형식이 아닌 경우 에는 ofPattern 메서드를 이용하여 파싱 하기가 어렵다. 아래의 코드를 보면

String zonedTimeStr = "2023-09-14T02:06:03.988+0000";
LocalDateTime localDateTime =
    LocalDateTime.parse(zonedTimeStr, DateTimeFormatter.ofPattern("y-M-dTH:m:s.SZ"));
System.out.println(localDateTime);
--------------------------------------------------------
Unknown pattern letter: T 오류가 발생한다.

 보통은 ofPattern으로 시간 형식에 맞게 각 symbol을 지정하게 되는데 리터럴 문자 'T' 역시 symbol로 인식하게 되어 오류가 발생한다.

이러한 경우에는 DateTimeFormatterBuilder를 사용하여 직접 DateTimeFormatter를 생성하여 처리할 수 있다.

String zonedTimeStr = "2023-09-14T02:06:03.988+0000";
DateTimeFormatter formatter = new DateTimeFormatterBuilder()
    .parseCaseInsensitive()
    .appendPattern("yyyy-MM-dd")
    .appendLiteral('T')
    .appendPattern("HH:mm:ss.SSS")
    .parseLenient()
    .appendOffset("+HHmm", "Z")
    .parseStrict()
    .toFormatter();

ZonedDateTime zonedDateTime = ZonedDateTime.parse(zonedTimeStr, formatter);
System.out.println("time: " + zonedDateTime);
System.out.println("zone: " + zonedDateTime.getZone());

String format = zonedDateTime.format(formatter);
System.out.println(format);
-------------------------------------------------------------------------
결과
time: 2023-09-14T02:06:03.988Z
zone: Z
2023-09-14T02:06:03.988Z
-------------------------------------------------------------------------
timezone 'Z'는 UTC를 의미한다.

 

시간 변환 케이스

timezone 목록은 List Of DataBase Timezones, List of Timezone Abbreviations을 참고

로컬 시간을 UTC 시간으로 변경 (LocalDateTime -> Instant)

테스트 환경의 timezone은 아래와 같다. (Asiz/Seoul +0900)

$>ll /etc/localtime
lrwxr-xr-x  1 root  wheel  36  9 10 20:12 /etc/localtime -> /var/db/timezone/zoneinfo/Asia/Seoul
LocalDateTime localDateTime = LocalDateTime.now();
System.out.println("localDateTime: " + localDateTime);

//내부적으로 ZoneOffset으로 지정된 시간초만큼을 뺀 epoch second로 Inatant를 생성하기 때문에
//ZoneOffset.UTC는 offset 초가 0 이므로 시간은 그대로이고 timezone만 UTC로 변경한 것과 같다.
Instant instant = localDateTime.toInstant(ZoneOffset.UTC);
System.out.println("instant: " + instant);

//ZoneOffset을 바로 지정하여 Instant를 생성할 수도 있다.
//9시간만큼의 초(3600 * 9) 만큼을 현재시간에 뺀 시간을 생성한다.
instant = localDateTime.toInstant(ZoneOffset.of("+0900"));
System.out.println("instant: " + instant);

//시스템에 적용된 timezone이 적용된 ZonedDateTime 인스턴스를 이용한다.
ZonedDateTime zonedDateTime = localDateTime.atZone(ZoneId.systemDefault());
instant = zonedDateTime.toInstant();
System.out.println("instant: " + instant);

ZoneOffset offset = zonedDateTime.getOffset();
instant = localDateTime.toInstant(offset);
System.out.println("instant: " + instant);
--------------------------------------------------------------------
결과
localDateTime: 2023-09-17T16:48:08.397653
instant: 2023-09-17T16:48:08.397653Z
instant: 2023-09-17T07:48:08.397653Z
instant: 2023-09-17T07:48:08.397653Z
instant: 2023-09-17T07:48:08.397653Z
--------------------------------------------------------------------
'Z'는 UTC timezone을 의미한다.

 

UTC 시간을 로컬 시간으로 변경 (Instant -> LocalDateTime)

Instant instant = Instant.now();
System.out.println("instant now: " + instant);
//변환할 timezone을 설정한다.
ZonedDateTime zonedDateTime = instant.atZone(ZoneId.systemDefault());
System.out.println("time zone: " + zonedDateTime.getZone());
LocalDateTime localDateTime = zonedDateTime.toLocalDateTime();
System.out.println("localDateTime: " + localDateTime);
----------------------------------------------------------
결과
instant now: 2023-09-17T08:11:04.676517Z
time zone: Asia/Seoul
localDateTime: 2023-09-17T17:11:04.676517

 

로컬 시간을 지정된 timezone 시간으로 변경 (LocalDateTime -> ZonedDateTime)

LocalDateTime localDateTime = LocalDateTime.now();
ZoneId systemZone = ZoneId.systemDefault();
//PST (America/Los_Angeles)
ZoneId pstZone = ZoneId.of(ZoneId.SHORT_IDS.get("PST"));

//현재 시간을 ZonedDateTime 인스턴스로 변환
ZonedDateTime zonedDateTime = ZonedDateTime.of(localDateTime, systemZone);
//ZonedDateTime을 Instant 시간 기준으로 PST Zone 시간으로 변경
ZonedDateTime pstZoneTime = zonedDateTime.withZoneSameInstant(pstZone);

System.out.println("local time now: " + localDateTime);
System.out.println("local zoned time now: " + zonedDateTime);
System.out.println("PST time now: " + pstZoneTime);

//ZonedDateTime.toLocalDateTime()은 시스템 timezone에 맞춰서 변환해 주는 것이 아니라 
//timezone 정보를 없애는 효과다.
//주어진 timezone에 맞춰서 시간을 변환하는 것은 withZoneSameInstant(toTimeZone)을 사용한다.
System.out.println(localDateTime + "은 미국 LA 시간으로 " + pstZoneTime.toLocalDateTime() + "입니다.");
---------------------------------------------------------
결과
local time now: 2023-09-17T18:50:58.739906
local zoned time now: 2023-09-17T18:50:58.739906+09:00[Asia/Seoul]
PST time now: 2023-09-17T02:50:58.739906-07:00[America/Los_Angeles]
2023-09-17T18:50:58.739906은 미국 LA 시간으로 2023-09-17T02:50:58.739906입니다.

 

UTC 시간을 지정된 timezone 시간으로 변경 (Instant -> ZonedDateTime)

//UTC 시간
Instant instant = Instant.now();
ZoneId pstZone = ZoneId.of(ZoneId.SHORT_IDS.get("PST"));
//PST timezone이 지정된 ZonedDateTime으로 변경
ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(instant, pstZone);
System.out.println("UTC time now: " + instant);
System.out.println("PST time now: " + zonedDateTime);

//PST time to local time (origin zone time -> UTC -> to zone time)
ZonedDateTime localDateTime = zonedDateTime.withZoneSameInstant(ZoneId.systemDefault());
System.out.println("local zone time now: " + localDateTime.toLocalDateTime());
-----------------------------------------------------------
결과
UTC time now: 2023-09-17T09:59:43.951825Z
PST time now: 2023-09-17T02:59:43.951825-07:00[America/Los_Angeles]
local zone time now: 2023-09-17T18:59:43.951825

 

지정된 timezone 시간을 로컬 시간으로 변경 (ZonedDateTime -> LocalDateTime)

ZonedDateTime을 LocalDateTime으로 변환하는 것은 ZonedDateTime.toLocalDateTime()을 호출하면 된다고 생각하기 쉬운데 .toLocalDateTime()은 ZonedDateTime에서 timezone 부분만 제거하는 효과라서 원하는 결과를 얻는 호출이 아니다.

timezone이 지정된 시간을 로컬 시간으로 변경하기 위해서는 이미 위 샘플코드에서 사용된 .withZoneSameInstant()를 사용한다.

ZoneId pstZone = ZoneId.of(ZoneId.SHORT_IDS.get("PST"));
ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(Instant.now(), pstZone);
System.out.println("pst zone time: " + zonedDateTime);
ZonedDateTime localZoneTime = zonedDateTime.withZoneSameInstant(ZoneId.systemDefault());
System.out.println("local zone time: " + localZoneTime);
LocalDateTime localDateTime = localZoneTime.toLocalDateTime();
System.out.println("local date time: " + localDateTime);
---------------------------------------------------------------------
결과
pst zone time: 2023-09-17T03:07:18.737404-07:00[America/Los_Angeles]
local zone time: 2023-09-17T19:07:18.737404+09:00[Asia/Seoul]
local date time: 2023-09-17T19:07:18.737404

 

지정된 timezone 시간을 다른 timezone 시간으로 변경(ZonedDateTime -> ZonedDateTime)

//방콕 시간
String zonedTimeStr = "2023-09-14T02:06:03.988+07:00";
DateTimeFormatter formatter = new DateTimeFormatterBuilder()
    .parseCaseInsensitive()
    .appendPattern("y-M-d")
    .appendLiteral('T')
    .appendPattern("H:m:s")
    .appendFraction(MILLI_OF_SECOND, 0, 3, true)
    .parseLenient()
    .appendOffsetId()
    .parseStrict()
    .toFormatter();

ZonedDateTime bangkokTime = ZonedDateTime.parse(zonedTimeStr, formatter);
ZoneId toTimeZone = ZoneId.of("America/Los_Angeles");
//LA 시간
ZonedDateTime losAngelesZoneTime = bangkokTime.withZoneSameInstant(toTimeZone);

System.out.println("방콕 시간 " + bangkokTime.toLocalDateTime() +
    "은 LA 시간으로 " + losAngelesZoneTime.toLocalDateTime() + " 입니다.");
-------------------------------------------------------------------------
결과
방콕 시간 2023-09-14T02:06:03.988은 LA 시간으로 2023-09-13T12:06:03.988 입니다.

 

timestamp를 로컬 시간으로 변경 (timestamp -> LocalDateTime)

Instant instant = Instant.ofEpochSecond(1695368422);
System.out.println("instant: " + instant);

//방법1
ZoneOffset offset = ZoneId.systemDefault().getRules().getOffset(instant);
System.out.println("zone offset: " + offset);
LocalDateTime localDateTime2 = LocalDateTime.ofEpochSecond(1695368422, 0, offset);
System.out.println("local date time1: " + localDateTime2);

//방법2
LocalDateTime localDateTime3 = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
System.out.println("local date time2: " + localDateTime3);
--------------------------------------------------------------
결과
instant: 2023-09-22T07:40:22Z
zone offset: +09:00
local date time1: 2023-09-22T16:40:22
local date time2: 2023-09-22T16:40:22

 

timestamp를 지정된 timezone 시간으로 변경

아래 코드는 주어진 timestamp 시간을 미국 LA 현지 시간으로 변경하는 예시 코드다.

//1695368422: 2023-09-22T07:40:22Z (UTC 시간)
Instant instant = Instant.ofEpochSecond(1695368422);
System.out.println("instant: " + instant);

//UTC 시간을 PST(미국 LA) 시간으로 변경
ZoneId pstZone = ZoneId.of(ZoneId.SHORT_IDS.get("PST"));
ZoneOffset pstOffset = pstZone.getRules().getOffset(instant);
System.out.println("PST time offset: " + pstOffset);

ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(instant, pstZone);
System.out.println("zoned date time: " + zonedDateTime);

//Timezone을 제거 하여 zoned time을 현지 시간화
LocalDateTime localDateTime = zonedDateTime.toLocalDateTime();
System.out.println(instant + " UTC 시간은 미국 LA 현지 시간 으로 " + localDateTime + " 입니다.");
----------------------------------------------------
결과
instant: 2023-09-22T07:40:22Z
PST time offset: -07:00
zoned date time: 2023-09-22T00:40:22-07:00[America/Los_Angeles]
2023-09-22T07:40:22Z UTC 시간은 미국 LA 현지 시간 으로 2023-09-22T00:40:22 입니다.

 

시간 변환을 위한 다양한 케이스가 있겠지만 자주 있을 법한 변환 예시를 적어 보았다. 시간 변환에 관련한 내용은 java.time 패키지에 있는 DateTimeFormatter, LocalDateTime, ZonedDateTime, OffsetDateTime, ZoneId, ZoneOffset 등 직접 코드를 확인해 보면 많은 도움이 될 것이다.

댓글

💲 추천 글