Java 8 날짜 – LocalDate, LocalDateTime, Instant

날짜 및 시간 API는 Java 8 릴리스의 가장 큰 기능 중 하나입니다. Java는 시작부터 일관된 날짜 및 시간 접근 방식이 부족했으며, Java 8 날짜 및 시간 API는 핵심 Java API에 환영받는 추가입니다.

새로운 Java 날짜 및 시간 API가 필요한 이유는 무엇인가요?

Java 8 날짜 및 시간 API를 살펴보기 전에 이것이 왜 필요한지 알아보겠습니다. Java에서 기존 날짜 및 시간 관련 클래스에는 몇 가지 문제가 있었습니다. 그 중 일부는 다음과 같습니다:

  • Java 날짜 및 시간 클래스가 일관되게 정의되지 않았습니다. 우리는 java.util 및 java.sql 패키지에 모두 Date 클래스를 가지고 있습니다. 또한, 포맷팅 및 구문 분석 클래스는 java.text 패키지에 정의되어 있습니다.
  • java.util.Date에는 날짜 및 시간 값이 모두 포함되어 있지만, java.sql.Date에는 날짜 값만 포함되어 있습니다. 이것을 java.sql 패키지에 두는 것은 전혀 의미가 없습니다. 또한, 두 클래스 모두 같은 이름을 가지고 있으며, 이는 매우 나쁜 디자인입니다.
  • 시간, 타임스탬프, 포맷팅 및 구문 분석에 대해 명확히 정의된 클래스가 없습니다. 파싱 및 포맷팅이 필요한 경우 java.text.DateFormat 추상 클래스를 사용합니다. 일반적으로 SimpleDateFormat 클래스가 파싱 및 포맷팅에 사용됩니다.
  • 모든 날짜 클래스는 가변이므로 스레드 안전하지 않습니다. 이것은 Java의 Date 및 Calendar 클래스의 가장 큰 문제 중 하나입니다.
  • Date 클래스는 국제화를 제공하지 않으며, 시간대 지원이 없습니다. 그래서 java.util.Calendar 및 java.util.TimeZone 클래스가 소개되었지만, 이들도 위에 나열된 모든 문제를 가지고 있습니다.

Date 및 Calendar 클래스에 정의된 메서드에는 몇 가지 다른 문제가 있지만 위의 문제들로 인해 Java에 견고한 날짜 및 시간 API가 필요하다는 것이 명확해졌습니다. 그래서 Joda Time 이 Java Date Time 요구 사항에 대한 품질 대체품으로 중요한 역할을했습니다.

Java 8 날짜 및 시간 설계 원칙

Java 8 날짜 및 시간 API는 JSR-310 의 구현입니다. 이는 기존의 날짜 및 시간 구현의 모든 결함을 극복하기 위해 설계되었습니다. 새로운 Date Time API의 일부 설계 원칙은 다음과 같습니다:

  1. 불변성: 새로운 Date-Time API의 모든 클래스는 불변하며 다중 스레드 환경에 적합합니다.

  2. 관심사 분리: 새 API는 사람이 읽을 수 있는 날짜 및 시간과 기계 시간 (Unix 타임스탬프) 사이에 명확하게 분리됩니다. Date, Time, DateTime, Timestamp, TimeZone 등에 대한 별도의 클래스를 정의합니다.

  3. 명확성: 메서드는 명확하게 정의되어 있으며 모든 클래스에서 동일한 작업을 수행합니다. 예를 들어, 현재 인스턴스를 가져오려면 now() 메서드를 사용합니다. 이러한 클래스들에는 별도의 클래스 대신 format() 및 parse() 메서드가 정의되어 있습니다.

    모든 클래스는 더 나은 처리를 위해 Factory PatternStrategy Pattern을 사용합니다. 한 클래스의 메서드를 사용한 후에는 다른 클래스와 함께 작업하는 것이 어렵지 않습니다.

  4. 유틸리티 작업: 모든 새로운 날짜-시간 API 클래스에는 플러스, 마이너스, 형식 지정, 구문 분석, 날짜/시간의 별도 부분 가져오기 등과 같은 일반적인 작업을 수행하는 메서드가 함께 제공됩니다.

  5. 확장 가능: 새로운 날짜 및 시간 API는 ISO-8601 달력 시스템에서 작동하지만 다른 ISO 이외의 달력과도 사용할 수 있습니다.

날짜 및 시간 API 패키지

Java 8 날짜 및 시간 API는 다음 패키지로 구성됩니다.

  1. java.time: 이것은 새로운 Java 날짜 및 시간 API의 기본 패키지입니다. 모든 중요한 기본 클래스가 이 패키지의 일부이며, LocalDate, LocalTime, LocalDateTime, Instant, Period, Duration 등이 있습니다. 이러한 모든 클래스는 변경할 수 없으며 스레드로부터 안전합니다. 대부분의 경우, 이러한 클래스들은 일반적인 요구 사항을 처리하는 데 충분할 것입니다.
  2. java.time.chrono: 이 패키지는 ISO 이외의 달력 시스템에 대한 일반적인 API를 정의합니다. 우리는 AbstractChronology 클래스를 확장하여 고유한 달력 시스템을 만들 수 있습니다.
  3. java.time.format: 이 패키지에는 날짜 및 시간 객체를 형식화하고 구문 분석하는 데 사용되는 클래스가 포함되어 있습니다. 대부분의 경우 java.time 패키지의 주요 클래스가 형식화 및 구문 분석 메서드를 제공하기 때문에 직접 사용하지 않을 것입니다.
  4. java.time.temporal: 이 패키지에는 시간 객체가 포함되어 있으며 날짜/시간 객체와 관련된 특정 날짜나 시간을 찾을 수 있습니다. 예를 들어, 월의 첫 번째 또는 마지막 날짜를 찾기 위해 사용할 수 있습니다. 이러한 메서드는 항상 “withXXX” 형식을 가지고 있어 쉽게 식별할 수 있습니다.
  5. java.time.zone Package: 이 패키지에는 다양한 시간대 및 해당 규칙을 지원하기 위한 클래스가 포함되어 있습니다.

Java 8 Date Time API Classes Examples

Java Date Time API의 가장 중요한 부분 중 대부분을 살펴보았습니다. 이제 예제와 함께 Date Time API의 가장 중요한 클래스를 살펴보겠습니다.

1. LocalDate

LocalDate는 yyyy-MM-dd의 기본 형식으로 Date를 나타내는 변경할 수 없는 클래스입니다. 현재 날짜를 가져오기 위해 now() 메서드를 사용할 수 있습니다. 또한 연도, 월 및 날짜에 대한 입력 인수를 제공하여 LocalDate 인스턴스를 만들 수 있습니다.

이 클래스는 특정 시간대에서 날짜를 얻기 위해 ZoneId를 전달할 수 있는 now()에 대한 오버로드된 메서드를 제공합니다. 이 클래스는 java.sql.Date와 동일한 기능을 제공합니다.

package com.journaldev.java8.time;

import java.time.LocalDate;
import java.time.Month;
import java.time.ZoneId;

/**
 * LocalDate Examples
 * @author pankaj
 *
 */
public class LocalDateExample {

	public static void main(String[] args) {
		
		//현재 날짜
		LocalDate today = LocalDate.now();
		System.out.println("Current Date="+today);
		
		//입력 인수를 제공하여 LocalDate 생성
		LocalDate firstDay_2014 = LocalDate.of(2014, Month.JANUARY, 1);
		System.out.println("Specific Date="+firstDay_2014);
		
		
		//잘못된 입력을 제공하여 날짜를 만들어보십시오.
		//LocalDate feb29_2014 = LocalDate.of(2014, Month.FEBRUARY, 29);
		//Exception in thread "main" java.time.DateTimeException: 
		//'2014'가 윤년이 아니기 때문에 유효하지 않은 날짜 'February 29'
		
		//"Asia/Kolkata"의 현재 날짜, ZoneId javadoc에서 얻을 수 있습니다.
		LocalDate todayKolkata = LocalDate.now(ZoneId.of("Asia/Kolkata"));
		System.out.println("Current Date in IST="+todayKolkata);

		//java.time.zone.ZoneRulesException: Unknown time-zone ID: IST
		//LocalDate todayIST = LocalDate.now(ZoneId.of("IST"));
		
		//기준 날짜인 1970년 1월 1일부터 날짜 얻기
		LocalDate dateFromBase = LocalDate.ofEpochDay(365);
		System.out.println("365th day from base date= "+dateFromBase);
		
		LocalDate hundredDay2014 = LocalDate.ofYearDay(2014, 100);
		System.out.println("100th day of 2014="+hundredDay2014);
	}

}

LocalDate 메서드에 대한 설명은 주석으로 제공됩니다. 이 프로그램을 실행하면 다음 출력이 표시됩니다.

Current Date=2014-04-28
Specific Date=2014-01-01
Current Date in IST=2014-04-29
365th day from base date= 1971-01-01
100th day of 2014=2014-04-10

2. LocalTime

LocalTime은 인간이 읽을 수 있는 형식으로 시간을 나타내는 불변 클래스입니다. 기본 형식은 hh:mm:ss.zzz입니다. LocalDate와 마찬가지로 이 클래스는 시간대 지원을 제공하며 시, 분 및 초를 입력 인수로 전달하여 인스턴스를 생성합니다.

package com.journaldev.java8.time;

import java.time.LocalTime;
import java.time.ZoneId;

/**
 * LocalTime 예제
 * @author pankaj
 *
 */
public class LocalTimeExample {

	public static void main(String[] args) {
		
		//현재 시간
		LocalTime time = LocalTime.now();
		System.out.println("Current Time="+time);
		
		//입력 인수를 제공하여 LocalTime 생성
		LocalTime specificTime = LocalTime.of(12,20,25,40);
		System.out.println("Specific Time of Day="+specificTime);
		
		
		//유효하지 않은 입력을 제공하여 시간을 만들어보세요
		//LocalTime invalidTime = LocalTime.of(25,20);
		//Exception in thread "main" java.time.DateTimeException: 
		//HourOfDay에 대한 유효하지 않은 값 (유효한 값은 0 - 23): 25
		
		// "Asia/Kolkata"에서 현재 날짜, ZoneId javadoc에서 얻을 수 있습니다
		LocalTime timeKolkata = LocalTime.now(ZoneId.of("Asia/Kolkata"));
		System.out.println("Current Time in IST="+timeKolkata);

		//java.time.zone.ZoneRulesException: 알 수 없는 시간대 ID: IST
		//LocalTime todayIST = LocalTime.now(ZoneId.of("IST"));
		
		//기본 날짜인 01/01/1970부터 날짜 가져오기
		LocalTime specificSecondTime = LocalTime.ofSecondOfDay(10000);
		System.out.println("10000th second time= "+specificSecondTime);

	}

}

출력:

Current Time=15:51:45.240
Specific Time of Day=12:20:25.000000040
Current Time in IST=04:21:45.276
10000th second time= 02:46:40

3. LocalDateTime

LocalDateTime은 yyyy-MM-dd-HH-mm-ss.zzz로 기본 형식을 갖는 불변의 날짜-시간 객체입니다. LocalDate와 LocalTime 입력 인수를 사용하여 LocalDateTime 인스턴스를 생성하는 팩토리 메서드를 제공합니다.

package com.journaldev.java8.time;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Month;
import java.time.ZoneId;
import java.time.ZoneOffset;

public class LocalDateTimeExample {

	public static void main(String[] args) {
		
		//현재 날짜
		LocalDateTime today = LocalDateTime.now();
		System.out.println("Current DateTime="+today);
		
		//LocalDate 및 LocalTime을 사용하여 현재 날짜
		today = LocalDateTime.of(LocalDate.now(), LocalTime.now());
		System.out.println("Current DateTime="+today);
		
		//입력 인수를 제공하여 LocalDateTime 생성
		LocalDateTime specificDate = LocalDateTime.of(2014, Month.JANUARY, 1, 10, 10, 30);
		System.out.println("Specific Date="+specificDate);
		
		
		//잘못된 입력을 제공하여 날짜 생성 시도
		//LocalDateTime feb29_2014 = LocalDateTime.of(2014, Month.FEBRUARY, 28, 25,1,1);
		//스레드 "main"에서 java.time.DateTimeException 발생:
		//시간 (유효한 값 0 - 23)에 대한 잘못된 값: 25

		
		//"Asia/Kolkata"의 현재 날짜, ZoneId javadoc에서 확인할 수 있습니다.
		LocalDateTime todayKolkata = LocalDateTime.now(ZoneId.of("Asia/Kolkata"));
		System.out.println("Current Date in IST="+todayKolkata);

		//java.time.zone.ZoneRulesException: 알 수 없는 시간대 ID: IST
		//LocalDateTime todayIST = LocalDateTime.now(ZoneId.of("IST"));
		
		//기준 날짜인 1970/01/01부터 날짜를 가져옵니다.
		LocalDateTime dateFromBase = LocalDateTime.ofEpochSecond(10000, 0, ZoneOffset.UTC);
		System.out.println("10000th second time from 01/01/1970= "+dateFromBase);

	}

}

세 가지 예제 모두 날짜/시간을 생성할 때 잘못된 인수를 제공하면 java.time.DateTimeException이 발생하는 것을 볼 수 있습니다. 이것은 RuntimeException이므로 명시적으로 catch할 필요가 없습니다.

우리는 또한 ZoneId를 전달하여 날짜/시간 데이터를 가져올 수 있다는 것을 알아봤는데, 지원되는 ZoneId 값 목록은 해당 JavaDoc에서 확인할 수 있습니다. 위 클래스를 실행하면 다음과 같은 출력이 나옵니다.

Current DateTime=2014-04-28T16:00:49.455
Current DateTime=2014-04-28T16:00:49.493
Specific Date=2014-01-01T10:10:30
Current Date in IST=2014-04-29T04:30:49.493
10000th second time from 01/01/1970= 1970-01-01T02:46:40

4. Instant

Instant 클래스는 기계 판독 가능한 시간 형식으로 작업하는 데 사용됩니다. Instant는 날짜 시간을 유닉스 타임스탬프로 저장합니다.

package com.journaldev.java8.time;

import java.time.Duration;
import java.time.Instant;

public class InstantExample {

	public static void main(String[] args) {
		//현재 타임스탬프
		Instant timestamp = Instant.now();
		System.out.println("Current Timestamp = "+timestamp);
		
		//타임스탬프에서 Instant
		Instant specificTime = Instant.ofEpochMilli(timestamp.toEpochMilli());
		System.out.println("Specific Time = "+specificTime);
		
		//기간 예제
		Duration thirtyDay = Duration.ofDays(30);
		System.out.println(thirtyDay);
	}

}

출력:

Current Timestamp = 2014-04-28T23:20:08.489Z
Specific Time = 2014-04-28T23:20:08.489Z
PT720H

Java 8 날짜 API 유틸리티

대부분의 Date Time 주요 클래스는 플러스/마이너스 일, 주, 월 등과 같은 다양한 유틸리티 메서드를 제공합니다. 또한 날짜를 조정하기 위한 TemporalAdjuster를 사용하는 다른 유틸리티 메서드와 두 날짜 간의 기간을 계산하는 메서드도 있습니다.

package com.journaldev.java8.time;

import java.time.LocalDate;
import java.time.LocalTime;
import java.time.Period;
import java.time.temporal.TemporalAdjusters;

public class DateAPIUtilities {

	public static void main(String[] args) {
		
		LocalDate today = LocalDate.now();
		
		//연도를 가져와서 윤년인지 확인합니다.
		System.out.println("Year "+today.getYear()+" is Leap Year? "+today.isLeapYear());
		
		//두 LocalDate를 비교하여 이전과 이후를 확인합니다.
		System.out.println("Today is before 01/01/2015? "+today.isBefore(LocalDate.of(2015,1,1)));
		
		//LocalDate에서 LocalDateTime을 생성합니다.
		System.out.println("Current Time="+today.atTime(LocalTime.now()));
		
		//더하기와 빼기 작업입니다.
		System.out.println("10 days after today will be "+today.plusDays(10));
		System.out.println("3 weeks after today will be "+today.plusWeeks(3));
		System.out.println("20 months after today will be "+today.plusMonths(20));

		System.out.println("10 days before today will be "+today.minusDays(10));
		System.out.println("3 weeks before today will be "+today.minusWeeks(3));
		System.out.println("20 months before today will be "+today.minusMonths(20));
		
		//날짜를 조정하는 Temporal adjusters입니다.
		System.out.println("First date of this month= "+today.with(TemporalAdjusters.firstDayOfMonth()));
		LocalDate lastDayOfYear = today.with(TemporalAdjusters.lastDayOfYear());
		System.out.println("Last date of this year= "+lastDayOfYear);
		
		Period period = today.until(lastDayOfYear);
		System.out.println("Period Format= "+period);
		System.out.println("Months remaining in the year= "+period.getMonths());		
	}
}

출력:

Year 2014 is Leap Year? false
Today is before 01/01/2015? true
Current Time=2014-04-28T16:23:53.154
10 days after today will be 2014-05-08
3 weeks after today will be 2014-05-19
20 months after today will be 2015-12-28
10 days before today will be 2014-04-18
3 weeks before today will be 2014-04-07
20 months before today will be 2012-08-28
First date of this month= 2014-04-01
Last date of this year= 2014-12-31
Period Format= P8M3D
Months remaining in the year= 8

자바 8 날짜 구문 분석 및 포맷팅

다양한 형식으로 날짜를 포맷하고 문자열을 구문 분석하여 Date Time 객체를 가져오는 것은 매우 일반적입니다.

package com.journaldev.java8.time;

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

public class DateParseFormatExample {

	public static void main(String[] args) {
		
		//포맷 예시
		LocalDate date = LocalDate.now();
		//기본 형식
		System.out.println("Default format of LocalDate="+date);
		//특정 형식
		System.out.println(date.format(DateTimeFormatter.ofPattern("d::MMM::uuuu")));
		System.out.println(date.format(DateTimeFormatter.BASIC_ISO_DATE));
		
		
		LocalDateTime dateTime = LocalDateTime.now();
		//기본 형식
		System.out.println("Default format of LocalDateTime="+dateTime);
		//특정 형식
		System.out.println(dateTime.format(DateTimeFormatter.ofPattern("d::MMM::uuuu HH::mm::ss")));
		System.out.println(dateTime.format(DateTimeFormatter.BASIC_ISO_DATE));
		
		Instant timestamp = Instant.now();
		//기본 형식
		System.out.println("Default format of Instant="+timestamp);
		
		//구문 분석 예시
		LocalDateTime dt = LocalDateTime.parse("27::Apr::2014 21::39::48",
				DateTimeFormatter.ofPattern("d::MMM::uuuu HH::mm::ss"));
		System.out.println("Default format after parsing = "+dt);
	}

}

출력:

Default format of LocalDate=2014-04-28
28::Apr::2014
20140428
Default format of LocalDateTime=2014-04-28T16:25:49.341
28::Apr::2014 16::25::49
20140428
Default format of Instant=2014-04-28T23:25:49.342Z
Default format after parsing = 2014-04-27T21:39:48

Java Date API Legacy Date Time Support

레거시 날짜/시간 클래스는 거의 모든 애플리케이션에서 사용되므로 하위 호환성을 유지하는 것이 반드시 필요합니다. 그래서 우리는 레거시 클래스를 새 클래스로 변환하거나 그 반대로 변환할 수 있는 여러 유틸리티 메서드가 있습니다.

package com.journaldev.java8.time;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.TimeZone;

public class DateAPILegacySupport {

	public static void main(String[] args) {
		
		//Date를 Instant로 변환
		Instant timestamp = new Date().toInstant();
		//이제 Instant를 LocalDateTime 또는 다른 유사한 클래스로 변환할 수 있습니다
		LocalDateTime date = LocalDateTime.ofInstant(timestamp, 
						ZoneId.of(ZoneId.SHORT_IDS.get("PST")));
		System.out.println("Date = "+date);
		
		//Calendar를 Instant로 변환
		Instant time = Calendar.getInstance().toInstant();
		System.out.println(time);
		//TimeZone를 ZoneId로 변환
		ZoneId defaultZone = TimeZone.getDefault().toZoneId();
		System.out.println(defaultZone);
		
		//특정 Calendar에서 ZonedDateTime 생성
		ZonedDateTime gregorianCalendarDateTime = new GregorianCalendar().toZonedDateTime();
		System.out.println(gregorianCalendarDateTime);
		
		//날짜 API를 레거시 클래스로 변환
		Date dt = Date.from(Instant.now());
		System.out.println(dt);
		
		TimeZone tz = TimeZone.getTimeZone(defaultZone);
		System.out.println(tz);
		
		GregorianCalendar gc = GregorianCalendar.from(gregorianCalendarDateTime);
		System.out.println(gc);
		
	}

}

출력:

Date = 2014-04-28T16:28:54.340
2014-04-28T23:28:54.395Z
America/Los_Angeles
2014-04-28T16:28:54.404-07:00[America/Los_Angeles]
Mon Apr 28 16:28:54 PDT 2014
sun.util.calendar.ZoneInfo[id="America/Los_Angeles",offset=-28800000,dstSavings=3600000,useDaylight=true,transitions=185,lastRule=java.util.SimpleTimeZone[id=America/Los_Angeles,offset=-28800000,dstSavings=3600000,useDaylight=true,startYear=0,startMode=3,startMonth=2,startDay=8,startDayOfWeek=1,startTime=7200000,startTimeMode=0,endMode=3,endMonth=10,endDay=1,endDayOfWeek=1,endTime=7200000,endTimeMode=0]]
java.util.GregorianCalendar[time=1398727734404,areFieldsSet=true,areAllFieldsSet=true,lenient=true,zone=sun.util.calendar.ZoneInfo[id="America/Los_Angeles",offset=-28800000,dstSavings=3600000,useDaylight=true,transitions=185,lastRule=java.util.SimpleTimeZone[id=America/Los_Angeles,offset=-28800000,dstSavings=3600000,useDaylight=true,startYear=0,startMode=3,startMonth=2,startDay=8,startDayOfWeek=1,startTime=7200000,startTimeMode=0,endMode=3,endMonth=10,endDay=1,endDayOfWeek=1,endTime=7200000,endTimeMode=0]],firstDayOfWeek=2,minimalDaysInFirstWeek=4,ERA=1,YEAR=2014,MONTH=3,WEEK_OF_YEAR=18,WEEK_OF_MONTH=5,DAY_OF_MONTH=28,DAY_OF_YEAR=118,DAY_OF_WEEK=2,DAY_OF_WEEK_IN_MONTH=4,AM_PM=1,HOUR=4,HOUR_OF_DAY=16,MINUTE=28,SECOND=54,MILLISECOND=404,ZONE_OFFSET=-28800000,DST_OFFSET=3600000]

레거시 TimeZone 및 GregorianCalendar 클래스의 toString() 메서드가 매우 장황하고 사용자 친화적이지 않음을 알 수 있습니다.

결론

I like this new Date Time API a lot. Some of the most used classes will be LocalDate and LocalDateTime. It’s very easy to work with the new classes. And, having similar methods that does a particular job makes it easy to find. It will take some time from moving legacy classes to new Date Time classes, but I believe it will be worth the time and effort.

Source:
https://www.digitalocean.com/community/tutorials/java-8-date-localdate-localdatetime-instant