Date and Time

백기선 님의 <더 자바, Java 8>를 보고 공부한 내용을 기록합니다.

1. 배경

  • 그동안 우리는 다양한 방법으로 시간과 날짜를 사용해왔다. 하지만 각각의 방법들은 나름대로 문제점들이 있었다.

  • java.util.Date

    • 클래스 이름이 명확하지 않았다. Date인데 시간까지 다룬다.

    • thread safe 하지 않다.

      • mutable 하기 때문에 setDate() 등으로 날짜를 마구 변경 가능

  • java.util.Calendar

    • 타입 안정성이 없어서 버그가 발생할 여지가 높다.

      • 월은 0부터 시작하고 day 는 1부터 시작한다.

  • joda-time API

    • 그래서 자바8 이전까지는 대부분의 날짜 시간 처리가 복잡한 어플리케이션에서 보통 joda-time API 를 사용해왔다.

<dependency>
    <groupId>joda-time</groupId>
    <artifactId>joda-time</artifactId>
    <version>2.12.2</version>
</dependency>
public class BeforeJava8Test {

    @Test
    void DateApiTest() {
        Date date = new Date();
        System.out.println("date = " + date);   //Thu Mar 16 14:16:15 KST 2023

        long time = date.getTime();
        System.out.println("time = " + time);   //1678943775380

        date.setTime(time - 1);                                 //1678944050334
        System.out.println("changed time = " + date.getTime()); //1678944050333
    }

    @Test
    void CalendarApiTest() {
        Calendar calendar = new GregorianCalendar(-1000, 2, 15); //month는 0부터 시작하므로 숫자를 바로 쓸 수 없음
        assertThat(calendar.getWeekYear()).isEqualTo(-1000);

        Calendar calendar2 = new GregorianCalendar(2002, Calendar.FEBRUARY, 15);
        System.out.println(calendar2);
    }

    @Test
    void jodaTimeApiTest() {
        DateTime dateTime = new DateTime();
        assertThat(dateTime.getYear()).isEqualTo(2023);
        assertThat(dateTime.getMonthOfYear()).isEqualTo(3);
        assertThat(dateTime.getDayOfMonth()).isEqualTo(16);
    }
}

2. Java8 부터 Date-Time API 의 등장

  • JSR-310 스펙의 구현체를 제공한다.

  • 디자인 철학

    • Clear : 명확한 이름

    • Immutable : 불변

    • Fluent : Null type 을 반환하지 않아서 chaining 형태로 계속 확장하여 사용 가능

    • Extensible

2-1. 주요 API 특징

  • 기계용 시간과 인류용 시간, 두 분류로 나눈다.

  • 기계용 시간은 EPOCK 부터 현재까지의 타임스탬프를 표현한다.

    • EPOCK : 리눅스가 시간을 처음 세기 시작했던 1970년 1월 1일 0시 0분 0초

  • 인류용 시간은 우리가 흔히 사용하는 년, 월, 일, 시, 분, 초 등을 표현한다.

  • 타임스탬프는 Instant 를 사용한다.

  • 특정날짜(LocalDate), 시간(LocalTime), 일시(LocalDateTime)을 사용할 수 있다.

  • 기간을 표현할 때는 Duration(시간기반), Period(날짜기반)을 사용할 수 있다.

  • DateTimeFormatter 를 사용해서 일시를 특정한 문자열로 포매팅할 수 있다.

2-2. 코드로 살펴보기


public class AfterJava8Test {

    @Test
    void instant로_기계용_시간_표현하기() {
        System.out.println("Instant.now() = " + Instant.now());
        System.out.println("Instant.now().atZone(ZoneId.of(\"UTC\")) = " + Instant.now().atZone(ZoneId.of("UTC")));

        //Instant.now() = 2023-03-16T05:52:42.740808Z
        //Instant.now().atZone(ZoneId.of("UTC")) = 2023-03-16T05:52:42.753796Z[UTC]
    }

    @Test
    void zonedDateTIme으로_타임존_표현하기() {
        ZonedDateTime zonedDateTime = ZonedDateTime.now();
        System.out.println("zonedDateTime = " + zonedDateTime); //2023-03-16T14:52:15.378961+09:00[Asia/Seoul]

        ZonedDateTime zonedDateTime2 = ZonedDateTime.now(ZoneId.systemDefault());
        System.out.println("zonedDateTime2 = " + zonedDateTime2);   //2023-03-16T14:52:15.383428+09:00[Asia/Seoul]
    }

    @Test
    void localDateTime으로_인류용_시간_표현하기() {
        LocalDateTime localDateTime = LocalDateTime.now();
        System.out.println("localDateTime = " + localDateTime); //localDateTime = 2023-03-16T14:53:32.503884

        LocalDateTime localDateTime1 = LocalDateTime.of(2002, 2, 15, 11, 23, 30);
        System.out.println("localDateTime1 = " + localDateTime1);   //localDateTime1 = 2002-02-15T11:23:30

        ZonedDateTime zonedDateTime = ZonedDateTime.of(localDateTime1, ZoneId.of("Asia/Seoul"));
        System.out.println("zonedDateTime = " + zonedDateTime); //zonedDateTime = 2002-02-15T11:23:30+09:00[Asia/Seoul]
    }

    @Test
    void period로_기간표현하기() {
        System.out.println("LocalDate.now() = " + LocalDate.now());

        Period period = Period.between(LocalDate.of(1994, 8, 11), LocalDate.now());
        System.out.println("period = " + period);   //P-28Y-7M-5D
        System.out.println("period.getYears() = " + period.getYears()); //28
        System.out.println("period.getMonths() = " + period.getMonths());   //7

        Period period2 = Period.between(LocalDate.now(), LocalDate.of(2023, 8, 11));
        System.out.println("period2.getYears() = " + period2.getYears());   //0
        System.out.println("period2.getMonths() = " + period2.getMonths()); //4
        System.out.println("period2.getDays() = " + period2.getDays());     //26
        System.out.println("period2.get(ChronoUnit.DAYS) = " + period2.get(ChronoUnit.DAYS));   //26
    }

    @Test
    void duration으로_기간표현하기() {
        Instant now = Instant.now();
        Instant plus = now.plus(10, ChronoUnit.SECONDS);
        Duration duration = Duration.between(now, plus);

        assertThat(duration.getSeconds()).isEqualTo(10);
    }

    @Test
    void parsing하는법() {
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        System.out.println("LocalDate.now().format(dateTimeFormatter) = " + LocalDate.now().format(dateTimeFormatter));
        //LocalDate.now().format(dateTimeFormatter) = 2023-03-16

        LocalDate parsed = LocalDate.parse("1998-08-30", dateTimeFormatter);
        System.out.println("parsed = " + parsed);   //parsed = 1998-08-30 (localDate)
    }

    @Test
    void legacyAPI지원() {
        ZoneId pst = TimeZone.getTimeZone("PST").toZoneId();
        System.out.println("pst = " + pst);     //America/Los_Angeles

        TimeZone legacyZoneAPI = TimeZone.getTimeZone(ZoneId.of("Asia/Seoul"));
        System.out.println("legacyZoneAPI = " + legacyZoneAPI);
        //legacyZoneAPI = sun.util.calendar.ZoneInfo[id="Asia/Seoul",offset=32400000,dstSavings=0,useDaylight=false,transitions=30,lastRule=null]

        Instant newInstant = new Date().toInstant();
        System.out.println("newInstant = " + newInstant);   //newInstant = 2023-03-16T07:09:46.767Z

        Date legacyInstant = Date.from(newInstant);
        System.out.println("legacyInstant = " + legacyInstant); //legacyInstant = Thu Mar 16 16:09:46 KST 2023
    }
}

3. 참고

Last updated