Spring/Java

Java Date & Time, 제대로 사용하기

gngsn 2022. 5. 22. 16:18

Java의 날짜와 시간을 제대로 다루기 위해 Date, LocalDate, LocalDateTime 의 개념을 이해하고 응용하는 것이 본 포스팅의 목표입니다.

 

 

History

이제는 자바 8이 꽤나 보편화된 것 같은데요.

자바 8버전이 중요하다고 하는 이유 중 하나가 바로 "날짜"를 다루는 방식입니다.

 

자바 8에 새로운 날짜와 시간 API가 생긴 이유는 아래와 같은 이유가 있습니다.

 

✔️ Mutable

자바 8 이전의 util.Date 클래스는 mutable 하기 때문에 thead safe하지 않았습니다.

 

public class App {
    public static void main(String[] args) throws InterruptedException {
        // mutable 한 객체 -> multi-thread 환경에서 사용하기 어렵다.
        Thread.sleep(1000 * 3);
        Date after3Second = new Date();
        System.out.println(after3Second);
        after3Second.setTime(time);
        System.out.println(after3Second);
    }
}

 

위의 코드를 예시로 들면 두 개의 스레드가 충돌이 일어날 수 있기 때문에 thread-safe 하지 않습니다.

 

mutable 하다는 것은 말그대로 변경 가능하다는 것입니다.

이 mutable하다는 특성으로 여러 스레드가 실행될 때 그 값을 보장받지 못할 수 있습니다.

 

 

✔️ 불분명한 네이밍

클래스 이름이 명확하지 않습니다.

아래와 같이 Date 이름을 가진 객체인데 시간까지 다루는 것처럼 말이죠.

 

Date date = new Date();        // Sat May 21 15:29:37 KST 2022
long time = date.getTime();    // 1653114577929

 

위의 코드를 실행해보면 Date라는 클래스의 인스턴스로 '날짜' 정보와 '시간' 정보를 같이 가지고 있는 것을 확인할 수 있습니다.

또, 시간을 가져오면 보통 사람들이 알고있는 형식의 시간이 아닙니다.

 

이 시간은 milliseconds 단위의 Unix Time (= Epoch time, POSIX time) 으로 표시된 시간인데요.

Unix Time이란 UTC 기준으로 했을 때 1970년 1월 1일 00시 00분 00초를 기준으로 해서 지난 시간을 의미합니다.

참고로 1970년 1월 1일 00시 00분 00초 UTC 인 이유는 UNIX 운영체제의 최초 출시년도가 1971년이기 때문입니다 ㅎㅎ.ㅎ.ㅎㅎ..

 

실제로 Unix Timestamp 변환 사이트에서 시간을 변경해보면 아래와 같은 시간 정보를 얻어올 수 있습니다.

 

즉, getTime에서 Time은 시간을 현시점의 "타임스탬프"로 얻어온다는 의미가 큽니다.

시간, 분, 초, 밀리초 등의 기대를 했다면 처음에는 많이 당황할 수도 있겠죠.

 

 

✔️ 버그

버그가 발생할 여지가 많았습니다.

타입 안정성이 없고, 월이 0부터 시작하는 등의 불편함을 가졌습니다.

 

Calendar birthDay = new GregorianCalendar(2022, 8, 12);
birthDay.getTime();    // Mon Sep 12 00:00:00 KST 2022

 

위의 코드에 버그가 있는데요.

예전에는 month가 0부터 시작해서 0이 1월이었기 때문에, 8월을 적고 싶으면 7을 적어야하는 문제가 생긴 것입니다.

실제로 9월 12일로 얻어오는 것을 확인할 수 있습니다.

 

그래서 아래와 같이 수정해야 했습니다.

 

Calendar birthDay = new GregorianCalendar(2022, GregorianCalendar.AUGUST, 12);
birthDay.getTime();      // Fri Aug 12 00:00:00 KST 2022

 

GregorianCalendar.AUGUST는 7이라는 숫자를 갖고 있습니다.

 

이런 문제점으로 날짜 시간 처리가 복잡한 애플리케이션에서는 보통 Joda Time을 쓰곤했습니다.

하지만 자바8부터는 날짜와 시간 관리가 조금 더 명확해졌습니다.

 

 

 

Now DateTime API

 

시간은 크게 기계용 시간 (machine time) vs 인류용 시간(human time)으로 구분할 수 있는데요.

 

기계가 다루는 시간으로 타임스탬프를 다루는 Instant 클래스를 제공합니다.

기계용 시간은 epoch부터 현재까지의 타임스탬프를 표현합니다.

 

일반적으로 개발자들이 사용하는 인류용 시간은

우리가 흔히 사용하는 연,월,일,시,분,초 등을 표현할 수 있으며,

아래에 소개할 다양한 형태의 날짜, 시간, 일시 클래스를 제공합니다.

 

아래와 같은 클래스가 있으며, 한 번 훑어보고 사용법에 대해 알아봅시다.

 

Class or Enum Year Month Day Hours Minutes Seconds* Zone Offset Zone ID toString Output Where Discussed
Instant          
✔️
    2013-08-20T15:16:26.355Z Instant Class
LocalDate
✔️
✔️
✔️
          2013-08-20 Date Classes
LocalDateTime
✔️
✔️
✔️
✔️
✔️
✔️
    2013-08-20T08:16:26.937 Date and Time Classes
ZonedDateTime
✔️
✔️
✔️
✔️
✔️
✔️
✔️
✔️
2013-08-21T00:16:26.941+09:00[Asia/Tokyo] Time Zone and Offset Classes
LocalTime      
✔️
✔️
✔️
    08:16:26.943 Date and Time Classes
MonthDay  
✔️
✔️
          --08-20 Date Classes
Year
✔️
              2013 Date Classes
YearMonth
✔️
✔️
            2013-08 Date Classes
Month  
✔️
            AUGUST DayOfWeek and Month Enums
OffsetDateTime
✔️
✔️
✔️
✔️
✔️
✔️
✔️
  2013-08-20T08:16:26.954-07:00 Time Zone and Offset Classes
OffsetTime      
✔️
✔️
✔️
✔️
  08:16:26.957-07:00 Time Zone and Offset Classes
Duration     ** ** **
✔️
    PT20H (20 hours) Period and Duration
Period
✔️
✔️
✔️
      *** *** P10D (10 days) Period and Duration

 

* : 정밀도가 나노초 단위입니다.

** : 해당 정보를 저장하지 않지만 이러한 단위로 시간을 제공하는 메서드가 있습니다.

*** : 해당 정보를 ZonedDateTime에 추가하면 DST(써머타임) 또는 기타 현지 시간 차이가 관찰됩니다.

 

 

위의 oracle에서 제공하는 표를 참고하면 클래스 별로 다루고자 하는 일시를 판단할 수 있습니다.

 

 

 

Instant

기계가 다루는 시간으로 타임스탬프를 다루는 Instant 클래스를 제공합니다.

기계용 시간은 EPOCH (1970년 1월 1일 0시 0분 0초)부터 현재까지의 타임스탬프를 표현합니다.

 

Instant instant = Instant.now();                     // (기준시 UTC, GMT)2022-05-21T08:21:07.333584Z

ZoneId zone = ZoneId.systemDefault();                // Asia/Seoul
//            = ZoneId.of("Asia/Seoul");

ZonedDateTime zonedDateTime = instant.atZone(zone);  // 2022-05-21T17:21:07.333584+09:00[Asia/Seoul]

 

Instant(인스턴트)를 이용해 타임스탬프는 경우에는,

한 순간의 한 지점을 다른 순간의 지점과 비교하고 싶을 때 사용할 가능성이 높습니다.

 

 

 

DateTime in Java 8

이제부터는 보통 개발 시 다룰 날짜 클래스는 어떤 것들이 있는지 알아보도록 하겠습니다.

즉, 인류용 시간을 다뤄볼텐데요.  java.time에서는 이 시간을 크게 두 가지로 구분합니다.

 

특정 시간대을 고려한 시간 표현과 시간대를 고려하지 않는 일시 표현으로 나눌 수 있습니다.

시간대에는 써머타임같은 시간과 관련된 규정을 포함합니다.

 

전자(시간대 고려)는 어떤 위치, 지역에서 사용되는 시간표현인지

즉, 타임존에 따라 표시 시간이 변동되는 ZonedDateTime, OffsetDateTime 객체로 표현될 수 있습니다.

혹시나 이 부분이 이해가 안간다면 분명 타임존에 대한 이해가 필요할 것 같아요. 

타임존은 이전 포스팅인 TimeZone, 어렵지 않게 이해하기에서 다루었으니, 참고 바랍니다.

 

 

후자(시간대 고려하지 않음)는 위치에 상관없이 그 날짜, 혹은 시간 데이터 자체만을 다룹니다.

자바에서는 Local 이라는 접두사로 이를 구분해서 LocalDateTime 과 같이 사용합니다.

 

 

예를 들어, 대부분의 사람들은 생일을 표시할 때 출생 국가에 관계없이, 같은 날을 표시합니다.

한국에서 8월 12일에 이른 새벽 태어났다고해서 영국에서 11일이라고 표시하지는 않으니까요,,,

따라서 출생 도시에 있든 지구 반대편에 있든 간에 같은 날로 생일을 표시하기 때문에 LocalDate 개체를 사용하여 생년월일을 나타낼 수 있습니다. 

 

그 반대로, 회의 시간을 잡을 때는 ZonedDateTime이 필요하게 됩니다.

14시 회의라고 해서 한국과 영국에 있는 두 사람이 각각의 국가 기준 14시에 회의라고 생각하면 영영 못만나겠죠.

 

 

 

LocalDateTime

자바 8 이후부터는 특정 날짜, 시간, 일시를 java.time 패키지 내의 LocalDate, LocalTime, LocalDateTime 객체를 통해 다룹니다.

 

명확해진 네이밍이 보여지나요?

해당 클래스들을 살펴보면 아래와 같은 필드를 갖고 있어요.

 

public final class LocalDate implements ... {
    // ...
    private final int year;
    private final short month;   
    private final short day;
    // ...
}
public final class LocalTime implements ... {
    // ...
    private final byte hour;
    private final byte minute;
    private final byte second;
    private final int nano;
    // ...
}
public final class LocalDateTime implements ... {
    // ... 
    private final LocalDate date;
    private final LocalTime time;
    // ...
}

 

 

Method

LocalDateTime을 생성하는 팩토리 메소드는 아래와 같이 정의됩니다.

LocalDate, LocalTime도 비슷하게 정의되어 있기 때문에 LocalDateTime만 적어두겠습니다.

 

//Factory 
public static LocalDateTime now()  
public static LocalDateTime now(ZoneId zone)  

public static LocalDateTime of(int year, Month month, int dayOfMonth, int hour, int minute [, /* ... */])
public static LocalDateTime of(LocalDate date, LocalTime time)
public static LocalDateTime ofInstant(Instant instant, ZoneId zone)

public static LocalDateTime parse(CharSequence text)
public static LocalDateTime parse(CharSequence text, DateTimeFormatter formatter)

 

주요 메소드는 아래와 같습니다.

 

// isX(..)
public boolean isBefore(ChronoLocalDateTime<?> other)
public boolean isAfter(ChronoLocalDateTime<?> other)

// format(..)
public String format(DateTimeFormatter formatter)

// else
public LocalTime toLocalTime()
public ZonedDateTime atZone(ZoneId zone)  
public OffsetDateTime atOffset(ZoneOffset offset)

// plus(..)
public LocalDateTime plusYears(long years)
public LocalDateTime plusMonths(long months)  
public LocalDateTime plusWeeks(long weeks)  
public LocalDateTime plusDays(long days)  
public LocalDateTime plusHours(long hours)
public LocalDateTime plusMinutes(long minutes)  
public LocalDateTime plusSeconds(long seconds)  
public LocalDateTime plusNanos(long nanos) 

// minus(..)
public LocalDateTime minusYears(long years)  
public LocalDateTime minusMonths(long months)  
public LocalDateTime minusWeeks(long weeks)  
public LocalDateTime minusDays(long days)  
public LocalDateTime minusHours(long hours)  
public LocalDateTime minusMinutes(long minutes)  
public LocalDateTime minusSeconds(long seconds)  
public LocalDateTime minusNanos(long nanos) 

// with(..)
public LocalDateTime withYear(int year)
public LocalDateTime withMonth(int month)
public LocalDateTime withDayOfMonth(int dayOfMonth)
public LocalDateTime withDayOfYear(int dayOfYear)
public LocalDateTime withHour(int hour)
public LocalDateTime withMinute(int minute)
public LocalDateTime withSecond(int second)
public LocalDateTime withNano(int nanoOfSecond)

 

 

Sample Code

// factory
LocalDateTime now = LocalDateTime.now();  
LocalDateTime datetime1 = LocalDateTime.of(2017, 1, 14, 10, 34);  
LocalDateTime dateTime2 = LocalDateTime.of(2019, Month.MARCH, 28, 14, 33, 48, 000000);
        
// format
DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String formatDateTime = now.format(format);  

// plus & minus
LocalDateTime datetime3 = datetime1.plusDays(120); 
LocalDateTime datetime4 = datetime1.minusDays(100);

 

 

 

ZonedDateTime

"Zoned" 에서 느껴지듯이 타임존을 고려한 시간을 표시합니다.

 

ZoneId zone = ZoneId.of("Asia/Seoul");  // 한국 내에 ZoneId.systemDefault() 와 동일
ZonedDateTime zonedDateTime = ZonedDateTime.now(zone);

 

위와 같이 사용할 수 있습니다.

ZonedDateTime 클래스는 특정 날짜특정 시간대에서 다른 시간대변환하기 위한 내장된(Built-in) 방법을 제공합니다.


마지막으로, 일광절약시간제(Daylight Saving Time)를 내부에서 완전히 제어하여 처리해줍니다.
각각의 시간대 별로 시간을 표시하기에 정말 편리하겠죠.

 

대부분의 메소드는 LocalDateTime과 비슷하기 때문에 자세한 내용은 생략하겠습니다.

 

 

 

Period

그 밖에도 시간과 관련된 기능으로 두 날짜/시간의 기간을 다루는 객체를 제공합니다.

기간을 표현할 때는 Duration (시간 기반)과 Period (날짜 기반)를 사용할 수 있습니다.

 

Period는 아래와 같이 생성할 수 있습니다.

 

Period between = Period.between(today, thisYearBirthday);
// or
Period until = today.until(thisYearBirthday);

 

기간에 대한 데이터를 가져올 때는 아래와 같이 사용할 수 있습니다.

 

// 2022년 5월 22일 기준

between.getYears();    // 23
between.getMonths();   // 9
between.getDays();     // 10

 

 

Method

// factory
public static Period ofYears(int years)  
public static Period ofMonths(int months)
public static Period ofWeeks(int weeks)
public static Period ofDays(int days)  
public static Period of(int years, int months, int days)
public static Period from(TemporalAmount amount)
public static Period parse(CharSequence text)  
public static Period between(LocalDate startDateInclusive, LocalDate endDateExclusive)

// withX()
public Period withYears(int years)
public Period withMonths(int months)  
public Period withDays(int days)  

// plusX()
public Period plus(TemporalAmount amountToAdd)
public Period plusYears(long yearsToAdd)  
public Period plusMonths(long monthsToAdd)
public Period plusDays(long daysToAdd)

// minusX()
public Period minus(TemporalAmount amountToSubtract)  
public Period minusYears(long yearsToSubtract)  
public Period minusMonths(long monthsToSubtract)
public Period minusDays(long daysToSubtract)  

public long toTotalMonths()

 

 

두 날짜 차이 계산

과거의 특정 시점부터 현재까지의 날짜를 계산하고 싶을 수도 있을텐데요.

즉, 두 날짜 간의 차이를 구하는 방법은 아래와 같습니다.

 

ChronoUnit.DAYS.between(birth, today);   // 8684

 

생일이 1998년 8월 12일이었을 때 오늘까지 8684일이 지난 것입니다.

뜬금없지만 8684일 밖에 안살았다는 게 놀랍네요...

 

 

 

Duration

Duration 메소드가 잘 정리되어 있는 링크인데, 참고하시면 좋을 것 같네요 😋 

주요 메소드는 아래와 같이 정리할 수 있겠네요 ~!

 

// ofX(..)
public static Duration ofDays(long days)  
public static Duration ofHours(long hours)  
public static Duration ofMinutes(long minutes)
public static Duration ofSeconds(long seconds)
public static Duration ofSeconds(long seconds, long nanoAdjustment)
public static Duration ofMillis(long millis)
public static Duration ofNanos(long nanos)
public static Duration of(long amount, TemporalUnit unit)
public static Duration from(TemporalAmount amount)
public static Duration parse(CharSequence text)  
public static Duration between(Temporal startInclusive, Temporal endExclusive)

// plusX(..)
public Duration plusNanos(long nanosToAdd)  
public Duration plusMillis(long millisToAdd)  
public Duration plusSeconds(long secondsToAdd)  
public Duration plusMinutes(long minutesToAdd)  
public Duration plusHours(long hoursToAdd)  
public Duration plusDays(long daysToAdd)

// minusX()
public Duration minus(Duration duration)  
public Duration minus(long amountToSubtract, TemporalUnit unit)
public Duration minusDays(long daysToSubtract)
public Duration minusHours(long hoursToSubtract)  
public Duration minusMinutes(long minutesToSubtract)  
public Duration minusSeconds(long secondsToSubtract)  
public Duration minusMillis(long millisToSubtract)
public Duration minusNanos(long nanosToSubtract)  

// toX()
public long toDays()  
public long toHours()  
public long toMinutes()  
public long toSeconds()  
public long toMillis()  
public long toNanos()

 

 

Sample Code

Instant instantNow = Instant.now();
Instant plus = instantNow.plus(10, ChronoUnit.SECONDS);
Duration between = Duration.between(instantNow, plus);

System.out.println(between.getSeconds());

 

 

 

생각보다 내용이 길어졌네요...🥲

그럼 지금까지 Java에서 날짜를 다루는 방법에 대해 알아보았습니다.

오타나 잘못된 내용이 있다면 댓글로 남겨주세요!

감사합니다 ☺️