Date/Time

  • Java
  • 2021년 12월 8일

1. 등장 배경

1) 명확하지 않은 클래스 이름

  • 날짜 클래스중 Date 는 시간과 TimeStamp 모두 표현할 수 있다. (사실상 TimeStamp)
  • 시간 값이 에폭타임 이라 하여 세계 표준시(UTC)로 1970년 1월 1일 00시 00분 00초를 기준으로 현재까지 흐른 모든 시간을 초(sec)단위로 표현 하여 사람이 알아보기 어렵다.

2) Thread safe하지 않은 mutable한 속성

public static void main(String[] args) throws InterruptedException {
    Date date = new Date();
    long time = date.getTime();
    System.out.println("date = " + date);
    Thread.sleep(1000 * 3);
    Date after3Seconds = new Date();
    System.out.println("after3Seconds = " + after3Seconds);

    after3Seconds.setTime(time);
    System.out.println("after3Seconds = " + after3Seconds);
}

/*
   [실행 결과]
   date = Thu Oct 28 20:22:24 KST 2021
   after3Seconds = Thu Oct 28 20:22:27 KST 2021
   after3Seconds = Thu Oct 28 20:22:24 KST 2021
*/

새로 생성한 after3Sceconds 라는 객체가 setter를 통해 다른 시간으로 변경이 된 것을 볼 수 있는데, 이는 Date 클래스가 mutable 하다는 것을 의미한다. mutable하기 때문에 thread unsafe 하다.

thread unsafe? Date 인스턴스의 값을 각각 다른 Thread에서 접근해서 변경이 가능하면 기존에 사용하던 Thread에서 변경 되어 잘못된 Date 정보를 가져와서 버그가 발생할 위험이 있다는 뜻.


3) 버그 발생할 여지가 많다.

사용법 자체에서도 사용법에 대해 오해할 수 있어 버그가 발생할 여지가 있다.

Calendar birthDay = new GregorianCalendar(1988, 6, 10);

위 코드를 보면 생일이 1988년 6월 10일임을 표현하고 싶지만, GregorianCalendar에서 month는 0부터시작하기 때문에 6을 넣으면 7월이라는 의미가 된다. 그래서 혼동하지 않기 위해서 상수 값을 쓰곤 했다. (ex: Calendar.JUNE)

하지만 이도 임시 방편이었고 암묵적으로 Java 8 이전에는 Joda-Time 을 사용했었다.



2. 디자인 철학

1) Clear

  • API 메소드는 명확해야한다.
  • 예를들어 클래스명이 Date지만 날짜 뿐 아니라 시간(Time)까지 다루게되면 명확하지 않다.
  • 시간(Time)역시 사람에게 익숙한 일반 시간이 아닌 에폭타임인 것은 Clear하지 않다.

2) Fluent

메소드가 null을 반환하는 경우가 없기 때문에 메소드 체이닝이 가능하기에 코드가 유려해진다.


3) Immutable

기존 날짜 인스턴스의 내용이 변하지 않으며 변경메소드 호출시 값이 변경되는게 아니라 새로운 날짜 인스턴스를 생성해 반환해야한다.

LocalDate today = LocalDate.of(2021, Month.OCTOBER, 28);
LocalDate nextMonth = today.plusMonths(1);

//LocalDateTime의 plusDays() 메서드
public LocalDateTime plusDays(long days) {
    LocalDate newDate = date.plusDays(days);
    return with(newDate, time);
}

private LocalDateTime with(LocalDate newDate, LocalTime newTime) {
    if (date == newDate && time == newTime) {
            return this;
    }
    return new LocalDateTime(newDate, newTime);
}

Date/Time의 모든 객체는 mutable한 속성의 단점을 해결하고자 Immutable한 속성을 갖고 설계가 되었는데 이 때문에 메서드를 이용해 날짜,시간을 변경하면 위에 정의된 with()함수를 사용하게 되고 with()함수는 새로운 객체를 만들어 반환하고 있는 것을 볼 수 있다.


4) Extensible

  • 확장 가능해야 한다.
  • 달력에는 윤달, 음력, 양력, 불교달력 등 다양한 종류의 달력의 확장이 가능해야 한다.



3. 기존 API와 추가된 API

1) 기존 API

  • java.util.Date
  • java.util.Calendar
  • java.text.SimpleDateFormat

2) 추가된 API

  • java.time.Instant : 기계시간
  • java.time.LocalDate : 날짜(시간x), 타임존x
  • java.time.LocalTime : 시간(날짜x), 타임존x
  • java.time.LocalDateTime : 날짜/시간, 타임존x
  • java.time.ZonedDateTime : 날짜/시간, 타임존o
  • java.time.DateTimeFormatter
  • java.time.Duration
  • java.time.Period
  • java.time.TemporalAdjuster



4. API 사용 예

1) Instant

Instant instant = Instant.now();
System.out.println(instant); //2021-11-02T14:07:19.218304100Z  (기준시 UTC 기준)

ZonedDateTime zonedDateTime = instant.atZone(ZoneId.systemDefault());
System.out.println(instant); //2021-11-02T23:08:45.188601700+09:00[Asia/Seoul]  (현재 KTC 기준)
  • Instant.now(): 현재 UTC(GMT)를 반환 (Universal Time Coordinated == Greenwich Mean Time)
  • ZoneId.systemDefault() : 현재 시스템상의 zone 정보를 반환 (ex: Asia/Seoul)
  • instant.atZone(zone) : UTC 기준이아닌 zone 의 위치 기반의 시간을 반환

2) LocalDate / LocalTime / LocalDateTime / ZonedDateTime

LocalDateTime now = LocalDateTime.now();    //서버의 시스템 zone 기준
System.out.println(now);

LocalDateTime of = LocalDateTime.of(1982, Month.JULU, 15,0,0,0);

ZonedDateTime nowInKorea = ZonedDateTime.now(ZoneId.of("Asia/Seoul"));
System.out.println(nowInKorea);

보통 사람이 읽고 쓰기 편한 시간 표현방식으로 표현해주는 API

  • LocalDateTime.now() : 현재 시스템 Zone에 해당하는(로컬) 일시를 반환
  • LocalDateTime.of(1988, Month.JUNE, 10, 0, 0, 0); : 로컬의 특정 일시를 반환
  • ZonedDateTime.now(ZoneId.of(“UTC”)) : 특정 Zone의 현재 시간을 반환합니다.
  • ZonedDateTime.of(1988, Month.JUNE.getValue(),10,0,0,0,0, ZoneId.of(“UTC”)) : 특정 Zone의 특정 일시를 반환합니다.

3) Duration / Period

//Period
LocalDate today = LocalDate.now();
LocalDate thisYearBirthDay = LocalDate.of(2022, Month.FEBRUARY,7);

Period period = Period.between(today, thisYearBirthDay);
System.out.println("생일까지 남은 기간 : " + period.getYears() + " 년 " + period.getMonths() + "월 " + period.getDays() + "일" );  //생일까지 남은 기간 : 0 년 3월 5일

Period p = today.until(thisYearBirthDay);
System.out.println("생일까지 남은 기간 : " + p.getYears() + " 년 " + p.getMonths() + "월 " + p.getDays() + "일" );  //생일까지 남은 기간 : 0 년 3월 5일

//Duration
Instant now = Instant.now();
Instant plus = now.plus(10,ChronoUnit.SECONDS);
Duration between = Duration.between(now,plus);
System.out.println(between.getSeconds());   //10

Period는 사람이 사용하는 날짜/시간의 기간을 측정, Duration은 초단위(나노,밀리)로 반환을 하기 때문에 주로 기계용 시간간의 기간을 측정하는데 사용할 수 있다.


시간 비교시 유용한 방식

위의 방식은 대부분 시간 메소드를 어떤식으로 사용해야하는 지에 대해서 이야기하는게 대부분인데, 실제로 제일 필요한 건 시간 비교가 제일 유용할 것 같아서 좀 더 정리해본다.

LocalDateTime time1 = LocalDateTime.of(2021, 11, 3, 10, 18, 0);
LocalDateTime time2 = LocalDateTime.of(2021, 11, 3, 10, 19, 0);

public boolean isAfter(ChronoLocalDate other)  //주어진날짜가 other보다 크면.. True

System.out.println(time1.isAfter(time2)); //false
System.out.println(time2.isAfter(time1)); //true

public boolean isBefore(ChronoLocalDate other) //주어진날짜가 other보다 작으면.. True

System.out.println(time1.isBefore(time2)); //true
System.out.println(time2.isBefore(time1)); //false

public boolean isEqual(ChronoLocalDate other) //주어진날짜가 other보다 같으면.. True

public int compareTo(ChronoLocalDate other)  //주어진날짜가 other보다 같으면 0, 크면 + 작으면 -
System.out.println(time1.compareTo(time2)); //-1
System.out.println(time2.compareTo(time1)); // 1
//날짜를 하루정도로 바꿔서 변환해보자 11/3 11/4 시간은 동일하게
System.out.println(time1.compareTo(time2)); //-1
System.out.println(time2.compareTo(time1)); // 1

public LocalDateTime truncatedTo(TemporalUnit unit)

isAfter,isBefore과 같은 메서드를 통해 비교를 수행할수 있다.

이때 truncatedTo() 메서드를 이용해서 시간을 잘라 내고 날짜만으로 비교가 가능한데 trucatedTo()메서드는 파라미터로 지정된 단위 이후의 값들을 버린 후, 복사한 LocalDateTime 객체를 리턴하는 메서드이다. 파라미터로 전달되는 단위는 ChronoUnit 클래스에 지정된 상수를 사용하며, DAYS보다 큰 단위인 YEARS, MONTHS 등의 값은 허용되지 않는다.

LocalDateTime time1 = LocalDateTime.of(2021, 11, 3, 10, 18, 3);
System.out.println(time1.truncatedTo(ChronoUnit.SECONDS)); //2021-11-03T10:18:03
System.out.println(time1.truncatedTo(ChronoUnit.MINUTES)); //2021-11-03T10:18
System.out.println(time1.truncatedTo(ChronoUnit.HOURS)); //2021-11-03T10:00
System.out.println(time1.truncatedTo(ChronoUnit.DAYS)); //2021-11-03T00:00

현 기준 날짜로 절삭 해서 사용가능히고 현 시간을 시점으로 + -도 ChronoUnit으로 더 효율적으로 할 수 있다.

LocalDateTime time1 = LocalDateTime.of(2021, 11, 3, 10, 18, 3);
LocalDateTime time2 = LocalDateTime.of(2021, 11, 4, 10, 18, 0);

//3일이 더해짐.
System.out.println(time1.plus(3, ChronoUnit.DAYS)); //2021-11-06T10:18:03
//3일이 빠짐.
System.out.println(time1.minus(3, ChronoUnit.DAYS)); //2021-11-06T10:18:03
//구체적으로 3년이 이런식으로 추가도 가능하다.
System.out.println(time1.plusYears(3));//2024-11-03T10:18:03
//빼는것도 ㅆㄱㄴ
System.out.println(time1.minusYears(3)); //2018-11-03T10:18:03

//아니면 이런식으로 사용할수도 있다.
ChronoUnit.HOURS.between(time1, time2); //23 (23.x 분 차이이기 때문에)

truncatedTo가 날짜부터는 자를 수 없는 이유

일수부터는 생략한다는 개념이 모호하다.예를 들어, day에 0이 오는것도 말이 안되며 1이 온다고 해도 일수를 잘라내 정확히 년,월을 뜻하는게 아니라 년,월,1일을 뜻하는 것이기 때문에 매개변수로 올 수 가 없는 것이다.

하지만 해당해의 1일을 만약에 표시하고 싶을경우에는 TemporalAdjusters(시간 조정기) 를 이용할 수 있다.

date.with(TemporalAdjusters.firstDayOfMonth()).truncatedTo(ChronoUnit.DAYS); //2021-11-01T00:00
date.with(TemporalAdjusters.firstDayOfYear()) : 해당 년도의 1월 1일 //2021-01-01T17:02:22.973160900
date.with(TemporalAdjusters.firstDayOfMonth()) : 해당 월의 1일 //2021-11-01T17:03:05.777745100
date.with(TemporalAdjusters.lastDayOfYear()) : 해당 년도의 마지막 날짜 //2021-12-31T17:03:34.531656400
date.with(TemporalAdjusters.lastDayOfMonth()) : 해당 월의 마지막 날짜 //2021-11-30T17:04:03.837483400

ChronoUnit

alter-text

4) DateTimeFormatter

//formatting
LocalDateTime now = LocalDateTime.now();

DateTimeFormatter formatter = DateTimeFormatter.ISO_DATE_TIME;
System.out.println(now.format(formatter));      //2021-11-02T23:28:51.388655

formatter = DateTimeFormatter.ofPattern("MM.dd.yyyy");
System.out.println(now.format(formatter));      //11.02.2021

DateTimeFormatter은 ofPattern()을 이용해 특정 패턴을 지정할 수 있고 미리 정의된 포맷터들이 존재하는데 이 형식을 이용하고자하면 굳이 새로 정의해줄 필요가 없다. 정의된 포맷터들은 여기 를 참고하자.

이때, Formatter에 사용되는 형식은 다음처럼 사용 가능하다.

  Symbol  Meaning                     Presentation      Examples
  ------  -------                     ------------      -------
   G       era                         text              AD; Anno Domini; A
   u       year                        year              2004; 04
   y       year-of-era                 year              2004; 04
   D       day-of-year                 number            189
   M/L     month-of-year               number/text       7; 07; Jul; July; J
   d       day-of-month                number            10

   Q/q     quarter-of-year             number/text       3; 03; Q3; 3rd quarter
   Y       week-based-year             year              1996; 96
   w       week-of-week-based-year     number            27
   W       week-of-month               number            4
   E       day-of-week                 text              Tue; Tuesday; T
   e/c     localized day-of-week       number/text       2; 02; Tue; Tuesday; T
   F       week-of-month               number            3

   a       am-pm-of-day                text              PM
   h       clock-hour-of-am-pm (1-12)  number            12
   K       hour-of-am-pm (0-11)        number            0
   k       clock-hour-of-am-pm (1-24)  number            0

   H       hour-of-day (0-23)          number            0
   m       minute-of-hour              number            30
   s       second-of-minute            number            55
   S       fraction-of-second          fraction          978
   A       milli-of-day                number            1234
   n       nano-of-second              number            987654321
   N       nano-of-day                 number            1234000000

   V       time-zone ID                zone-id           America/Los_Angeles; Z; -08:30
   z       time-zone name              zone-name         Pacific Standard Time; PST
   O       localized zone-offset       offset-O          GMT+8; GMT+08:00; UTC-08:00;
   X       zone-offset 'Z' for zero    offset-X          Z; -08; -0830; -08:30; -083015; -08:30:15;
   x       zone-offset                 offset-x          +0000; -08; -0830; -08:30; -083015; -08:30:15;
   Z       zone-offset                 offset-Z          +0000; -0800; -08:00;

   p       pad next                    pad modifier      1

   '       escape for text             delimiter
   ''      single quote                literal           '
   [       optional section start
   ]       optional section end
   #       reserved for future use
   {       reserved for future use
   }       reserved for future use

LocalDateTime now = LocalDateTime.now();
formatter = DateTimeFormatter.ofPattern("MM.dd.yyyy");
System.out.println(now.format(formatter));      //11.02.2021

//parsing
LocalDate parse = LocalDate.parse("08.12.2021",formatter);
System.out.println(parse);

또한, Date/Time타입의 parse()메서드를 통해서 특정 문자열을 ofPattern에서 선언한 패턴 방식으로 파싱하여 LocalDate 타입의 인스턴스를 생성해 반환할 수도 있다.


5) 레거시 API 지원

예전에 구현 및 사용하던 날짜와 시간(Date) API와 현재 추가된 LocalDate, LocalDateTime, Instant는 서로 호환 된다!

public static void main(String[] args) {
    Date date = new Date();
    Instant instant = date.toInstant();
    Date newDate = Date.from(instant);

    GregorianCalendar gregorianCalendar = new GregorianCalendar();
    LocalDateTime dateTime = gregorianCalendar.toInstant()
            .atZone(ZoneId.systemDefault())
            .toLocalDateTime();
    ZonedDateTime zonedDateTime = ZonedDateTime.from(dateTime);

    GregorianCalendar from = GregorianCalendar.from(zonedDateTime);

    ZoneId zoneId = TimeZone.getTimeZone("PST").toZoneId();
    TimeZone timeZone = TimeZone.getTimeZone(zoneId);
}
  1. GregorianCalendar와 Date 타입의 인스턴스를 Instant나 ZonedDateTime으로 변환
  2. java.util.TimeZone에서 java.time.ZoneId로 상호 변환 가능





Reference

백기선 인프런 강의 : 더 자바, Java8

(Java) 날짜 비교하기 ( LocalDate, LocalDateTime, Date, Calendar)

(Java8 Time API) Duration과 Period 사용법 (+ChronoUnit)

https://codeblog.jonskeet.uk/2017/04/23/all-about-java-util-date/

Tags :

Related Posts

[WSL2] 포트포워딩과 window에서 workbench로 접속하기

[WSL2] 포트포워딩과 window에서 workbench로 접속하기

WSL2를 이용하여 개발을 진행서 외부에서 접근하고 싶거나, 배포를 위해 접근하고 싶을 수가 있는데, 문제가 되는 것이 WSL2는 VM과 같은 환경이라 별도의 IP를 갖는다는 점이다. 그러면 포트포워딩을 하면 되지 않느냐라고 할 수 있는데 맞다 포트포워딩을 하면된다 하지만 재부팅을 할때마다 변경되는 IP에 매번 포트 포워딩을 할 수 없는 노릇이기에 Powershell 파일을 이용하여 재부팅마다 wsl2의 ip를 잡아 특정 포트를 포트포워딩 하는 방법을 남기려고 한다....

Read More
[MST] Prim 알고리즘

[MST] Prim 알고리즘

우선순위 큐의 방법을 이용하는 알고리즘으로 vertex를 한개씩 선택하며 최소 비용의 edge를 찾는 방법이다. decrease-key의 개념을 이용하며 decrease-key는 현재 계산된 v노드까지의 거리보다 현재 노드 u부터 v까지의 경로가 더 작다면 값을 갱신해주는 방법을 이용한다. 1. 특징 정점 선택 기반 시작 정점부터 출발하여 해당 노드까지의 최소 비용을 기록하는 배열을 이용하여 구하는 방식 자료구조중 하나인 우선순위 큐를 이용하며, 우선순위 큐를 어떻게 구현했는가가 시간복잡도에 영향 2. Pesudo Code 3. 구현 방법 vertex들의 key값을 Infinity로 초기화 start vertex의 key값을 0으로 초기화 (어떤 vertex를 선택하더라고 MST가 나온다.) 현재 vertex에 인접한 vertex들 중 선택하지 않았고, 가장 vertex의 key값이 작은 vertex을 찾기 (exract-min = 최소값 추출) 현재 vertex를 선택 인접한 vertex중 vertex의 key값보다 간선의 가중치가 더 작다면 key값을 가중치로 갱신 (decrease -key) 인접한 vertex중 선택하지 않았고, 가장 vertex의 key값이 작은 vertex를 기준으로 3번부터 다시 반복 모든 vertex가 선택되었다면 종료 1) 인접 행렬 위에 설명한 3번 방법의 extract-min을 아래와 같이 배열로 구현하며, 매번 V회 반복한다....

Read More
Tree

Tree

그래프의 일종으로, 여러 노드가 한개의 노드를 가리킬 수 없는 구조 선형구조가 아닌 (비선형), 부모자식의 관계를 가지는 계층형 구조 1. 용어 개념 (설명) Node (노드): 트리를 구성하고 있는 각각의 요소를 의미한다. Edge (= link, 간선) : 트리를 구성하기 위해 노드와 노드를 연결하는 선을 의미한다. Root Node (루트 노드) : 트리 구조에서 최상위에 있는 노드를 의미한다. Sibling (형제 노드) : 같은 부모를 갖는 노드 Degree (차수) : 해당 노드의 자식노드 개수 Depth (깊이) : 루트 노드 부터 현재 노드까지의 간선의 개수 Level (레벨) : 같은 깊이를 갖는 노드들의 집합 루트 노드의 레벨을 1이 아닌 0으로 시작할 수도 있다. 편한대로 하면 된다. Height (높이) : 트리의 최대 레벨 Terminal Node (= leaf Node, 단말 노드) : 하위에 다른 노드가 연결되어 있지 않은 노드를 의미한다. Internal Node (내부노드, 비단말 노드) : 노드를 제외한 모든 노드로 루트 노드를 포함한다. https://www.gowoonsori.com/images/datastructure/tree/perfect2.png does not exist 위의 트리로 예를 들어 설명하자면, 아래와 같이 된다....

Read More