JAVA의 가장 기본이 되는 내용

날짜와 시간

cwchoiit 2024. 4. 4. 15:08
728x90
반응형
SMALL

참고 자료:

 

김영한의 실전 자바 - 중급 1편 | 김영한 - 인프런

김영한 | 실무에 필요한 자바의 다양한 중급 기능을 예제 코드로 깊이있게 학습합니다., [사진]국내 개발 분야 누적 수강생 1위, 제대로 만든 김영한의 실전 자바[사진][임베딩 영상]단순히 자바

www.inflearn.com

 

날짜와 시간에 대한 작업은 굉장히 아주 많이 어렵고 까다롭다. 왜냐하면 고려할 사항이 무진장 많기 때문인데 윤년, 각 달의 일수, 일광 절약 시간(DST), 타임존(GMT, UTC, ...) 등 고려할 사항이 너무 많다. 그래서 그냥 개발자가 직접 날짜와 시간을 계산하면 99.9%의 확률로 잘못된 계산이 될 것이다.

 

그래서 이러한 복잡성 때문에 대부분의 현대 개발 환경에서는 날짜와 시간을 처리하기 위해 잘 설계된 라이브러리를 사용해야 한다. 자바의 날짜와 시간 라이브러리는 java.time 패키지다.

이 표가 자바가 제공하는 날짜와 시간 라이브러리의 축약버전이다. 다양한 클래스들이 있고 그 클래스가 표현하는 단위와 방식이 어떻게 되는지가 자세히 나와있다. 하나씩 자세히 알아보자.

 

LocalDate, LocalTime, LocalDateTime

LocalDate: 날짜만 표현할 때 사용한다. 년월일을 다룬다. 예) 2024-04-04

LocalTime: 시간만을 표현할 때 사용한다. 시분초를 다룬다. 예) 09:20:30.213

LocalDateTime: LocalDate와 LocalTime을 합한 개념이다. 예) 2024-04-04T09:20:30.213

 

앞에 Local(현지의)이 붙는 이유는 세계 시간대를 고려하지 않아서 타임존이 적용되지 않기 때문이다. 특정 지역의 날짜와 시간만 고려할 때 사용한다. 예) 국내 서비스만 고려하는 애플리케이션

 

ZonedDateTime, OffsetDateTime

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

  • 예) 2024-04-04T09:20:30.213+9:00[Asia/Seoul]
  • +9:00은 UTC(협정 세계시)로 부터의 시간대 차이이다. 오프셋이라고 하고 +0900으로도 잘 표현된다. 한국은 UTC보다 +9:00 시간이다.
  • Asia/Seoul을 타임존이라고 한다. 이 타임존을 알면 오프셋과 일광 절약 시간제에 대한 정보를 알 수 있다.
  • 일광 절약 시간제가 적용된다.

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

  • 예) 2024-04-04T09:20:30.213+9:00
  • 일광 절약 시간제가 적용되지 않는다.

 

Asia/Seoul 같은 타임존 안에는 일광 절약 시간제에 대한 정보와 UTC+9:00와 같은 UTC로부터 시간 차이인 오프셋 정보를 모두 포함하고 있다. 일광 절약 시간제(DST, 썸머타임)를 알려면 타임존을 알아야 한다. 따라서 ZonedDateTime은 일광 절약 시간제를 함께 처리한다. 반면에 타임존을 알 수 없는 OffsetDateTime은 일광 절약 시간제를 처리하지 못한다. 

 

Year, Month, YearMonth, MonthDay

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

 

Instant

Instant는 UTC(협정 세계시)를 기준으로 하는, 시간의 한 지점을 나타낸다. Instant는 날짜와 시간을 나노초 정밀도로 표현하며, 1970년 1월 1일 0시 0분 0초(UTC)를 기준으로 경과한 시간으로 계산된다. 쉽게 이야기해서 Instant 내부에는 초 데이터만 들어있다.

 

Period, Duration

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

  • 특정 시점의 시간(시각)
    • 이 프로젝트는 2024년 4월 20일까지 완료해야해
    • 다음 회의는 10시에 진행한다
  • 시간의 간격(기간)
    • 이 프로젝트는 3개월 남았어
    • 라면은 3분동안 끓어야 해

Period, Duration은 시간의 간격(기간)을 표현하는데 사용된다.

 

Period

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

 

Duration

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

 

 

LocalDate, LocalTime, LocalDateTime 생성

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

LocalDate

생성

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

계산

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

LocalTime

생성

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

계산

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

LocalDateTime

생성

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

분리

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

합체

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

계산

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

비교

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

isEquals() vs equals()

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

 

ZonedDateTime

"Asia/Seoul" 같은 타임존 안에는 일광 절약 시간제에 대한 정보와 UTC+9:00와 같은 UTC로부터 시간 차이인 오프셋 정보를 모두 포함하고 있다. 자바는 타임존을 ZondId 클래스로 제공한다.

public class ZonedDateTimeMain {
    public static void main(String[] args) {
        Set<String> availableZoneIds = ZoneId.getAvailableZoneIds();
        for (String availableZoneId : availableZoneIds) {
            System.out.println("availableZoneId = " + availableZoneId);
        }

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

 

실행결과:

....
availableZoneId = America/Winnipeg
availableZoneId = Europe/Vatican
availableZoneId = Asia/Amman
availableZoneId = Etc/UTC
availableZoneId = SystemV/AST4ADT
availableZoneId = Asia/Tokyo
availableZoneId = America/Toronto
availableZoneId = Asia/Singapore
availableZoneId = Australia/Lindeman
availableZoneId = America/Los_Angeles
availableZoneId = SystemV/EST5EDT
availableZoneId = Pacific/Majuro
availableZoneId = America/Argentina/Buenos_Aires
availableZoneId = Europe/Nicosia
availableZoneId = Pacific/Guadalcanal
availableZoneId = Europe/Athens
availableZoneId = US/Pacific
availableZoneId = Europe/Monaco
SystemDefaultZoneId = Asia/Seoul
zoneId.getRules() = ZoneRules[currentStandardOffset=+09:00]

 

ZoneId.systemDefault() 메서드는 시스템이 사용하는 기본 ZoneId를 반환한다. 현재 내 PC의 ZoneId는 Asia/Seoul이다.

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

 

ZonedDateTime

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

ZonedDateTime 클래스

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

 

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

예) 2013-11-21T18:20:30.213+9:00[Asia/Seoul]

+9:00는 UTC(협정 세계시)로 부터의 시간대 차이이다. 오프셋이라고 한다.

ZoneId를 통해 타임존을 알면 일광 절약 시간제에 대한 정보도 알 수 있다. 따라서 일광 절약 시간제가 적용된다.

 

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

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

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

        ZonedDateTime utc = zdt3.withZoneSameInstant(ZoneId.of("UTC"));
        System.out.println("utc = " + utc);
    }
}

실행결과:

zdt = 2024-04-04T16:52:47.055743+09:00[Asia/Seoul]
zdt2 = 2030-01-01T13:30:50+09:00[Asia/Seoul]
zdt3 = 2030-01-01T13:30:50+09:00[Asia/Seoul]
utc = 2030-01-01T04:30:50Z[UTC]

 

위 코드에서 보면 생성하는 방법이 역시 2가지다. now(), of(...).

 

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

 

OffsetDateTime

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

 

OffsetDateTime 클래스

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

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

예) 2013-11-21T08:20:30.213+9:00

ZoneId가 없으므로 일광 절약 시간제가 적용되지 않는다.

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

        LocalDateTime localDateTime = LocalDateTime.of(2023, 1, 1, 13, 30, 50);
        System.out.println("localDateTime = " + localDateTime);

        OffsetDateTime offsetDateTime = OffsetDateTime.of(localDateTime, ZoneOffset.of("+01:00"));
        System.out.println("offsetDateTime = " + offsetDateTime);
    }
}

 

실행 결과:

now = 2024-04-04T19:54:45.743425+09:00
localDateTime = 2023-01-01T13:30:50
offsetDateTime = 2023-01-01T13:30:50+01:00

 

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

 

ZoneDateTime vs OffsetDateTime

- ZoneDateTime은 구체적인 지역 시간대를 다룰 때 사용하며, 일광 절약 시간을 자동으로 처리할 수 있다. 사용자 지정 시간대에 따른 시간 계산이 필요할 때 적합하다.

- OffsetDateTime은 UTC와의 시간 차이만을 나타낼 때 사용하며, 지역 시간대의 복잡성을 고려하지 않는다. 시간대 변환 없이 로그를 기록하고, 데이터를 저장하고 처리할 때 적합하다.

 

참고로, 이 두가지는 글로벌 서비스를 하지 않으면 잘 사용하지 않는다.

 

Instant (기계 중심의 시간)

Instant는 UTC(협정 세계시)를 기준으로 하는, 시간의 한 지점을 나타낸다. Instant는 날짜와 시간을 나노초 정밀도로 표현하며, 1970년 1월 1일 0시 0분 0초(UTC 기준)를 기준으로 경과한 시간으로 계산된다. 쉽게 이야기해서 Instant 내부에는 초 데이터만 들어있다. (나노초 포함). 따라서 날짜와 시간을 계산에 사용할 때는 적합하지 않다.

 

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이 들어간다. 

참고 - Epoch 시간

Epoch time 또는 Unix timestamp는 컴퓨터 시스템에서 시간을 나타내는 방법 중 하나이다. 이는 1970년 1월 1일 00:00:00 UTC부터 현재까지 경과된 시간을 초 단위로 표현한 것이다. 즉, Unix 시간은 1970년 1월 1일 이후로 경과한 전체 초의 수로, 시간대에 영향을 받지 않는 절대적인 시간 표현 방식이다. 참고로 Epoch라는 뜻은 어떤 중요한 사건이 발생한 시점을 기준으로 삼는 시작점을 뜻하는 용어다. Instant는 바로 이 Epoch 시간을 다루는 클래스이다.

 

Instant 특징

  • 장점
    • 시간대 독립성: Instant는 UTC를 기준으로 하므로, 시간대에 영향을 받지 않는다. 이는 전 세계 어디서나 동일한 시점을 가리키는데 유용하다.
    • 고정된 기준점: 모든 Instant는 1970년 1월 1일 0시 0분 0초 UTC를 기준으로 하기 때문에 시간 계산 및 비교가 명확하고 일관된다.
  • 단점
    • 사용자 친화적이지 않음: Instant는 기계적인 시간 처리에는 적합하지만, 사람이 읽고 이해하기에는 직관적이지 않다. 예를 들어, 날짜와 시간을 계산하고 사용하는데 필요한 기능이 부족하다.
    • 시간대 정보 부재: Instant에는 시간대 정보가 포함되어 있지 않아, 특정 지역의 날짜와 시간으로 변환하려면 추가적인 작업이 필요하다.

사용 예 (물론, 글로벌 서비스를 다룬다고 가정할 때 의미가 생긴다)

  • 전 세계적인 시간 기준 필요 시: Instant는 UTC를 기준으로 하므로, 전 세계적으로 일관된 시점을 표현할 때 사용하기 좋다. 예를 들어, 로그 기록이나, 트랜잭션 타임스탬프, 서버 간의 시간 동기화 등이 이에 해당한다.
  • 시간대 변환 없이 시간 계산 필요 시: 시간대의 변화 없이 순수하게 시간의 흐름(예: 지속 시간 계산)만을 다루고 싶을 때 Instant가 적합하다. 이는 시간대 변환의 복잡성 없이 시간 계산을 할 수 있게 해준다.
  • 데이터 저장 및 교환: 데이터베이스에 날짜와 시간 정보를 저장하거나, 다른 시스템과 날짜와 시간 정보를 교환할 때 Instant를 사용하면, 모든 시스템에서 동일한 기준점(UTC)을 사용하게 되므로 데이터의 일관성을 유지하기 쉽다.
public class InstantMain {
    public static void main(String[] args) {
        Instant now = Instant.now();
        System.out.println("now = " + now);

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

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

        // Epoch Time에서 0초 경과 후 시간
        Instant epochStart = Instant.ofEpochSecond(0);
        System.out.println("epochStart = " + epochStart);

        // Epoch Time에서 3600초(1시간) 경과 후 시간
        Instant later = epochStart.plusSeconds(3600);
        System.out.println("later = " + later);

        // Epoch Time 으로부터 얼마나 지났나요? 
        long laterEpochSecond = later.getEpochSecond();
        System.out.println("laterEpochSecond = " + laterEpochSecond);
    }
}

 

실행결과:

now = 2024-04-04T13:27:02.390809Z
zdt = 2024-04-04T22:27:02.413119+09:00[Asia/Seoul]
from = 2024-04-04T13:27:02.413119Z
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 1970년 1월 1일 0시 0분 0초를 기준으로 흐른 초를 반환한다.
  • 여기서는 앞서 에포크 시간에 3600초를 더했기 때문에 3600이 반환된다.

 

Instant 정리

  • 조금 특별한 시간, 기계 중심, UTC 기준
  • 에포크 시간으로부터 흐른 시간을 초 단위로 저장
  • 전세계 모든 서버 시간을 똑같이 맞출 수 있음. 항상 UTC 기준이므로 한국에 있는 Instant, 미국에 있는 Instant의 시간이 똑같음
  • 서버 로그, epoch 시간 기반 계산이 필요할 때, 간단히 두 시간의 차이를 구할 때
  • 단점: 초 단위의 간단한 연산 기능, 복잡한 연산 못함
  • 대안: 날짜 계산 필요하면 LocalDateTime 또는 ZonedDateTime 사용

 

기간, 시간의 간격 Duration, Period

위에서 Duration, Period의 차이를 얘기했었다. 둘 다 시간의 간격(기간)을 표현하는데 사용하는데 표현하는 방식이 다르다.

 

Period

두 날짜 사이의 간격을 년, 월, 일 단위로 나타낸다. (예: 이 프로젝트는 3개월 남았다)

 

Duration

두 시간 사이의 간격을 시, 분, 초(나노초)단위로 나타낸다 (예: 라면을 끓이는 시간은 3분이다)

 

Period

public class Period {
     private final int years;
     private final int months;
     private final int days;
}

다음과 같이 가지고 있는 필드가 년, 월, 일로 있다.

 

Period를 사용해서 원하는 기간만큼 날짜에 더하거나 뺄 수 있고, 두 날짜 사이의 기간도 구할 수 있다.

public static void main(String[] args) {

    Period period = Period.ofDays(10);
    System.out.println("period = " + period);

    LocalDate ld = LocalDate.of(2030, 1, 1);
    LocalDate plusDate = ld.plus(period);
    System.out.println("ld = " + ld);
    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.getMonths() + "개월" + between.getDays() + "일");
}

실행결과:

period = P10D
ld = 2030-01-01
plusDate = 2030-01-11
기간 = 3개월1일

 

 

Duration

두 시간 사이의 간격을 시, 분, 초(나노초)로 나타내고 다음과 같이 클래스가 구성되어 있다.

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

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

 

Duration을 사용해서 특정 시간에 시분초값을 더하거나 뺄 수 있으며, 두 시간 사이의 간격도 시분초로 나타낼 수 있다.

public static void main(String[] args) {

    Duration duration = Duration.ofMinutes(10);
    System.out.println("duration = " + duration);

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

    LocalTime plusLt = lt.plus(duration);
    System.out.println("더한 시간 = " + plusLt);

    LocalTime start = LocalTime.of(9, 0);
    LocalTime end = LocalTime.of(10, 0);
    Duration between = Duration.between(start, end);
    System.out.println("차이 = " + between.getSeconds() + "초");
    System.out.println("근무 시간 = " + between.toHours() + "시간 " + between.toMinutesPart() + "분");
}

실행결과:

duration = PT10M
기준 시간 = 01:00
더한 시간 = 01:10
차이 = 3600초
근무 시간 = 1시간 0분

 

 

날짜와 시간의 핵심 인터페이스

시간은 크게 두가지 분류를 할 수 있다.

  • 특정 시점의 시간
  • 시간의 간격

둘을 표현하기 위해서 사용한 클래스들은 다음과 같다.

  • 특정 시점의 시간: LocalDateTime, LocalDate, LocalTime, ZonedDateTime, OffsetDateTime
  • 시간의 간격: Duration, Period

벌써 느낌이 둘로 인터페이스가 나뉠 것 같고 인터페이스를 구현한 구현체가 저들일거란 생각이 든다. 그래서 명세를 제공하고 그 명세를 어떤방식으로 구현하던 사용하는 클라이언트는 변경을 할 필요가 없게끔 설계했을것만 같은 생각이 든다. 맞다.

 

 

TemporalAccessor

  • 날짜와 시간을 읽기 위한 기본 인터페이스
  • 이 인터페이스는 특정 시점의 날짜와 시간 정보를 읽을 수 있는 최소한의 기능을 제공한다.

Temporal

  • TemporalAccessor의 하위 인터페이스로, 날짜와 시간을 조작(추가, 빼기 등)하기 위한 기능을 제공한다. 이를 통해 날짜와 시간을 변경하거나 조정할 수 있다.

간단히 정리하자면, TemporalAccessor는 읽기 전용 접근을, Temporal은 읽기와 쓰기(조작) 모두를 지원한다.

 

 

여기에 추가적으로 알아야 할 인터페이스가 있다. 

 

시간의 단위시간 필드.

 

시간의 단위TemporalUnit이다.

근데 시간의 단위가 뭐야?

초, 분, 시간 단위를 말한다고 생각하면 된다. 코드로 보면 바로 이해가 될 것이다.

 

TemporalUnitMain

import java.time.temporal.ChronoUnit;

public class TemporalUnitMain {
    public static void main(String[] args) {

        ChronoUnit[] values = ChronoUnit.values();
        for (ChronoUnit value : values) {
            System.out.println("value = " + value);
        }
    }
}

코드를 보면, TemporalUnit을 구현한 ChronoUnit의 values() 메서드를 사용한다.

즉, ENUM 클래스란 이야기다. 찍어보면 다음과 같이 나온다.

실행결과:

value = Nanos
value = Micros
value = Millis
value = Seconds
value = Minutes
value = Hours
value = HalfDays
value = Days
value = Weeks
value = Months
value = Years
value = Decades
value = Centuries
value = Millennia
value = Eras
value = Forever

 

시간 단위(ChronoUnit)로 시간 계산하기

그래서 이 단위를 이용해서 원하는 시간 단위로 시간의 계산이 가능하다.

System.out.println("시간 단위 = " + ChronoUnit.HOURS);
System.out.println("1시간을 초 단위로 = " + ChronoUnit.HOURS.getDuration().getSeconds());

System.out.println("일 단위 = " + ChronoUnit.DAYS);
System.out.println("1일을 초 단위로 = " + ChronoUnit.DAYS.getDuration().getSeconds());

LocalTime lt1 = LocalTime.of(1, 10, 0);
LocalTime lt2 = LocalTime.of(1, 20, 0);

long secondsBetween = ChronoUnit.SECONDS.between(lt1, lt2);
System.out.println("두 시간의 차이를 초 단위로 = " + secondsBetween);

long millisBetween = ChronoUnit.MILLIS.between(lt1, lt2);
System.out.println("두 시간의 차이를 밀리초 단위로 = " + millisBetween);

long minBetween = ChronoUnit.MINUTES.between(lt1, lt2);
System.out.println("두 시간의 차이를 분 단위로 = " + minBetween);

실행결과:

시간 단위 = Hours
1시간을 초 단위로 = 3600
일 단위 = Days
1일을 초 단위로 = 86400
두 시간의 차이를 초 단위로 = 600
두 시간의 차이를 밀리초 단위로 = 600000
두 시간의 차이를 분 단위로 = 10

 

이런게 시간의 단위 TemporalUnit이란 인터페이스.

그리고 그 인터페이스를 구현한 ChronoUnit.

 

 

그럼 시간 필드는 진짜 뭐야?

이건 개념이 조금 헷갈리는데 코드를 보면 또 이해가 된다.

 

ChronoFieldMain

import java.time.temporal.ChronoField;

public class TemporalFieldMain {
    public static void main(String[] args) {

        ChronoField[] values = ChronoField.values();
        for (ChronoField value : values) {
            System.out.println("value = " + value);
        }
    }
}

이 또한, 열거형인 클래스 ChronoField.

찍어보면 다음과 같이 나온다.

실행 결과:

value = NanoOfSecond
value = NanoOfDay
value = MicroOfSecond
value = MicroOfDay
value = MilliOfSecond
value = MilliOfDay
value = SecondOfMinute
value = SecondOfDay
value = MinuteOfHour
value = MinuteOfDay
value = HourOfAmPm
value = ClockHourOfAmPm
value = HourOfDay
value = ClockHourOfDay
value = AmPmOfDay
value = DayOfWeek
value = AlignedDayOfWeekInMonth
value = AlignedDayOfWeekInYear
value = DayOfMonth
value = DayOfYear
value = EpochDay
value = AlignedWeekOfMonth
value = AlignedWeekOfYear
value = MonthOfYear
value = ProlepticMonth
value = YearOfEra
value = Year
value = Era
value = InstantSeconds
value = OffsetSeconds

 

그러니까 이게 필드로 제공이 되는데, DayOfMonth란 필드를 예시로 들면 그 달의 며칠을 말하는 것.

다음 코드를 보자.

System.out.println("일년의 개월 범위 " + ChronoField.MONTH_OF_YEAR.range());
System.out.println("한 달의 일 수 범위 " + ChronoField.DAY_OF_MONTH.range());

실행 결과:

일년의 개월 범위 1 - 12
한 달의 일 수 범위 1 - 28/31

이런게 필드라고 생각하면 된다. 이걸 어떻게 사용할 수 있을까?

 

ChangeTimeMain

public class ChangeTimeMain {
    public static void main(String[] args) {

        LocalDateTime localDateTime = LocalDateTime.of(2030, 1, 1, 13, 30, 59);
        System.out.println("시간 필드의 Year = " + localDateTime.get(ChronoField.YEAR));
        System.out.println("시간 필드의 연도를 기준으로 월 = " + localDateTime.get(ChronoField.MONTH_OF_YEAR));
        System.out.println("시간 필드의 달을 기준으로 일 = " + localDateTime.get(ChronoField.DAY_OF_MONTH));
        System.out.println("시간 필드의 하루를 기준으로 시간 = " + localDateTime.get(ChronoField.HOUR_OF_DAY));
        System.out.println("시간 필드의 시간을 기준으로 분 = " + localDateTime.get(ChronoField.MINUTE_OF_HOUR));
        System.out.println("시간 필드의 분을 기준으로 초 = " + localDateTime.get(ChronoField.SECOND_OF_MINUTE));

    }
}

예를 들면 초를 나타낼 때 100초, 1000초로도 표현이 가능하지만, 1시간에 정해진 초는 0초 ~ 59초 사이이다. 이걸 표현하고 싶을 때 사용한다.

실행결과:

시간 필드의 Year = 2030
시간 필드의 연도를 기준으로 월 = 1
시간 필드의 달을 기준으로 일 = 1
시간 필드의 하루를 기준으로 시간 = 13
시간 필드의 시간을 기준으로 분 = 30
시간 필드의 분을 기준으로 초 = 59

 

근데, 저렇게 잘 안쓰고 편의 메서드를 사용한다. 그니까 저것을 가지고 이런 편의 메서드를 제공한다고 보면 된다.

System.out.println("getYear = " + localDateTime.getYear());
System.out.println("getMonthValue = " + localDateTime.getMonthValue());
System.out.println("getDayOfMonth = " + localDateTime.getDayOfMonth());
System.out.println("getHour = " + localDateTime.getHour());
System.out.println("getMinute = " + localDateTime.getMinute());
System.out.println("getSecond = " + localDateTime.getSecond());

실행결과:

getYear = 2030
getMonthValue = 1
getDayOfMonth = 1
getHour = 13
getMinute = 30
getSecond = 59

 

 

물론, 없는 편의 메서드도 있다.

getMinuteOfDay, getSecondOfDay 이런건 없다.

System.out.println("일 단위를 기준으로 분 = " + localDateTime.get(ChronoField.MINUTE_OF_DAY));
System.out.println("일 단위를 기준으로 초 = " + localDateTime.get(ChronoField.SECOND_OF_DAY));
일 단위를 기준으로 분 = 810
일 단위를 기준으로 초 = 48659

 

그래서, 지금까지 시간의 기간(간격)을 더하고 빼는 여러 방법을 다 나열해보자.

LocalDateTime localDateTime = LocalDateTime.of(2030, 1, 1, 13, 30, 59);

LocalDateTime plusDt1 = localDateTime.plus(10, ChronoUnit.YEARS);
System.out.println("plusDt1 = " + plusDt1);

LocalDateTime plusDt2 = localDateTime.plusYears(10);
System.out.println("plusDt2 = " + plusDt2);

Period period = Period.ofYears(10);
LocalDateTime plusDt3 = localDateTime.plus(period);
System.out.println("plusDt3 = " + plusDt3);

실행결과:

plusDt1 = 2040-01-01T13:30:59
plusDt2 = 2040-01-01T13:30:59
plusDt3 = 2040-01-01T13:30:59

 

여기서 핵심은 이것이다.

TemporalAccessor.get(), Temporal.plus()와 같은 인터페이스를 통해 특정 구현 클래스와 무관하게 일관성있는 시간 조회, 조작 기능을 제공한다는 것. plus() 메서드를 보라. 10이라는 int, Period 타입, ChronoUnit까지 전부 다 받아준다.

 

물론, 모든게 다 되는건 아니다. 다음 코드는 에러가 난다.

LocalDate now = LocalDate.now();
int minute = now.get(ChronoField.SECOND_OF_MINUTE);
System.out.println("minute = " + minute);

실행결과:

Exception in thread "main" java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: SecondOfMinute
	at java.base/java.time.LocalDate.get0(LocalDate.java:698)
	at java.base/java.time.LocalDate.get(LocalDate.java:641)
	at time.ChangeTimeMain.main(ChangeTimeMain.java:41)

당연히 LocalDate 타입은 시분초에 대한 정보는 없기 때문에 필드 SECOND_OF_MINUTE으로는 값을 가져올 수 없다.

그래서 이런 문제를 예방하기 위해 TemporalAccessor와 Temporal 인터페이스는 이 타입이 특정 시간 단위나 필드를 사용할 수 있는지 확인할 수 있는 메서드도 제공한다.

TemporalAccessor

boolean isSupported(TemporalField field);

Temporal

boolean isSupported(TemporalUnit unit);

 

그래서 아래 코드처럼 안전하게 확인 후 시간을 가져오거나 가져오지 않거나 할 수 있다.

LocalDate now = LocalDate.now();
boolean supported = now.isSupported(ChronoField.SECOND_OF_MINUTE);
if (supported) {
    int minute = now.get(ChronoField.SECOND_OF_MINUTE);
    System.out.println("minute = " + minute);
}

 

날짜와 시간 조회하고 조작하기 (중요⭐️)

날짜와 시간을 조작하고 조회하는 경우를 은근 많이 만난다. 그래서 중요하다.

우선 날짜와 시간을 조작하는 with() 메서드를 사용해보자.

 

ChangeTime2Main

import java.time.LocalDateTime;
import java.time.temporal.ChronoField;

public class ChangeTime2Main {
    public static void main(String[] args) {
        LocalDateTime ldt = LocalDateTime.of(2020, 1, 1, 13, 30, 59);
        System.out.println("ldt = " + ldt);

        LocalDateTime newLdt = ldt.with(ChronoField.YEAR, 2030);
        System.out.println("newLdt = " + newLdt);

        LocalDateTime newLdt2 = ldt.withYear(2030);
        System.out.println("newLdt2 = " + newLdt2);
    }
}

 

with() 메서드를 사용하면 원하는 필드에 대해 날짜와 시간을 조작할 수 있다.

실행결과:

ldt = 2020-01-01T13:30:59
newLdt = 2030-01-01T13:30:59
newLdt2 = 2030-01-01T13:30:59

근데 이제 with() 메서드를 사용하려면 필드를 사용하는게 귀찮으니까 편의 메서드를 제공한다. withYear()가 그 예시이다. 

그리고 불변객체에 대해서 공부할 때 새로운 객체를 만들어낼 때 메서드의 이름을 통상 withXxx() 라고 말했는데 여기가 딱 그 대표적인 예시이다.

 

근데 이제 이런 경우가 있다. 정해진 LocalDateTime을 기준으로 다음으로 오는 금요일이라던지, 해당 달의 마지막 일요일이라던지 이런값을 구하고 싶을 때가 있다. TemporalAdjuster 인터페이스를 사용을 하면 된다.

 

원래라면, 이 인터페이스를 직접 구현한 구현체가 필요하겠지만 자바는 이미 필요한 구현체들을 TemporalAdjusters 라는 구현클래스에 구현체를 다 모아두었다. 이걸 사용하면 된다.

 

ChangeTime2Main 일부

LocalDateTime now = LocalDateTime.now();

LocalDateTime nextFriday = now.with(TemporalAdjusters.next(DayOfWeek.FRIDAY));
System.out.println("nextFriday = " + nextFriday);

LocalDateTime lastSundayInMonth = now.with(TemporalAdjusters.lastInMonth(DayOfWeek.SUNDAY));
System.out.println("lastSundayInMonth = " + lastSundayInMonth);

실행결과:

nextFriday = 2024-04-19T10:51:35.605616
lastSundayInMonth = 2024-04-28T10:51:35.605616

 

기가 막힌다. 지금까지는 직접 구하고 그랬는데 아는게 힘인게 역시 맞다..

그리고 이 클래스가 제공하는 주요 메서드들은 다음과 같다.

 

TemporalAdjusters 클래스의 주요 메서드

아래표에서 파라미터는 설명을 위해 몇개만 써넣은거지 안 써넣은게 없다는 뜻은 아니다.

메서드 설명
dayOfWeekInMonth(int ordinal, DayofWeek dayOfWeek) 주어진 요일이 몇 번째인지에 따라 날짜를 조정한다. 예를 들어, 파라미터에 (2, DayOfWeek.MONDAY) 이렇게 들어가면 2번째 주 월요일을 반납한다.
firstDayOfMonth 해당 월의 첫번째 날로 조정한다.
firstDayOfNextMonth 해당 월의 다음달 첫번째 날로 조정한다.
firstDayOfNextYear 다음 년도 첫번째 날로 조정한다.
firstDayOfYear 이번 년도 첫번째 날로 조정한다.
firstInMonth(DayOfWeek dayOfWeek) 첫번째 주의 주어진 요일로 조정한다. 예를 들어, 파라미터에 (DayOfWeek.TUESDAY)이렇게 들어가면 첫번째 주 화요일을 반환한다.
lastDayOfMonth 해당 월의 마지막 날로 조정한다.
lastDayOfNextMonth 다음 달의 마지막 날로 조정한다.
lastDayOfNextYear 다음 년도 마지막 날로 조정한다.
lastDayOfYear 이번 년도 마지막 날로 조정한다.
lastInMonth(DayOfWeek dayOfWeek) 주어진 요일 중 해당 월의 마지막 요일로 조정한다.
next(DayOfWeek dayOfWeek) 주어진 요일 이후의 가장 가까운 요일로 조정한다.
nextOrSame 주어진 요일 이후의 가장 가까운 요일로 조정하되, 현재 날짜가 주어진 요일인 경우 현재 날짜를 반환한다.
previous 주어진 요일 이전의 가장 가까운 요일로 조정한다.
previousOrSame 주어진 요일 이전의 가장 가까운 요일로 조정하되, 현재 날짜가 주어진 요일인 경우 현재 날짜를 반환한다.

 

 

날짜와 시간 문자열 파싱과 포맷팅

이게 용어를 정리하면 뭔가 좀 확 익숙해진다.

 

  • 포맷팅: 날짜를 원하는 형태의 문자로 변경 (날짜 -> 문자)
  • 파싱: 문자를 날짜로 변경 (문자 -> 날짜)

FormatterAndParseMain

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class FormatterAndParseMain {
    public static void main(String[] args) {

        LocalDateTime now = LocalDateTime.of(2024, 4, 14, 11, 16, 59);
        System.out.println("now = " + now);
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy년 MM월 dd일");

        String formattedDateTime = now.format(formatter);
        System.out.println("formattedDateTime = " + formattedDateTime);
    }
}

실행결과:

now = 2024-04-14T11:16:59
formattedDateTime = 2024년 04월 14일

 

이렇게 원하는 형태의 문자열로 날짜를 문자로 변경할 수 있다. 이것을 포맷팅이라고 한다.

 근데 그 때 형태가 중요하다. MM월을 mm월로 사용할 수 없다. 정해진 규칙이 있기 때문에.

mm이라고 소문자로 표현하면 분을 의미하게 된다.

https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html#patterns

 

DateTimeFormatter (Java Platform SE 8 )

Parses the text using this formatter, without resolving the result, intended for advanced use cases. Parsing is implemented as a two-phase operation. First, the text is parsed using the layout defined by the formatter, producing a Map of field to value, a

docs.oracle.com

이번엔 문자열을 날짜와 시간 타입으로 변경해보자.

FormatterAndParseMain 일부

DateTimeFormatter formatter2 = DateTimeFormatter.ofPattern("yyyy년 MM월 dd일 HH시 mm분 ss초");
String dateTimeString = "2020년 11월 03일 11시 20분 15초";
LocalDateTime parsedDateTime = LocalDateTime.parse(dateTimeString, formatter2);

System.out.println("dateTimeString = " + dateTimeString);
System.out.println("parsedDateTime = " + parsedDateTime);

실행결과:

dateTimeString = 2020년 11월 03일 11시 20분 15초
parsedDateTime = 2020-11-03T11:20:15

 

문자열로 된 날짜와 시간을 날짜와 시간 타입으로 변경했다. 

포맷팅과 파싱을 보면 알겠지만 패턴은 형식이 그대로 일치해야 한다! 

그니까, 파싱할 때 패턴을 "yyyy년 MM월 dd일 HH:mm:ss" 이렇게 설정했는데 문자열이 "yyyy년 MM월 dd일 HH시 mm분 ss초" 이렇게 모양새가 다르면 파싱 못한다.

 

 

 

728x90
반응형
LIST

'JAVA의 가장 기본이 되는 내용' 카테고리의 다른 글

중첩 클래스(정적 중첩 클래스, 내부 클래스)  (0) 2024.04.14
Stream API  (2) 2024.04.07
Enum  (0) 2024.04.03
Class 클래스  (0) 2024.04.03
Wrapper Class  (0) 2024.04.02