CHAP12 - Modern-Java-in-Action/Online-Study GitHub Wiki
기존 Date
, Calendar
문제점을 보완하기 위해, java.time
패키지가 추가되었다.
- Date 패키지의 문제점(1.1부터 deprecated)
- 특정 시점을 날짜가 아닌 밀리초 단위로 표현하며, 오프셋은 1900년을 기준으로 함.
- 달 인덱스가 0부터 시작함.
- JVM 기본시간대인 중앙유럽시간대를 사용하나, 시간대 정보를 가지고 있지는 않음.
- DateFormat 기능을 지원하나 스레드에 안전하지 않음.
- 가변 클래스여서 함수형 프로그래밍 방법론 적용시 유지보수가 어려움.
- Calendar
- 달의 인덱스가 0부터 시작함.
- Date와 비교해서 DateFormat등 일부 기능을 사용할 수 없음.
- 가변 클래스임.
-
주요특징
- 모두 불변형이다.
- 값을 바꾸면 새로운 객체를 생성해서 반환한다.
-
특정시점의 시간을 표현
- LocalDate : 시간대 정보와 시/분/초 정보없이, 연/월/일.
- LocalTime : 시간대 정보와 연/월/일 정보없이, 시/분/초.
- LocalDateTime : 시간대 정보없이, 연/월/일/시/분/초.
- Instant : 유닉스 에포크시간을 기준으로 특정 지점까지 시간을 밀리초로 표현.
- 유닉스 에포크시간 : 1970년 1월 1일 0시 0분 0초 UTC
- UTC 세계표준시 / 그리니치표준시 / 한국시간-9
-
특정시점간 시간 차이를 표현
- Duration : 시/분/초 단위로 특정 지점간 시간 표현.
- LocalTime 사용불가, LocalDate/LocalDateTime 사용가능(1일을 24시간으로 표현)
- Period : 연/월/일 단위로 특정 지점간 시간 표현.
- LocalDate 사용불가, LocalTime/LocalDateTime 사용가능(24시간을 1일로 표현)
- Duration : 시/분/초 단위로 특정 지점간 시간 표현.
객체생성 및 사용방법
LocalDate date = LocalDate.of(2017, 9, 21);
int year = date.getYear();
Month month = date.getMonth();
int day = date.getDayOfMonth();
DayOfWeek dow = date.getDayOfWeek();
LocalDate date2 = LocalDate.parse("2017-09-21");
객체 멤버변수 접근방법
LocalDate nowDate = LocalDate.now();
System.out.println(nowDate);
//get메서드에 ChronoField Enum을 넘겨도 된다
int y = date.get(ChronoField.YEAR);
int m = date.get(ChronoField.MONTH_OF_YEAR);
int d = date.get(ChronoField.DAY_OF_MONTH);
System.out.println(y+" "+m+" "+d);
//항목마다 정의된 get메서드를 호출해도 된다
int y2 = date.getYear();
int m2 = date.getMonthValue();
int d2 = date.getDayOfMonth();
System.out.println(y2+" "+m2+" "+d2);
유용한 기능
int len = date.lengthOfMonth(); //달마다 일 수
boolean leap = date.isLeapYear(); //윤년여부를 알 수 있음
System.out.println(month.getValue()); //달은 1월부터 1, 12월이 12
System.out.println(dow.getValue()); //요일은 월요일부터 1, 일요일이 7
객체생성 및 사용방법
//LocalTime
LocalTime time = LocalTime.of(13, 45, 20);
int hour = time.getHour();
int minute = time.getMinute();
int second = time.getSecond();
//문자열을 파싱해서 만들 수도 있음
LocalTime time2 = LocalTime.parse("13:45:20");
//LocalDateTime은 날짜, 시간 모두를 표현
LocalDateTime dt1 = LocalDateTime.of(2017, Month.SEPTEMBER, 21, 13, 45, 20);
System.out.println(Month.SEPTEMBER);
LocalTime, LocalDate를 변환할 수 있다.
LocalDateTime dt2 = LocalDateTime.of(date, time);
LocalDateTime dt3 = date.atTime(13, 45, 20);
LocalDateTime dt4 = date.atTime(time);
LocalDateTime dt5 = time.atDate(date);
LocalDate date3 = dt1.toLocalDate();
LocalTime time3 = dt1.toLocalTime();
Instant는 시간을 밀리초단위로 표현한다.
//Instant
System.out.println(Instant.ofEpochSecond(3));
System.out.println(Instant.ofEpochSecond(3, 0));
System.out.println(Instant.ofEpochSecond(2, 1_000_000_000));
System.out.println(Instant.ofEpochSecond(4, -1_000_000_000));
인스턴트에서 바로 연/월/일/시/분/초 정보를 읽으려고하면 예외를 던진다. LocalDate 등으로 변환하고 읽어야 한다.
try{
int day = Instant.now().get(ChronoField.DAY_OF_MONTH);
}catch(UnsupportedTemporalTypeException e){
e.printStackTrace();
}
//변환할 때는 시간대정보를 지정해주어야 한다. 시간대정보는 다다음절에서 다룸.
int day = DateTime.ofInstant(Instant.now(), ZoneOffset.UTC).get(ChronoField.DAY_OF_MONTH);
Duration은 특정시점간 시간차를 시/분/초로 표현하므로 LocalDate에는 사용할 수 없다.
//Duration
LocalTime time1 = LocalTime.now();
LocalTime time2 = time1.plusHours(1);
LocalDate dateTime1 = LocalDate.now();
LocalDate dateTime2 = dateTime1.plusDays(1);
Instant instant1 = Instant.now();
Instant instant2 = instant1.plusMillis(1000);
Duration d1 = Duration.between(time1, time2);
System.out.println("d1: "+d1);
try{ //Duration은 초와 나노초로 시간단위를 표현하므로 LocalDate는 사용할 수 없다.
Duration d2 = Duration.between(dateTime1, dateTime2);
System.out.println("d2: "+d2);
}catch(UnsupportedTemporalTypeException e){
//e.printStackTrace();
}
Duration d3 = Duration.between(instant1, instant2);
System.out.println("d3: "+d3);
Period는 특정 시점간 시간차를 연/월/일로 표현한다. 따라서 LocalTime에는 사용할 수가 없다. LocalDateTime으로 계산해도 예외를 던진다.
LocalDateTime ldt1 = LocalDateTime.now();
LocalDateTime ldt2 = ldt1.plusYears(1);
Duration test = Duration.between(ldt1, ldt2);
System.out.println("duration test: "+test);
//Period test2 = Period.between(ldt1, ldt2); //예외를 던진다
Period test2 = Period.between(ldt1.toLocalDate(), ldt2.toLocalDate());
System.out.println("period test: "+test2);
//period test: P1Y
다만, Duration으로 LocalDateTime간 시간차를 계산하면 연/월/일을 시간으로 변환해서 출력한다.
LocalDateTime ldt1 = LocalDateTime.now();
LocalDateTime ldt2 = ldt1.plusYears(1);
Duration test = Duration.between(ldt1, ldt2);
System.out.println("duration test: "+test);
//duration test: PT8760H
-
Duration과 Period가 공통으로 제공하는 메서드
//with으로 값을 지정
LocalDate date1 = LocalDate.of(2017, 9, 21);
LocalDate date2 = date1.withYear(2011);
LocalDate date3 = date2.withDayOfMonth(25);
LocalDate date4 = date3.with(ChronoField.MONTH_OF_YEAR, 2);
//plus, minus로 연산
LocalDate date22 = date1.plusWeeks(1);
LocalDate date33 = date22.minusYears(1);
LocalDate date44 = date33.plus(6, ChronoUnit.MONTHS);
-
날짜/시간 클래스의 공통 메서드
다음 코드를 실행했을 때 date의 변숫값은?
LocalDate date = LocalDate.of(2014, 3, 18);
date = date.with(ChronoField.MONTH_OF_YEAR, 9);
date = date.plusYears(2).minusDays(10);
date.withYear(2011);
정답
2016-09-08LocalDate date1 = LocalDate.now(); //2022-01-21
LocalDate date2 = date1.with(nextOrSame(DayOfWeek.SUNDAY)); //2022-01-23
LocalDate date3 = date2.with(lastDayOfMonth()); //2022-01-31
-
Temporal
TemporalAdjuster 인터페이스를 구현한 것이 TemporalAdjusters이다. 커스텀으로 직접 만들 수도 있다. 이를테면 다음 근무일을 구하는 TemporalAdjuster는 다음과 같이 만들 수 있다.
- TemporalAdjuster를 클래스로 구현하고 인스턴스를 생성해 인자로 넘기기
LocalDate date = LocalDate.now().with(new NextWorkingDay());
import java.time.DayOfWeek;
import java.time.temporal.ChronoField;
import java.time.temporal.ChronoUnit;
import java.time.temporal.Temporal;
import java.time.temporal.TemporalAdjuster;
public class NextWorkingDay implements TemporalAdjuster {
@Override
public Temporal adjustInto(Temporal temporal) {
DayOfWeek dow = DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));
int dayToAdd = 1;
if(dow == DayOfWeek.FRIDAY) dayToAdd = 3;
else if(dow == DayOfWeek.SATURDAY) dayToAdd = 2;
return temporal.plus(dayToAdd, ChronoUnit.DAYS);
}
}
- TemporalAdjuster를 람다로 구현해 인자로 넘기기
LocalDate date1 = LocalDate.now().with(
temporal -> {
DayOfWeek dow = DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));
int dayToAdd = 1;
if(dow==DayOfWeek.FRIDAY) dayToAdd = 3;
else if(dow==DayOfWeek.SATURDAY) dayToAdd = 2;
return temporal.plus(dayToAdd, ChronoUnit.DAYS);
}
);
- 람다로 구현한 TemporalAdjuster를 변수에 저장했다가 사용하기
TemporalAdjuster nextWorkingDay = TemporalAdjusters.ofDateAdjuster(
temporal -> {
DayOfWeek dow = DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));
int dayToAdd = 1;
if(dow==DayOfWeek.FRIDAY) dayToAdd = 3;
else if(dow==DayOfWeek.SATURDAY) dayToAdd = 2;
return temporal.plus(dayToAdd, ChronoUnit.DAYS);
}
);
LocalDate date2 = LocalDate.now().with(nextWorkingDay);
출력하기
LocalDate date = LocalDate.of(2014, 3, 18);
System.out.println(date.format(DateTimeFormatter.BASIC_ISO_DATE));
//20140318
System.out.println(date.format(DateTimeFormatter.ISO_LOCAL_DATE));
//2014-03-18
파싱하기
LocalDate date2 = LocalDate.parse("20140318", DateTimeFormatter.BASIC_ISO_DATE);
LocalDate date3 = LocalDate.parse("2014-03-18", DateTimeFormatter.ISO_LOCAL_DATE);
System.out.println(date2);
System.out.println(date3);
포맷 정하기
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
LocalDate date4 = LocalDate.now();
String formattedDate = date4.format(formatter);
System.out.println(formattedDate);
//21/01/2022
LocalDate date5 = LocalDate.parse(formattedDate, formatter);
System.out.println(date5);
//2022-01-21
포맷 정하고, Locale지정하기
DateTimeFormatter timeFomatterItalian = DateTimeFormatter.ofPattern("dd/MMMM/yyyy", Locale.ITALIAN);
String formattedDateItalian = LocalDate.now().format(timeFomatterItalian);
DateTimeFormatter timeFomatterKorean = DateTimeFormatter.ofPattern("dd/MMMM/yyyy", Locale.KOREAN);
String formattedDateKorean = LocalDate.now().format(timeFomatterKorean);
System.out.println(formattedDateItalian); //21/gennaio/2022
System.out.println(formattedDateKorean); //21/1월/2022
메서드 체인 방식으로 포맷을 정하기
DateTimeFormatter koreanFormatter = new DateTimeFormatterBuilder()
.appendLiteral("'")
.appendText(ChronoField.YEAR)
.appendLiteral(".")
.appendText(ChronoField.MONTH_OF_YEAR)
.appendLiteral(".")
.appendText(ChronoField.DAY_OF_MONTH)
.appendLiteral(".")
.appendLiteral("(")
.appendText(ChronoField.DAY_OF_WEEK)
.appendLiteral(")")
.parseCaseInsensitive()
.toFormatter(Locale.KOREA);
String formattedDateKo2 = LocalDate.now().format(koreanFormatter);
System.out.println(formattedDateKo2);
//'2022.1월.21.(금요일)
-
ZonedDateTime : 시간대 정보를 포함한 날짜, 시간 정보
ZoneId romeZone = ZoneId.of("Europe/Rome");
ZoneId korZone = ZoneId.of("Asia/Tokyo");
LocalDateTime dateTime = LocalDateTime.now();
ZonedDateTime zdt2 = dateTime.atZone(romeZone);
System.out.println(zdt2); //한국시간 기준 로마시간대로 변환해줌 > 왜?
zdt2 = dateTime.atZone(korZone);
System.out.println(zdt2); //한국시간대로 변환해줌
-
여기서 의문, LocalDateTime을 ZonedDateTime으로 바꿀 때 왜 기준시간을 한국시간으로 잡았을까, 그리고 그 과정은 어떻게 이뤄질까?
- 두번째 인수로 기준 시간대를 넘길 수 있다.
- 없다면 ZoneRules에 정의된 기준대로 해당시각에 맞는 시간대를 찾아 기준 시간대로 삼는다(UTC를 구하고 얼마나 차이나는지 확인한다.)
- 그래서 한국에서 생성한 LocalDateTime 객체를 런던에서 ZonedDateTime으로 바꾼다면 의도하지 않은 오류가 발생할 수 있다.
//호환성을 위해 Instant로 작업하는것이 유리
LocalDateTime dt = LocalDateTime.ofInstant(Instant.now(), ZoneOffset.UTC);
LocalDateTime dt2 = LocalDateTime.ofInstant(Instant.now(), ZoneId.of("Europe/Rome"));
LocalDateTime dt3 = LocalDateTime.ofInstant(Instant.now(), ZoneId.of("Asia/Tokyo"));
System.out.println(dt); //2022-01-21T10:47:21.983854600
System.out.println(dt2); //2022-01-21T11:47:21.983854600
System.out.println(dt3); //2022-01-21T11:47:21.983854600
시간대별 호환성을 유지하기 위해 Instant로 작업하는 것이 유리.
일본달력 계산하기.
LocalDate date = LocalDate.of(2014, Month.MARCH, 18);
JapaneseDate japaneseDate = JapaneseDate.from(date);
System.out.println(japaneseDate); //Japanese Heisei 26-03-18
이슬람력 계산하기.
HijrahDate ramadanDate = HijrahDate.now()
.with(ChronoField.DAY_OF_MONTH, 1)
.with(ChronoField.MONTH_OF_YEAR, 9);
System.out.println("Ramadan starts on " + IsoChronology.INSTANCE.date(ramadanDate)
+ " and ends on" + IsoChronology.INSTANCE.date(ramadanDate.with(TemporalAdjusters.lastDayOfMonth())));
마야달력, 태음태양력 등 지역화된 달력 시스템을 만들 수 있다. 다만, 1년이 12개월, 1달은 31일 이하, 1년이 정해진 수의 달로 이루어진다는 일반적인 가정을 적용할 수 없다. 따라서 프로그램 입출력을 지역화하는 상황을 제외하면 LocalDate를 사용해야 한다.
Chronology japaneseChronology = Chronology.ofLocale(Locale.JAPAN);
ChronoLocalDate now = japaneseChronology.dateNow();
System.out.println(now);
-
(10분) The Problem with Time & Timezones - Computerphile - YouTube
-
간단한 예시
- 율리우스력에서 그레고리력으로 바꿀 때, 오차를 조정하기 위해 열흘을 건너뛰었다.
- 나라마다 그레고리력을 받아들인 시기가 다르다.
- 같은 장소에서 다른 시간대를 쓴다.
- 이스라엘의 유대인과 팔레스타인끼리
- 미국 내 인디언보호구역
- 일부 국가에서 여름에 한시간 당겨 생활하는 서머타임을 적용한다.
- 같은 국가라도 서머타임 적용여부가 지역에 따라 달라진다.
- 남반구와 북반구와 계절이 반대이므로, 남반구는 서머타임을 11월에 적용한다.
- 일부 국가는 소속 시간대를 변경한 경우가 있다.
- 달력의 오차를 조정하기 위해 '윤초'를 사용한다.
- 윤초가 삽입된 날의 마지막을 59 / 59 / 00으로 하면 1초만큼의 시간 딜레이가 생긴다.
- 윤초가 삽입된 날의 마지막을 59 / 60 / 00으로 하면 모든 시간처리 라이브러리를 재정의해야 한다.
- 윤초를 24 * 60 * 60으로 나누어서 윤초가 삽입된 날은 1초마다 a밀리초를 더해주면, 시간동기화/시간처리 문제는 해결되나 1초가 부정확해진다.
- 율리우스력에서 그레고리력으로 바꿀 때, 오차를 조정하기 위해 열흘을 건너뛰었다.
-
합리적인 대안 : joda-time 등 이미 검증된 라이브러리를 사용하고, 위에서 언급한 문제를 혼자 해결하려는 생각은 버린다.