본문 바로가기

Java

[Java] 날짜와 시간

날짜와 시간 라이브러리가 필요한 이유

날짜와 시간을 계산하는 것은 단순하게 생각하면 쉬워보이지만, 실제로는 매우 복잡하고 어렵습니다.

그렇다면 왜 어려울까요?

  • 날짜와 시간 차이 계산
  • 윤년 계산
  • 일광 절약 시간(Daylight Saving Time, DST) 변환
  • 타임존 계산

 

자바 날짜와 시간 라이브러리의 역사

자바는 날짜와 시간 라이브러리를 지속해서 업데이트 했습니다.

 

JDK 1.0 (java.util.Date)

 

문제점

  • 타임존 처리 부족: 초기 Date 클래스는 타임존(time zone)을 제대로 처리하지 못했다.
  • 불편한 날짜 시간 연산: 날짜 간 연산이나 시간의 증감 등을 처리하기 어려웠다.
  • 불변 객체 부재: Date 객체는 변경 가능(mutable)하여, 데이터가 쉽게 변경될 수 있었고 이로 인해 버그
    가 발생하기 쉬웠다.

해결책

  • JDK 1.1에서 java.util.Calendar 클래스 도입으로 타임존 지원 개선.
  • 날짜 시간 연산을 위한 추가 메소드 제공.


JDK 1.1 (java.util.Calendar)

 

문제점

  • 사용성 저하: Calendar 는 사용하기 복잡하고 직관적이지 않았다.
  • 성능 문제: 일부 사용 사례에서 성능이 저하되는 문제가 있었다.
  • 불변 객체 부재: Calendar 객체도 변경 가능하여, 사이드 이펙트, 스레드 안전성 문제가 있었다.

해결책

  • Joda-Time 오픈소스 라이브러리의 도입으로 사용성, 성능, 불변성 문제 해결.

Joda-Time

 

문제점

  • 표준 라이브러리가 아님: Joda-Time은 외부 라이브러리
  • 자바 표준에 포함되지 않아 프로젝트에 별도로 추가해야 했다.

해결책

  • 자바 8에서 java.time 패키지(JSR-310)를 표준 API로 도입.


JDK 8(1.8) (java.time 패키지)

 

  • java.time 패키지는 이전 API의 문제점을 해결하면서, 사용성, 성능, 스레드 안전성, 타임존 처리 등에서 크게
    개선되었다. 
  • 변경 불가능한 불변 객체 설계로 사이드 이펙트, 스레드 안전성 보장, 보다 직관적인 API 제공으로 날짜와 시간 연산을 단순화했다.
  • LocalDate , LocalTime , LocalDateTime , ZonedDateTime , Instant 등의 클래스를 포함한다.
  • Joda-Time의 많은 기능을 표준 자바 플랫폼으로 가져왔다.

 

 

자바 날짜와 시간 라이브러리 소개

 

자바 날짜와 시간 라이브러리는 자바 공식 문서가 제공하는 다음 표 하나로 정리할 수 있습니다.

 

Overview (The Java™ Tutorials > Date Time > Standard Calendar)

The Java Tutorials have been written for JDK 8. Examples and practices described in this page don't take advantage of improvements introduced in later releases and might use technology no longer available. See Java Language Changes for a summary of updated

docs.oracle.com

 

LocalDate, LocalTime, LocalDateTime

  • LocalDate: 날짜만 표현할 때 사용한다. 년, 월, 일을 다룬다. 예) 2013-11-21
  • LocalTime: 시간만을 표현할 때 사용한다. 시, 분, 초를 다룬다. 예) 08:20:30.213
  • 초는 밀리초, 나노초 단위도 포함할 수 있다.
  • LocalDateTime: LocalDate 와 LocalTime 을 합한 개념이다. 예) 2013-11-21T08:20:30.213

앞에 Local (현지의, 특정 지역의)이 붙는 이유는 세계 시간대를 고려하지 않아서 타임존이 적용되지 않기 때문입니다. 
특정 지역의 날짜와 시간만 고려할 때 사용합니다.

 

기본 날짜와 시간 - LocalDate

import java.time.LocalDate;

public class LocalDateMain {

    public static void main(String[] args) {
        LocalDate nowDate = LocalDate.now();
        LocalDate ofDate = LocalDate.of(2013, 11, 21);
        System.out.println("오늘 날짜: " + nowDate);
        System.out.println("지정 날짜: " + ofDate);
        // 계산(불변)
        LocalDate plusDays = ofDate.plusDays(10);
        System.out.println("지정 날짜+10d: " + plusDays);
    }
}
오늘 날짜: 2024-04-12
지정 날짜: 2013-11-21
지정 날짜+10d: 2013-12-01

 

생성

  • now( ): 현재 시간을 기준으로 생성한다.
  • of(...): 특정 날짜를 기준으로 생성한다. (년,월,일을 입력할 수 있다.)

계산

  • plusDays( ): 특정 일을 더한다. 다양한 plusXxx( ) 메서드가 존재한다.

불변

  • 모든 날짜 클래스는 불변이다.
  • 따라서 변경이 발생하는 경우 새로운 객체를 생성해서 반환하므로 반환값을 꼭 받아야 한다.

 

LocalTime

import java.time.LocalTime;

public class LocalTimeMain {

    public static void main(String[] args) {
        LocalTime nowTime = LocalTime.now();
        LocalTime ofTime = LocalTime.of(9, 10, 30);
        System.out.println("현재 시간: " + nowTime);
        System.out.println("지정 시간: " + ofTime);
        // 계산(불변)
        LocalTime ofTimePlus = ofTime.plusSeconds(30);
        System.out.println("지정 시간+30s: " + ofTimePlus);
    }
}
현재 시간: 19:22:55.750747400
지정 시간: 09:10:30
지정 시간+30s: 09:11

 

 

LocalDateTime

LocalDateTime 은 LocalDate 와 LocalTime 을 내부에 가지고 날짜와 시간을 모두 표현한다

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;

public class LocalDateTimeMain {

    public static void main(String[] args) {
        LocalDateTime nowDt = LocalDateTime.now();
        LocalDateTime ofDt = LocalDateTime.of(2016, 8, 16, 8, 10, 1);
        System.out.println("현재 날짜 시간: " + nowDt);
        System.out.println("지정 날짜 시간: " + ofDt);

        // 날짜와 시간 분리
        LocalDate localDate = ofDt.toLocalDate();
        LocalTime localTime = ofDt.toLocalTime();
        System.out.println("localDate = " + localDate);
        System.out.println("localTime = " + localTime);

        // 날짜와 시간 합체
        LocalDateTime localDateTime = LocalDateTime.of(localDate, localTime);
        System.out.println("localDateTime = " + localDateTime);

        // 계산(불변)
        LocalDateTime ofDtPlus = ofDt.plusDays(1000);
        System.out.println("지정 날짜+1000d: " + ofDtPlus);
        LocalDateTime ofDtPlus1Year = ofDt.plusYears(1);
        System.out.println("지정 날짜+1년: " + ofDtPlus1Year);

        // 비교
        System.out.println("현재 날짜시간이 지정 날짜시간보다 이전인가?" + nowDt.isBefore(ofDt));
        System.out.println("현재 날짜시간이 지정 날짜시간보다 이후인가?" + nowDt.isAfter(ofDt));
        System.out.println("현재 날짜시간과 지정 날짜시간이 같은가?" + nowDt.isEqual(ofDt));
    }
}
현재 날짜 시간: 2024-04-12T19:33:16.977548200
지정 날짜 시간: 2016-08-16T08:10:01
localDate = 2016-08-16
localTime = 08:10:01
localDateTime = 2016-08-16T08:10:01
지정 날짜+1000d: 2019-05-13T08:10:01
지정 날짜+1년: 2017-08-16T08:10:01
현재 날짜시간이 지정 날짜시간보다 이전인가? false
현재 날짜시간이 지정 날짜시간보다 이후인가? true
현재 날짜시간과 지정 날짜시간이 같은가? false

생성

  • now() : 현재 날짜와 시간을 기준으로 생성한다.
  • of(...) : 특정 날짜와 시간을 기준으로 생성한다.

분리

  • 날짜( LocalDate )와 시간( LocalTime )을 toXxx() 메서드로 분리할 수 있다.

합체

  • LocalDateTime.of(localDate, localTime)
  • 날짜와 시간을 사용해서 LocalDateTime 을 만든다.

계산

  • plusXxx() : 특정 날짜와 시간을 더한다. 다양한 plusXxx() 메서드가 존재한다.

비교

  • isBefore(): 다른 날짜시간과 비교한다. 현재 날짜와 시간이 이전이라면 true 를 반환한다.
  • isAfter(): 다른 날짜시간과 비교한다. 현재 날짜와 시간이 이후라면 true 를 반환한다.
  • isEquals(): 다른 날짜시간과 시간적으로 동일한지 비교한다. 시간이 같으면 true 를 반환한다.

isEquals( ) vs equals( )

  • isEquals( ) 는 단순히 비교 대상이 시간적으로 같으면 true 를 반환한다. 객체가 다르고, 타임존이 달라도 시
    간적으로 같으면 true 를 반환한다. 쉽게 이야기해서 시간을 계산해서 시간으로만 둘을 비교한다.
    예) 서울의 9시와 UTC의 0시는 시간적으로 같다. 이 둘을 비교하면 true 를 반환한다.
  • equals( ) 객체의 타입, 타임존 등등 내부 데이터의 모든 구성요소가 같아야 true 를 반환한다.
    예) 서울의 9시와 UTC의 0시는 시간적으로 같다. 이 둘을 비교하면 타임존의 데이터가 다르기 때문에
    false 를 반환한다.

 

 

ZonedDateTime, OffsetDateTime

  • ZonedDateTime: 시간대를 고려한 날짜와 시간을 표현할 때 사용한다. 여기에는 시간대를 표현하는 타임존이
    포함된다.
  • OffsetDateTime: 시간대를 고려한 날짜와 시간을 표현할 때 사용한다. 여기에는 타임존은 없고, UTC로 부터
    의 시간대 차이인 고정된 오프셋만 포함한다.

 

Zoneld

자바는 타임존을 ZoneId 클래스로 제공하고 있습니다.

import java.time.ZoneId;

public class ZoneIdMain {

    public static void main(String[] args) {
        for (String availableZoneIds : ZoneId.getAvailableZoneIds()) {
            //System.out.println("availableZoneIds = " + availableZoneIds);
            //ZoneId zoneId = ZoneId.of("Asia/Seoul");
            ZoneId zoneId = ZoneId.of(availableZoneIds);
            System.out.println(zoneId + " | " + zoneId.getRules());
        }

        ZoneId zoneId = ZoneId.systemDefault();
        System.out.println("zoneId.systemDefault = " + zoneId);

        ZoneId seoulZoneId = ZoneId.of("Asia/Seoul");
        System.out.println("seoulZoneId = " + seoulZoneId);
    }
}
Asia/Aden | ZoneRules[currentStandardOffset=+03:00]
America/Cuiaba | ZoneRules[currentStandardOffset=-04:00]
Etc/GMT+9 | ZoneRules[currentStandardOffset=-09:00]
Etc/GMT+8 | ZoneRules[currentStandardOffset=-08:00]
Africa/Nairobi | ZoneRules[currentStandardOffset=+03:00]
America/Marigot | ZoneRules[currentStandardOffset=-04:00]
...
zoneId.systemDefault = Asia/Seoul
seoulZoneId = Asia/Seoul

 

생성

  • ZoneId.systemDefault( ): 시스템이 사용하는 기본 ZoneId를 반환한다.
  • ZoneId.of( ): 타임존을 직접 제공해서 ZoneId를 반환한다.

ZoneId는 내부에 일광 절약 시간 관련 정보, UTC와의 오프셋 정보를 포함하고 있습니다.

 

 

ZonedDateTime

 

ZonedDateTime은 LocalDateTime 에 시간대 정보인 ZoneId 가 합쳐진 것입니다.

public class ZonedDateTime {
 private final LocalDateTime dateTime;
 private final ZoneOffset offset;
 private final ZoneId zone;
}

 

 

ZonedDateTIme: 시간대를 고려한 날짜와 시간을 표현할 때 사용합니다. 여기에 시간대를 표현하는 타임존이 포함됩니다.

  • 예) 2013-11-21T08:20:30.213+9:00[Asia/Seoul]
  • +9:00은 UTC(협정 세계시)로 부터의 시간대 차이이다. 오프셋이라고 한다. 
  • Asia/Seoul은 타임존이라고 한다. 타임존을 알면 오프셋도 알 수 있다. +9:00 같은 오프셋 정보도 타임존에 포함된다.
  • 추가로 ZoneId를 통해 타임존을 알면 일광 절약 시간제에 대한 정보도 알 수 있다. 

 

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;

public class ZonedDateTimeMain {

    public static void main(String[] args) {
        ZonedDateTime nowZdt = ZonedDateTime.now();
        System.out.println("nowZdt = " + nowZdt);

        LocalDateTime ldt = LocalDateTime.of(2030, 1, 1, 13, 30, 50);
        ZonedDateTime zdt1 = ZonedDateTime.of(ldt, ZoneId.of("Asia/Seoul"));
        System.out.println("zdt1 = " + zdt1);

        ZonedDateTime zdt2 = ZonedDateTime.of(2030, 1, 1, 13, 30, 50, 0, ZoneId.of("Asia/Seoul"));
        System.out.println("zdt2 = " + zdt2);

        ZonedDateTime utcZdt = zdt2.withZoneSameInstant(ZoneId.of("UTC"));
        System.out.println("utcZdt = " + utcZdt);
    }
}
nowZdt = 2024-04-15T16:38:48.957650600+09:00[Asia/Seoul]
zdt1 = 2030-01-01T13:30:50+09:00[Asia/Seoul]
zdt2 = 2030-01-01T13:30:50+09:00[Asia/Seoul]
utcZdt = 2030-01-01T04:30:50Z[UTC]

 

생성

  • now( ): 현재 날짜와 시간을 기준으로 생성한다. 이 때 ZoneId는 현재 시스템을 따른다.(ZoneId.systemDefault( ))
  • of( ): 특정 날짜와 시간을 기준으로 생성한다. ZoneId를 추가해야 한다.

 

타임존 변경

  • withZoneSameInstant(ZoneId): 타임존을 변경한다. 타임존에 맞추어 시간도 함께 변경된다. 이 메서드를 사용하면 지금 다른 나라는 몇 시 인지 확인할 수 있다.

 

OffsetDateTime

OffsetDateTime은 LocalDateTime에 UTC 오프셋 정보인 ZoneOffset 이 합쳐진 것입니다.

public class OffsetDateTime {
 private final LocalDateTime dateTime;
 private final ZoneOffset offset;
}

 

OffsetDateTime: 시간대를 고려한 날짜와 시간을 표현할 때 사용한다. 여기에는 타임존은 없고, UTC로 부터의 시간대 차이인 고정된 오프셋만 포함된다.

  • 에) 2013-11-21T08:20:30.213+9:00
  • ZoneId가 없으므로 일광 절약 시간제가 적용되지 않는다.
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;

public class OffsetDateTimeMain {

    public static void main(String[] args) {
        OffsetDateTime nowOdt = OffsetDateTime.now();
        System.out.println("nowOdt = " + nowOdt);

        LocalDateTime ldt = LocalDateTime.of(2030, 1, 1, 13, 30, 50);
        System.out.println("ldt = " + ldt);
        OffsetDateTime odt = OffsetDateTime.of(ldt, ZoneOffset.of("+01:00"));
        System.out.println("odt = " + odt);

    }
}
nowOdt = 2024-04-15T16:49:40.271583500+09:00
ldt = 2030-01-01T13:30:50
odt = 2030-01-01T13:30:50+01:00

 

ZoneOffset은 +01:00 처럼 UTC와의 시간 차이인 오프셋 정보만 보관합니다.

 

ZonedDateTime vs OffsetDateTime

  • ZonedDateTime은 구체적인 지역 시간대를 다룰 때 사용하며, 일광 절약 시간을 자동으로 처리할 수 있다. 사용자 지정 시간대에 따른 시간 계산이 필요할 때 적합하다.
  • OffsetDateTime은 UTC와의 시간 차이만을 나타날 때 사용하며, 지역 시간대의 복잡성을 고려하지 않는다. 시간대 변환 없이 로그를 기록하고, 데이터를 저장하고 처리할 때 적합하다.

 

Year, Month, YearMonth, MonthDay


년, 월, 년월, 달일을 각각 다룰 때 사용한다. 자주 사용하지는 않습니다.
DayOfWeek 와 같이 월, 화, 수, 목, 금, 토, 일을 나타내는 클래스도 있습니다

 

Instant


Instant 는 UTC(협정 세계시)를 기준으로 하는, 시간의 한 지점을 나타냅니다. 

Instant 는 날짜와 시간을 나노초 정밀도로 표현하며, 1970년 1월 1일 0시 0분 0초(UTC)를 기준으로

경과한 시간으로 계산됩니다. 쉽게 이야기해서 Instant 내부에는 초 데이터만 들어있습니다. (나노초 포함)
따라서 날짜와 시간을 계산에 사용할 때는 적합하지 않습니다. 

public class Instant {
 private final long seconds;
 private final int nanos;
 ...
}
  • UTC 기준 1970년 1월 1일 0시 0분 0초라면 seconds 에 0이 들어간다.
  • UTC 기준 1970년 1월 1일 0시 0분 10초라면 seconds 에 10이 들어간다.
  • UTC 기준 1970년 1월 1일 0시 1분 0초라면 seconds 에 60이 들어간다.

 

Instant 특징

 

장점

  • 시간대 독립성: Instant는 UTC를 기준으로 하므로, 시간대에 영향을 받지 않는다. 이는 전 세계 어디서나 동일한 시점을 가리키는데 유용하다.
  • 고정된 기준점: 모든 Instant는 1970년 1월 1일 UTC를 기준으로 하기 때문에, 시간 계산 및 비교가 명확하고 일관된다.

단점

  • 사용자 친화적이지 않음: Instant는 기계적인 시간 처리에는 적합하지만, 사람이 읽고 이해하기에는 직관적이지 않다.
  • 시간대 정보 부재: Instant에는 시간대 정보가 포함되지 않아, 특정 지역의 날짜와 시간으로 변환하려면 추가적인 작업이 필요하다.

사용 예

  • 전 세계적인 시간 기준 필요 시 (로그 기록, 트랜잭션 타임스탬프, 서버간의 시간 동기화 등)
  • 시간대 변환 없이 시간 계산 필요 시 
  • 데이터 저장 및 교환 (데이터의 일관성 유지)
import java.time.Instant;
import java.time.ZonedDateTime;

public class InstantMain {

    public static void main(String[] args) {
        // 생성
        Instant now = Instant.now();
        System.out.println("now = " + now);

        ZonedDateTime zdt = ZonedDateTime.now();
        Instant from = Instant.from(zdt);
        System.out.println("from = " + from);

        Instant epochStart = Instant.ofEpochSecond(0);
        System.out.println("epochStart = " + epochStart);

        // 계산
        Instant later = epochStart.plusSeconds(3600);
        System.out.println("later = " + later);

        // 조회
        long laterEpochSecond = later.getEpochSecond();
        System.out.println("laterEpochSecond = " + laterEpochSecond);
    }
}
now = 2024-04-15T11:14:13.040083800Z
from = 2024-04-15T11:14:13.067475300Z
epochStart = 1970-01-01T00:00:00Z
later = 1970-01-01T01:00:00Z
laterEpochSecond = 3600

 

생성

  • now( ): UTC를 기준 현재 시간의 Instant를 생성한다.
  • from( ): 다른 타입의 날짜와 시간을 기준으로 Instant를 생성한다. 참고로 Instant는 UTC를 기준으로 하기 때문에 시간대 정보가 필요하다. 따라서 LocalDateTime은 사용할 수 없다.
  • ofEpochSecond( ): 에포크 시간을 기준으로 Instant를 생성한다. 0초로 선택하면 에포크 시간인 1970년 1월 1일 0시 0분 0초로 생성된다

계산

  • plusSeconds( ): 초를 더한다.

조회

  • getEpochSecond( ): 에포크 시간인 UTC를 기준으로 흐른 초를 반환한다.
  • 여기서는 앞서 에포크 시간에 3600초를 더했기 때문에 3600이 반환된다.

 

Period, Duration

 

시간의 개념은 크게 2가지로 표현할 수 있습니다.

 

특정 시점의 시간(시각)

  • 이 프로젝트는 2013년 8월 16일 까지 완료해야해
  • 다음 회의는 11시 30분에 진행한다.
  • 내 생일은 8월 16일이야.

시간의 간격(기간)

  • 앞으로 4년은 더 공부해야 해
  • 이 프로젝트는 3개월 남았어
  • 라면은 3분 동안 끓어야 해

Period , Duration 은 시간의 간격(기간)을 표현하는데 사용됩니다.
시간의 간격은 영어로 amount of time(시간의 양)으로 불립니다.

 

Period

두 날짜 사이의 간격을 년, 월, 일 단위로 나타냅니다.

public class Period {
 private final int years;
 private final int months;
 private final int days;
}
import java.time.LocalDate;
import java.time.Period;

public class PeriodMain {

    public static void main(String[] args) {
        // 생성
        Period period = Period.ofDays(10);
        System.out.println("period = " + period);

        // 계산에 사용
        LocalDate currentDate = LocalDate.of(2030, 1, 1);
        LocalDate plusDate = currentDate.plus(period);
        System.out.println("currentDate = " + currentDate);
        System.out.println("plusDate = " + plusDate);

        // 기간 차이
        LocalDate startDate = LocalDate.of(2023, 1, 1);
        LocalDate endDate = LocalDate.of(2023, 4, 2);
        Period between = Period.between(startDate, endDate);
        System.out.println("between = " + between);
        System.out.println("기간: " + between.getMonths() + "개월 " + between.getDays() + "일");
    }
}
period = P10D
currentDate = 2030-01-01
plusDate = 2030-01-11
between = P3M1D
기간: 3개월 1일

 

생성

  • of( ): 특정 기간을 지정해서 Period를 생성한다.
  • of(년, 월, 일)
  • ofDays( )
  • ofMonths( )
  • ofYears( )

계산에 사용

  • 2030년 1월 1일에 10일을 더하면 2030년 1월 11일이 된다. 라고 표현할 때 특정 날짜에 10일이라는 기간을 더할 수 있다.

기간 차이

  • 2023년 1월 1일과 2023년 4월 2일 간의 차이는 3개월 1일이다. 라고 표현할 때 특정 날짜의 차이를 구하면 기간이 된다.
  • Period.between(startDate, endDate)와 같이 특정 날짜의 차이를 구하면 Period가 반환된다.

 

Duration

두 시간 사이의 간격을 시, 분, 초(나노초) 단위로 나타냅니다.

public class Duration {
 private final long seconds;
 private final int nanos;
}

 

 

내부에서 초를 기반으로 시, 분, 초를 계산해서 사용한다.

  • 1분 = 60초
  • 1시간 = 3600초
import java.time.Duration;
import java.time.LocalTime;

public class DurationMain {

    public static void main(String[] args) {
        Duration duration = Duration.ofMinutes(30);
        System.out.println("duration = " + duration);

        LocalTime lt = LocalTime.of(1, 0);
        System.out.println("lt = " + lt);

        // 계산에 사용
        LocalTime plusTime = lt.plus(duration);
        System.out.println("더한 시간: " + plusTime);
        
        // 시간 차이
        LocalTime start = LocalTime.of(9, 0);
        LocalTime end = LocalTime.of(10, 0);
        Duration between = Duration.between(start, end);
        System.out.println("between = " + between);
        System.out.println("차이: " + between.getSeconds() + "초");
        System.out.println("근무 시간: " + between.toHours() + "시간" + between.toMinutesPart() + "분");
        System.out.println("근무 시간: " + between.toHours() + "시간 or " + between.toMinutes() + "분");
    }
}
duration = PT30M
lt = 01:00
더한 시간: 01:30
between = PT1H
차이: 3600초
근무 시간: 1시간0분
근무 시간: 1시간 or 60분

 

생성

  • of( ): 특정 시간을 지정해서 Period를 생성한다.
  • of(지정):
  • ofSeconds( ):
  • ofMinutes( ):
  • ofHours( ):

계산에 사용

  • 1:00에 30분을 더하면 1:30이 된다. 라고 표현할 때 특정 시간에 30분이라는 시간(시간의 간격)을 더할 수 있다.

시간 차이

  • 9시와 10시의 차이는 1시간이라고 표현할 때 시간의 차이를 구할 수 있다.
  • Durantion(start, end) 와 같이 특정 시간의 차이를 구하면 Duration이 반환된다.