Cf. 기존에 Java에서 시간과 관련해 제공하던 API
Date
, GregorianCalendar
, SimpleDateFormat
등이 있습니다.
Date
는 mutable한 특성(인스턴스 내부의 값 변경 가능)때문에 thread-safe하지 못하다는 단점이 있습니다. - 동시성 문제를 명확히 해결하지 않으면 중간에 값이 변경될 가능성이 있습니다. 또한Date
가 time까지 다루는 등 클래스명과 실제 처리하는 데이터가 다른 경우가 있습니다. 이 뿐만 아니라 타입 안정성이 없어 버그가 발생할 여지가 많습니다.- 예를 들어,
GregorianCalender
를 통해Calendar
인스턴스를 만들 때 '월'은 0부터 시작하는 것을 감안하여 입력해야 합니다. 이러한 특성을 두고 타입안정성이 없다고 합니다.
- 예를 들어,
- 위와 같은 문제들 때문에 Java 8 이전에 날짜 시간 처리가 복잡한 애플리케이션에서는 'Joda Time'라는 외부 오픈소스 라이브러리가 대중적으로 사용되곤 했습니다. 하지만 Java 8부터는 이 Joda Time의 기능들이 Java 표준 API로 들어오게 되었습니다.
Java 8에서 제공하는 Date-Time API(JSR-310)
철학
- clear : 명확해야 합니다.
- fluent: 코드가 수려해야 하고(읽기 쉽고), 메서드 체이닝이 가능해야 합니다.
- immutable : 기존 객체의 값에 변화를 주면 기존 객체의 값이 변화되는 것이 아니라 새로운 객체로 반환됩니다.
- extensible: 확장이 가능해야 합니다.
주요 API
- 기계용 시간(machine time)과 인류용 시간(human time)으로 나뉩니다.
Cf. 기계용 시간은 EPOCK(1970/01/01 00:00:00)부터 현재까지의 timestamp를 표현합니다. - timestamp로는
Instant
을 사용 가능합니다. - 특정 날짜(
LocalDate
), 시간(LocalTime
), 일시(LocalDateTime
) 사용 가능합니다. - 기간을 표현할 때는
Duration
(시간 기반)과Period
(날짜 기반) 사용 가능합니다. DateTimeFormatter
를 사용해서 일시를 특정한 형태의 문자열로 만들 수 있습니다.
Instant
- now() : 현재 UTC(Universal Time Coordinated)을 반환합니다.
- UTC와 GMT(Greenwich Mean Time)은 동일합니다.
- 특정 TimeZone의 타임스탬프를 얻고자 하면 ZonedDateTime 인스탄스를 반환해주는
atZone(ZoneId z)
를 사용할 수 있다. - 현재 시스템상의 TimeZone을 사용한다고 할 때 한국의 경우 타임스탬프 뒤에
+09:00[Asia/Seoul]
가 함께 출력됩니다. - Cf. 현재 시스템상의 TimeZone을 사용하고자 할 때에는 ZoneId.systemDefault()를 사용할 수 있습니다.
import java.time.ZoneId;
public class App {
public static void main(String[] args) {
Instant now = Instant.now();
ZoneId zone = ZoneId.systemDefault(); // 현재 system상의 zone
ZonedDateTime now = now.atZone(zone);
}
}
ZonedDateTime
LocalDateTime
에 TimeZone의 개념이 결부된 개념입니다.
now()
에 인자를 전달하지 않을 경우Instant
와 마찬가지로 UTC(+00:00) 기준 시간을 반환받습니다.- 특정 시간대의 현재 시간을 구할 때에는
now()
의 인자로ZoneId
혹은ZoneOffset
을 전달합니다. - 특정 시간을 구할 때에는
of(ZoneId or ZoneOffset)
를 사용합니다. - 만약
ZoneId.of(ZoneId or ZoneOffset)
의 인자는 '국가명/도시'뿐만 아니라 '시차'로도 전달 가능합니다.
public class App {
public static void main(String[] args) {
System.out.println("Current(UTC) " + ZonedDateTime.now());
System.out.println("Current(Chicago) " + ZonedDateTime.now(ZoneId.of("America/Chicago")));
System.out.println("Current(Paris) " + ZonedDateTime.now(ZoneId.of("Europe/Paris")));
// ZoneId에 ZoneOffset값을 넣어도 값이 나오긴 한다. (+2 == UTC+2 == UTC+02 == UTC+02:00)
System.out.println("Current(+2)" + ZonedDateTime.now(ZoneId.of("+2")));
System.out.println("Current(UTC+2) " + ZonedDateTime.now(ZoneId.of("UTC+2")));
System.out.println("Current(UTC+02) " + ZonedDateTime.now(ZoneId.of("UTC+02")));
System.out.println("Current(UTC+02:00) " + ZonedDateTime.now(ZoneId.of("UTC+02:00")));
System.out.println("Current(GMT+2) " + ZonedDateTime.now(ZoneId.of("GMT+2")));
System.out.println("Current(GMT+02) " + ZonedDateTime.now(ZoneId.of("GMT+02")));
System.out.println("Current(GMT+02:00) " + ZonedDateTime.now(ZoneId.of("GMT+02:00")));
}
}
ZoneId vs ZoneOffset
ZoneId
는 TimeZone을, ZoneOffset
은 시차(UTC와의 시차)를 의미합니다. 예를 들어 서울의 타임존은 Asia/Seoul
이지만, 시차는 `+09:00'입니다.
영토의 위치, 크기 등에 따라 항상 하나의 타임존을 사용하지 않는 지역들이 많습니다. 예를 들어 올해(2022년 기준) 캐나다의 토론토는 01.01 ~ 03.13, 11.06 ~ 12.31 사이에는 EST Timezone을 따르지만 03.13 ~ 11.06 간에는 EDT Timezone을 따릅니다.
때문에 특정 지역의 타임존에 따르는 시간을 구할 때에는 ZonedDateTime의 인자로 ZoneId를 사용하는 것이 좋습니다.
LocalDateTime
- 자동으로 시스템의 TimeZone이 적용된 시간이 반환되며, 타임존 정보를 가지지 않습니다. (배포한 서버의 TimeZone을 따름에 유의해야 합니다.)
- 특정 날짜의
LocalDateTime
을 구하고자 할 때에는LocalDateTime.of(int year, Month month, int dayOfMonth, int hour, int minute)
를 사용해야 합니다. (인자로 초, 밀리초까지도 전달할 수 있습니다.) - Cf.
plus(long amount, TemporalUnit unit)
에서TemporalUnit
으로는ChronoUnit
을 사용해야 합니다.
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
public class App {
public static void main(String[] args) {
LocalDateTime now = LocalDateTime.now();
LocalDateTime after10Minutes = now.plus(10, ChronoUnit.MINUTES);
}
}
Cf. Instant에서 ZonedDateTime으로의 변환
Instant
타입의 인스턴스를ZonedDateTime
으로 변환할 때는atZone(ZoneId or ZoneOffset)
을 사용합니다.ZonedDateTime
타입의 인스턴스를Instant
로 변환할 때는toInstant()
를 사용합니다.
public class App {
public static void main(String[] args) {
Instant instant1 = Instant.now();
ZonedDateTime zonedDateTime = instant1.atZone(ZoneId.of("Asia/Seoul"));
LocalDateTime localDateTime = zonedDateTime.toLocalDateTime();
System.out.println(instant1);
// 2022-10-22T16:13:09.757512100Z
System.out.println(zonedDateTime);
// 2022-10-23T01:13:09.757512100+09:00[Asia/Seoul]
System.out.println(localDateTime);
// 2022-10-23T01:13:09.757512100
}
}
ZonedDateTime -> LocalDateTime
타임존 정보가 빠지기만 할 뿐 타임존이 현재 시스템의 타임존으로 바뀌고 시간이 달라지지는 않는다는 점에 유의해야 합니다.
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
public class App {
public static void main(String[] args) {
ZonedDateTime now = ZonedDateTime.now(ZoneId.of("Asia/Seoul"));
LocalDateTime nowLocal = now.toLocalDateTime();
}
}
LocalDateTime -> ZonedDateTime 간의 변환
시간대를 적용시키고자 한다면 atZone(ZoneId or ZoneOffset)
을 통해 해당 시간이 어떤 시간대(의 시간)인지가 적용된 ZonedDateTime
을 반환받을 수 있습니다.
public class App {
public static void main(String[] args) {
LocalDateTime nowLocalDateTime = LocalDateTime.now();
ZonedDateTime nowInSeoul = nowLocalDateTime.atZone(ZoneId.of("Asia/Seoul")); // 현재 시간이 Asia/Seoul의 시간임을 정의해준다.
System.out.println("now(no timezone) " + nowLocalDateTime);
// now(no timezone) 2022-10-23T00:41:24.471570400
System.out.println("now(with timezone) " + nowInSeoul);
// now(with timezone) 2022-10-23T00:41:24.471570400+09:00[Asia/Seoul]
}
}
ZonedTimeDate의 시간대 변환
ZonedDateTime
인스턴스를 사용해 다른 시간대의 해당 시간을 얻을 때에는 ZonedDateTime.ofInstant()
를 통해 해당 시간이 다른 시간대에서는 어떻게 변하는지 알 수 있습니다.
Cf. 이 때, 기존의 ZonedDateTime
인스턴스를 Instant
타입으로 변환해 인자로 전달해주어야 합니다.
public class App {
public static void main(String[] args) {
ZonedDateTime nowInSeoul = ZonedDateTime.now(ZoneId.of("Asia/Seoul"));
System.out.println("now(Asia/Seoul) " + nowInSeoul);
// now(with timezone) 2022-10-23T00:41:24.471570400+09:00[Asia/Seoul]
System.out.println("now(Paris) " + ZonedDateTime.ofInstant(nowInSeoul.toInstant(), ZoneId.of("Europe/Paris")));
// now(Paris) 2022-10-22T17:41:24.471570400+02:00[Europe/Paris]
}
}
Period
인류용 시간(LocalDate
)상의 기간을 나타낼 수 있습니다.
Period.between(LocalDate l1, LocalDate l2)
,until(LocalDate compareDate)
Period
를 그대로 출력할 시P[남은 달]M[남은 날]D
형태로 출력됩니다.- 만약
l1
(=compareDate
)가 기준LocalDate
보다 빠를 경우[남은 달]
과 [남은 날]
은 0보다 작거나 같은 수가 출력됩니다. getYears()
,getMonths()
,getDays()
뿐만 아니라get(TemporalUnit unit)
도 사용할 수도 있습니다.
public class App {
public static void main(String[] args) {
LocalDate today = LocalDate.now();
LocalDate nextYearBirthDay = LocalDate.of(2023, Month.FEBRUARY, 14);
Period period = Period.between(today, nextYearBirthDay);
System.out.println(period); // P3M22D
System.out.println(period.getDays()); // 22
Period until = today.until(nextYearBirthDay);
System.out.println(until.get(ChronoUnit.DAYS)); // 22
System.out.println(until.getDays()); // 22
}
}
Duration
기계용 시간(Instant
)상의 기간을 나타낼 수 있습니다.
Duration.between(Instant i1, Instant i2)
Duration
를 그대로 출력할 시P[남은 시간]T[남은 초]S
형태로 출력됩니다.
public class App {
public static void main(String[] args) {
Instant inst1 = Instant.now();
Instant inst2 = inst1.plus(10, ChronoUnit.SECONDS);
Duration duration = Duration.between(inst1, inst2);
}
}
Formatting
DateTimeFormatter.ofPattern("String pattern")
을 통해LocalDateTime
,LocalDate
에 대한 커스텀 포매팅이 가능합니다.parse(LocalDateTime l, DateTimeFormatter formatter)
를 통해 포매팅된 문자열 형태의 날짜 데이터를LocalDateTime
혹은LocalDate
형태로 파싱 할 수 있습니다.- 커스텀 패턴에 시분초 정보가 있는데 이 정보가 없는
LocalDate
를 포매팅하려하면Unsupported field: ClockHourOfAmPm
에러가 발생합니다. - 포매팅과 파싱 사이에는 포매터(
DateTimeFormatter
)의 형태가 일치해야 합니다.
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class App {
public static void main(String[] args) {
LocalDateTime now = LocalDateTime.now();
// formatting
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
System.out.println(now.format(formatter));
// parsing
LocalDate result = LocalDate.parse("2023 - 02 - 14", formatter);
System.out.println("parsed " + result);
}
}
Java8 이전의 시간 관련 API와의 연계
Date
와Instant
간의 변환
import java.util.Date;
public class App {
public static void main(String[] args) {
Date date = new Date();
Instant instant = date.toInstant();
Date dateResult = Date.from(instant);
}
}
GregorianCalendar
와Instant
간의 변환 (ZonedDateTime
,LocalDateTime
등으로 추가 변환 가능)- Cf.
GregorianCalendar
에는 TimeZone 데이터가 존재하므로ZonedDateTime
으로 부터 변환될 수 있습니다.
- Cf.
import java.time.ZonedDateTime;
import java.util.GregorianCalendar;
public class App {
public static void main(String[] args) {
GregorianCalendar calendar = new GregorianCalendar();
Instant instant = calendar.toInstant();
ZonedDateTime zonedDateTime = instant.atZone(ZoneId.systemDefault());
LocalDateTime localDateTime = zonedDateTime.toLocalDateTime();
GregorianCalendar calendar2 = GregorianCalendar.from(zonedDateTime);
}
}
ZoneId
와TimeZone
간의 변환
import java.time.ZoneId;
import java.util.TimeZone;
public class App {
public static void main(String[] args) {
ZoneId zoneId = TimeZone.getTimeZone("PST").toZoneId();
TimeZone timeZone = TimeZone.getTimeZone(zoneId);
}
}
'Backend > Java' 카테고리의 다른 글
Java8) Annotation 변경 사항(타입 선언부에 사용, @Repeatable) (0) | 2022.12.26 |
---|---|
Java8) Java 비동기 프로그래밍과 CompletableFuture (0) | 2022.10.31 |
Java8) Optional이란? (null을 처리하는 또 다른 방법) (0) | 2022.10.31 |
Java8) 함수형 인터페이스(Functional Interface)와 람다(Lambda) (0) | 2022.09.18 |
Java) 2진수의 정수를 10진수 정수로 변환하기(정수의 기수 변환) (0) | 2022.06.30 |