CHAP12 - Modern-Java-in-Action/Online-Study GitHub Wiki

새로운 날짜와 시간 API

기존 Date, Calendar 문제점을 보완하기 위해, java.time 패키지가 추가되었다.

  • Date 패키지의 문제점(1.1부터 deprecated)
    • 특정 시점을 날짜가 아닌 밀리초 단위로 표현하며, 오프셋은 1900년을 기준으로 함.
    • 달 인덱스가 0부터 시작함.
    • JVM 기본시간대인 중앙유럽시간대를 사용하나, 시간대 정보를 가지고 있지는 않음.
    • DateFormat 기능을 지원하나 스레드에 안전하지 않음.
    • 가변 클래스여서 함수형 프로그래밍 방법론 적용시 유지보수가 어려움.
  • Calendar
    • 달의 인덱스가 0부터 시작함.
    • Date와 비교해서 DateFormat등 일부 기능을 사용할 수 없음.
    • 가변 클래스임.

12.1 LocalDate, LocalTime, Instant, Duratioin, Period

  • 주요특징

    • 모두 불변형이다.
    • 값을 바꾸면 새로운 객체를 생성해서 반환한다.
  • 특정시점의 시간을 표현

    • 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일로 표현)

LocalDate

객체생성 및 사용방법

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
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은 날짜, 시간 모두를 표현
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는 시간을 밀리초단위로 표현한다.

//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/Period

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가 공통으로 제공하는 메서드

    ModernJavaDurationPeriodCommonMethod

12.2 날짜 조정, 파싱, 포매팅

날짜 조정 : 절대적 with, 상대적 plus/minus

//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);
  • 날짜/시간 클래스의 공통 메서드

    ModernJavaDateTimeCommonMethod

퀴즈12-1: LocalDate 조정

다음 코드를 실행했을 때 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-08

TemporalAdjusters로 날짜조정 커스텀 메서드 만들기

LocalDate 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

    ModernJavaTemporalAdjusters

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.(금요일)

12.3 다양한 시간대와 캘린더 활용 방법

ZonedDateTime

  • ZonedDateTime : 시간대 정보를 포함한 날짜, 시간 정보

    ModernJavaZonedDateTimeConcepts

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);

The Problem with Time & Timezones - Computerphile

  • (10분) The Problem with Time & Timezones - Computerphile - YouTube

  • 간단한 예시

    • 율리우스력에서 그레고리력으로 바꿀 때, 오차를 조정하기 위해 열흘을 건너뛰었다.
      • 나라마다 그레고리력을 받아들인 시기가 다르다.
    • 같은 장소에서 다른 시간대를 쓴다.
      • 이스라엘의 유대인과 팔레스타인끼리
      • 미국 내 인디언보호구역
    • 일부 국가에서 여름에 한시간 당겨 생활하는 서머타임을 적용한다.
      • 같은 국가라도 서머타임 적용여부가 지역에 따라 달라진다.
      • 남반구와 북반구와 계절이 반대이므로, 남반구는 서머타임을 11월에 적용한다.
    • 일부 국가는 소속 시간대를 변경한 경우가 있다.
    • 달력의 오차를 조정하기 위해 '윤초'를 사용한다.
      • 윤초가 삽입된 날의 마지막을 59 / 59 / 00으로 하면 1초만큼의 시간 딜레이가 생긴다.
      • 윤초가 삽입된 날의 마지막을 59 / 60 / 00으로 하면 모든 시간처리 라이브러리를 재정의해야 한다.
      • 윤초를 24 * 60 * 60으로 나누어서 윤초가 삽입된 날은 1초마다 a밀리초를 더해주면, 시간동기화/시간처리 문제는 해결되나 1초가 부정확해진다.
  • 합리적인 대안 : joda-time 등 이미 검증된 라이브러리를 사용하고, 위에서 언급한 문제를 혼자 해결하려는 생각은 버린다.

⚠️ **GitHub.com Fallback** ⚠️