[Java/자바] java.time 패키지 (날짜와 시간 다루기)

이전에 Date 클래스와 Calendar 클래스를 이용하여 날짜와 시간 데이터를 다루는 방법을 포스팅했었습니다.

이번에 포스팅할 java.time 패키지는 위 두 클래스의 단점을 보완한 패키지로 JDK1.8부터 추가되었습니다.

 

java.time 패키지

패키지 설명
java.time 날짜와 시간을 다루는데 필요한 핵심 클래스들을 제공
java.time.chrono 표준(ISO)이 아닌 달력 시스템을 위한 클래스들을 제공
java.time.format 날짜와 시간을 파싱하고 형식화하기 위한 클래스들을 제공
java.time.temporal 날짜와 시간의 필드(field)와 단위(unit)를 위한 클래스들을 제공
java.time.zone 시간대(time-zone)와 관련된 클래스들을 제공

java.time 패키지는 위와 같이 4개의 하위 패키지를 가지고 있습니다.

위 패키지들은 String 클래스들 처럼 "불변(immutable)"이라는 것이라는 특징을 가집니다.

그래서 날짜와 시간을 변경하는 메서드들은 기존의 객체를 변경하지 않고 변경되면 항상 새로운 객체를 반환합니다.

멀티 쓰레드 환경에서 스레드의 안전하지 않음.
멀티 쓰레드 환경에서는 동시에 여러 쓰레드가 같은 객체에 접근할 수 있기 때문에, 변경 가능한 객체는 데이터가 잘못될 가능성이 있어 쓰레드에 안전하지 않다.

하지만 Calendar 클래스는 변경가능하기 때문에 멀티 쓰레드 환경에서 안전하지 못합니다.

 

java.time 패키지의 핵심 클래스들
클래스 설명 조합
LocalDate 날짜  
LocalTime 시간  
LocalDateTime 날짜와 시간 LocalDate + LocalTime
ZonedDateTime 날짜와 시간, 시간대 LocalDateTime + 시간대

 


객체 생성하기 - now(), of()

java.time 패키지에 속한 클래스의 객체를 생성하는데 now()와 of()가 사용됩니다.

 

now()는 현재 날짜와 시간을 저장하는 객체를 생성하고,

of()는 해당 필드의 값을 순서대로 지정해주어 원하는 날짜와 시간으로 객체를 생성합니다.

// now()
LocalDate date = LocalDate.now(); // 2023-01-16
LocalTime time = LocalTime.now();  // 22:16:53.198942300

// of()
LocalDate date = LocalDate.of(2022, 12, 31); // 2022-12-31
LocalTime time = LocalTime.of(23, 59, 59);  // 23:59:59

/* of()메서드의 매개변수
LocalDate of(int year, int month, int dayOfMonth)
LocalTime of(int hour, int min, int sec, int nanoOfSecond)
*/

특정 필드의 값 가져오기

java.time 패키지는 Calendar와 다르게 월은 (1 ~ 12)의 범위를 갖고, 요일은 월요일(1)부터 ~ 일요일(7)의 범위를 갖습니다.

 

 

LocalDate 메서드
메서드 반환 타입 설 명
getYear() int 년도(2022)
getMonth() Month Month열거값 반환(DECEMBER, MARCH, JUNE ...)
getMonthValue() int 월(12)
getDayOfYear() int 일(31)
getDayOfMonth() int 요일
getDayOfWeek() DayOfWeek Week 열거값 반환(MONDAY, FRIDAY ...)
isLeapYear() boolean 윤년여부 확인(true, false)

 

LocalTime 메서드
메서드 반환 타입 설 명
getHour() int 시(23)
getMinute() int 분(59)
getSecond() int 초(59)
getNano() int
나노 초(.1984)

 

 

get()과 getLong(). 메서드

위 메서드 외에도 get()과 getLong()이 있는데,

get()은 int형 정수 범위 내의 값을 가져올 때 사용하고, getLong()은 나노초 처럼 큰 값 long 타입을 가져올 때 사용합니다.

 

get() 메서드는 TemporalField의 열거형 값들을 매개변수로 함께 사용하는데 아래처럼 사용합니다.

// int get(TemporalField field)
// long getLong(TemporalField field)

LocalDate date = LocalDate.now();
today.get(ChronoField.YEAR)); // 2023

 

get()과 함께 사용하는 TemporalField의 열거값
TemporalField(열거값) 설 명
ERA 시대
YEAR 연도
MONTH_OF_YEAR
DAY_OF_MONTH
DAY_OF_WEEK 요일 (월요일:1, 화요일:2, ..., 일요일:7)
AMPM_OF_DAY 오전/오후
HOUR_OF_DAY 시(0~23)  : 밤 12시를 0으로 표현
CLOCK_HOUR_OF_DAY 시(1~24) : 밤 12시를 24시로 표현
HOUR_OF_AMPM 시(0~11)
CLOCK_HOUR_OF_AMPM 시(1~12)
MINUTE_OF_HOUR
SECOND_OF_MINUTE
DAY_OF_YEAR 해당 연도의 몇 번째 날 (1~365, 윤년이면 366)
EPOCH_DAY EPOCH(1970년 1월 1일)을 기준으로 몇 번째 날

필드 값 변경하기 - with(), plus(), minus()

 

with()

with를 사용하면 TemporalField의 열거값을 매개변수로 원하는 필드를 직접 지정하여 변경합니다.

// Local Date with(TemporalField field, long newValue)

LocalDate date = LocalDate.now();
System.out.println(date); // 2023-01-16

date = date.with(ChronoField.DAY_OF_WEEK, 3); // 요일을 수요일(3)로 변경
System.out.println(date); // 2023-01-18

 

with날짜, with시간

날짜와 시간에서 특정 필드 값을 변경하려면 with로 시작하는 메서드를 사용합니다.

		LocalDate date = LocalDate.now(); // 오늘의 날짜
		LocalTime time = LocalTime.now();  // 현재 시간

		System.out.println(date); // 2023-01-16
		System.out.println(date.withYear(2022)); // 2022-01-16
		System.out.println(date.withMonth(12)); // 2023-12-16
		System.out.println(date.withDayOfMonth(31)); // 2023-01-31
		System.out.println(date.getDayOfYear()); // 16 (1월 1일 부터 16번째 일)

		System.out.println(time); // 22:59:37.880646500
		System.out.println(time.withHour(1)); // 01:59:37.880646500
		System.out.println(time.withMinute(30)); // 22:30:37.880646500
		System.out.println(time.withSecond(50)); // 22:59:50.880646500
		System.out.println(time.withNano(3333)); // 22:59:37.000003333

 

plus(), minus()

plus(), minus()TemporalField의 열거값을 매개변수로 원하는 필드를 직접 지정하여 변경하고

앞에 with를 붙여서 사용하면 특정 필드 값을 변경합니다.

LocalDate date2 = LocalDate.now(); // 오늘의 날짜
LocalTime time2 = LocalTime.now();  // 현재 시간

System.out.println(date2); // 2023-01-16
System.out.println(date2.plus(3, ChronoUnit.DAYS)); // 2023-01-19
System.out.println(date2.plusDays(10)); // 2023-01-26
System.out.println(time2); // 23:07:48.202294900
System.out.println(time2.plusHours(3)); // 02:07:48.202294900

날짜와 시간 비교 - isAfter(), isBefore(), isEqual()

날짜와 시간을 비교할 때 compareTo()로도 비교할 수 있는데 같으면 0, 이전이면 음수, 이후면 양수를 반환합니다.

LocalDate date1 = LocalDate.of(2000, 12, 31);
LocalDate date2 = LocalDate.now(); // 2023-01-16

System.out.println(date1.compareTo(date2)); // -23
System.out.println(date1.isAfter(date2)); // false
System.out.println(date1.isBefore(date2)); // true
System.out.println(date1.isEqual(date2)); // false

equals()로도 두 날짜가 같은지 비교할 수도 있지만, equals()는 연표가 다르면 날짜가 같아도 false를 반환하기 때문에

날짜가 서로 같은지 확인하기 위해서는 isEquals()를 사용하는게 좋습니다.


LocalDateTime과 ZonedDateTime

 

LocalDateTime 만들기
LocalDate date = LocalDate.now();
LocalTime time = LocalTime.now();

LocalDateTime dt = LocalDateTime.of(date, time); // 2023-01-16T23:23:44.638521400
LocalDateTime dt2 = date.atTime(time); // 2023-01-16T23:23:44.638521400
LocalDateTime dt3 = time.atDate(date); // 2023-01-16T23:23:44.638521400
LocalDateTime dt4 = date.atTime(12, 30, 30); // 2023-01-16T12:30:30
LocalDateTime dt5 = time.atDate(LocalDate.of(2000, 12, 31)); // 2000-12-31T23:23:44.638521400

// LocalDateTime -> LocalDate, LocalTime으로 변환
LocalDateTime dt = LocalDateTime.of(2000, 12, 31, 12 ,30, 30);
LocalDate date = dt.toLocalDate();
LocalTime time = dt.toLocalTime();

 

ZonedDateTime 만들기
LocalDateTime dateTime = LocalDateTime.of(2000, 12, 31, 1, 30, 30);
ZoneId zid = ZoneId.of("Asia/Seoul"); // Asia/Seoul
ZonedDateTime zdt = dateTime.atZone(zid); // 2000-12-31T01:30:30+09:00[Asia/Seoul]

ZonedDateTime seoulTime = ZonedDateTime.now(); // 2023-01-16T23:31:49.875954900+09:00[Asia/Seoul]

ZoneId nyId = ZoneId.of("America/New_York"); // America/New_York
ZonedDateTime nyTime = ZonedDateTime.now().withZoneSameInstant(nyId); // 2023-01-16T09:31:49.876955600-05:00[America/New_York]

TemporalAdjusters

TemporalAdjusters 클래스는 지난 주 토욜일이 몇 일 인지, 이번 달의 4번째 일요일이 몇일인지와 같은 날짜를 계산할 수 있도록 도와줍니다.

 

단, TemporalAdjusters는 adjustInto()로 정의되어있는 추상 메서드를 구현하여 사용해야합니다. 

 

TemporalAdjusters  메서드
메서드 설 명
  firstDayOfNextYear()   다음 해의 첫 날
  firstDayOfNextMonth()   다음 달의 첫 날
  firstDayOfYear()   올 해의 첫 날
  firstDayOfMonth()   이번 달의 첫 날
  lastDayOfYear()   올 해의 마지막 날
  lastDayOfMonth()   이번 달의 마지막 날
  firstInMonth(DayOfWeek dayOfWeek)   이번 달의 첫 번째 요일
  lastInMonth(DayOfWeek dayOfWeek)   이번 달의 마지막 요일
  previous(DayOfWeek dayOfWeek)   지난 요일(당일 미포함)
  previousOrSame(DayOfWeek dayOfWeek)   지난 요일(당일 포함)
  next(DayOfWeek dayOfWeek)   다음 요일(당일 미포함)
  nextOrSame(DayOfWeek dayOfWeek)   다음 요일(당일 포함)
  dayOfWeekInMonth(int ordinal, DayOfWeek dayOfWeek)   이번 달의 n번째 요일

 

특정 날짜로 부터 2일 후의 날짜를 계산하는 예제
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.time.temporal.Temporal;
import java.time.temporal.TemporalAdjuster;
import static java.time.DayOfWeek.*;
import static java.time.temporal.TemporalAdjusters.*;

class DayAfterTomorrow implements TemporalAdjuster {
    @Override
    // 날짜와 시간에 관련된 대부분의 클래스는 Temporal 인터페이스를 구현하였으므로 이 메서드의 매개변수가 될 수 있다
    public Temporal adjustInto(Temporal temporal) {
        return temporal.plus(2, ChronoUnit.DAYS); // 2일을 더한다.
    }
}

class NewTimeEx3 {
    public static void main(String[] args) {
        LocalDate today = LocalDate.now();
        LocalDate date  = today.with(new DayAfterTomorrow());

        System.out.println(today);
        System.out.println(date);

        today.with(firstDayOfNextMonth());        // 다음 달의 첫 날
        today.with(firstDayOfMonth());            // 이 달의 첫 날
        today.with(lastDayOfMonth());             // 이 달의 마지막 날
        today.with(firstInMonth(TUESDAY));        // 이 달의 첫번째 화요일
        today.with(lastInMonth(TUESDAY));         // 이 달의 마지막 화요일
        today.with(previous(TUESDAY));            // 지난 주 화요일
        today.with(previousOrSame(TUESDAY));      // 지난 주 화요일(오늘 포함)
        today.with(next(TUESDAY));                // 다음 주 화요일
        today.with(nextOrSame(TUESDAY));          // 다음 주 화요일(오늘 포함)
        today.with(dayOfWeekInMonth(4, TUESDAY)); // 이 달의 4번째 화요일
    }
}

Period와 Duration

Period는 날짜의 차이

Duration은 시간의 차이를 계산합니다.

 

두 날짜의 차이를 계산하는데 between() 메서드를 사용하는데 같은 역할을 하는 메서드로 utill()이 있습니다.

이 둘의 차이는 between()은 static 메서드이고, utill()은 인스턴스 메서드라는 것입니다.

 

// 날짜의 차이
LocalDate date1 = LocalDate.of(2020, 1, 1);
LocalDate date2 = LocalDate.of(2022, 12, 31);

Period pe = Period.between(date1, date2);

System.out.println("date1 = " + date1); // date1 = 2020-01-01
System.out.println("date2 = " + date2); // date2 = 2022-12-31
System.out.println("pe = " + pe); // pe = P2Y11M30D

System.out.println("YEAR = " + pe.get(ChronoUnit.YEARS)); // YEAR = 2
System.out.println("MONTH = " + pe.get(ChronoUnit.MONTHS)); // MONTH = 11
System.out.println("DAY = " + pe.get(ChronoUnit.DAYS)); // DAY = 30

// 시간의 차이
LocalTime time1 = LocalTime.of(10, 10, 20);
LocalTime time2 = LocalTime.of(12, 30, 50); 

Duration du = Duration.between(time1, time2);

System.out.println("time1 = " + time1); // time1 = 10:10:20
System.out.println("time2 = " + time2); // time2 = 12:30:50
System.out.println("du = " + du); // du = PT2H20M30S

LocalTime tmpTime = LocalTime.of(0, 0).plusSeconds(du.getSeconds()); // 02:20:30

System.out.println("HOUR = " + tmpTime.getHour()); // HOUR = 2
System.out.println("MINUTE = " + tmpTime.getMinute()); // MINUTE = 20
System.out.println("SECOND = " + tmpTime.getSecond()); // SECOND = 30
System.out.println("NANO = " + tmpTime.getNano()); // NANO = 0

파싱과 포맷

날짜와 시간의 형식화에는 format() 메서드가 사용됩니다.

DateTimeFormatter에 상수로 정의된 형식들을 매개변수로 원하는 형식(문자열)으로 출력합니다.

LocalDate day = LocalDate.of(2022, 12, 31);
String d = day.format(DateTimeFormatter.ISO_LOCAL_DATE); // 2022-12-31
String d2 = DateTimeFormatter.ISO_LOCAL_DATE.format(day); // 2022-12-31

 

또한 parse() 메서드를 이용하여 문자열 데이터를 날짜 데이터로 변환할 수도 있습니다.

LocalDate date = LocalDate.parse("2022-12-31"); // 2022년 12월 31일
LocalDate time = LocalTime.parse("23:59:59"); // 23시 59분 59초

 

출력형식 직접 정의하기

SimpleDateFormat 클래스 처럼 기호를 이용하여 DateTimeFormatterofPattern()으로 원하는 출력형식을 직접 작성할 수 있습니다.

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");