Java Date - Instant, LocalDateTime, ZonedDateTime
( 이미지 출처 : https://openjdk.java.net )
JDK 8부터는 Instant, LocalDateTime , ZonedDateTime 등이 추가됐다. 이 들은 immutable하고 thread-safe 하기 때문에 더 편하고 안전하게 날짜와 시간을 다룰 수 있게 되었습니다.
Date의 대부분의 기능은 JDK 1.1부터 deprecated 되었고 JDK 7까지는 Calendar 혹은 GregorianCalendar를 이용해서 날짜와 시간을 다뤄왔지만 JDK 8부터는 그럴 필요가 없어졌습니다.
java.time package in JDK 8
JDK 8 에서는 dates, times, instants과 durations을 위해 java.time 패키지를 추가했다. 이 패키지에서 제공하는 모든 class들은 immutable하기 때문에 thread-safe합니다.
-
Instant : timestamp(UTC 1970-01-01T00:00:00Z 로부터 흐른 시간)를 다룹니다.
- LocalDateTime : time-zone 을 제외한 date-time 값을 다룹니다.
- LocalDate : time-zone 을 제외한 date 을 다룹니다.
-
LocalTime : time-zone 을 제외한 time 을 다룹니다.
- ZonedDateTime : time-zone 을 포함한 date-time 값을 다룬다.
java.util.Date 의 문제점
d2.naver.com - Java의 날짜와 시간 API 글에서 java.util.Date 클래스의 문제점을 세세하게 설명해주고 있습니다.
- 불변 객체가 아니다
- int 상수 필드의 남용
- 헷갈리는 월 지정
- 일관성 없는 요일 상수
- Date와 Calendar의 불편한 역할 분담
- 오류에 둔감한 시간대 ID지정
- java.util.Date 하위 클래스의 문제
그 중 가장 “불변 객체가 아니다”는 불편함을 넘어서 예상하기 힘든 기능 오류를 발생시킬 수 있습니다.
Mutable 객체
아래 코드의 출처는 “Effective Java 3/E 한글판 303 page, 아이템50” 입니다.
기간을 표현하는 아래와 같은 Period 클래스가 있습니다.
public final class Period {
private final Date start;
private final Date end;
pulbic Period(Date start, Date end) {
if (start.compareTo(end)>0) {
throw new IllegalArgumentException(start + " is later than " + end);
}
this.start = start;
this.end = end;
}
public Date start() {
return start;
}
public Date end() {
return end;
}
...(생략)
}
Period 객체가 가진 Date 객체는 final
도 적용되고 캡슐화 되어있어서 얼핏 불변처럼 보이고 시작 시각이 종료시각보다 늦을 수 없다는 불변식이 지켜질 수 있을 것 처럼 보입니다. 하지만 mutable한 Date 객체가 client에게 제공되었기 때문에 그 불변식은 깨질 수 있습니다.
Date start = new Date();
Date end = new Date();
Period period = new Period(start, end);
end.setYear(78);
Period의 생성자에서 시작시각과 종료시각에 대한 validation이 진행됐지만 그 이 후 client가 종료시각의 year을 수정함으로써 Period의 불변식도 깨지게 되었습니다.
이와 같은 상황은 client의 의도가 있었다면 해킹으로 이어질 수 있고 의도가 없었다면 원인을 쉽게 찾기 힘든 기능 오류로 이어질 수 있습니다.
해결방법
Immutable 객체 사용
앞서 이야기 드렸듯이 JDK 8 부터는 Instant, LocalDateTime , ZonedDateTime 등의 immutable 객체가 제공되기 때문에 쉽게 해결가능합니다.
public final class Period {
private final LocalDateTime start;
private final LocalDateTime end;
pulbic Period(LocalDateTime start, LocalDateTime end) {
if (start.isAfter(end)) {
throw new IllegalArgumentException(start + " is later than " + end);
}
this.start = start;
this.end = end;
}
public LocalDateTime start() {
return start;
}
public LocalDateTime end() {
return end;
}
...(생략)
}
LocalDateTime 객체는 불변이기 때문에 end 의 값을 변경할 방법이 없습니다. 결국 Period 객체도 불변 객체가 됩니다.
LocalDateTime start = LocalDateTime.now();
LocalDateTime end = LocalDateTime.now();
Period period = new Period(start, end);
// end 를 변경할 방법 이 없습니다.
Defensive Copy
JDK 7 이전에는 방어적으로 Date의 복사본을 사용함으로써 Period 객체를 불변 객체로 만들 수 있습니다.
public final class Period {
private final Date start;
private final Date end;
pulbic Period(Date start, Date end) {
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
if (this.start.compareTo(this.end)>0) {
throw new IllegalArgumentException(this.start + " is later than " + this.end);
}
}
public Date start() {
return new Date(start.getTime());
}
public Date end() {
return new Date(end.getTime());
}
...(생략)
}
아래와 같이 client가 end 와 period.end()의 값을 변경해도 Period가 가진 속성은 변경되지 않습니다.
Date start = new Date();
Date end = new Date();
Period period = new Period(start, end);
end.setYear(78);
period.end().setYear(78);
Period 생성자에서 start와 end의 복사본을 먼저 생성 후 validation을 한 것도 이유가 있습니다.
아래처럼 validation이 먼저 이뤄진다면, validation이 끝난 시점에 다른 쓰레드가 start, end를 변경하고 변경된 start, end 값이 복제되어 Period의 valiation이 무효화 될 수 있습니다.
pulbic Period(Date start, Date end) {
if (start.compareTo(end)>0) {
throw new IllegalArgumentException(start + " is later than " + end);
}
// validation이 끝난 시점에 다른 쓰레드가 start, end 를 바꾸면 불변식이 깨집니다.
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
}
이러한 공격을 검사시점/사용시점(time-of-check/time-of-use) 공격이라고 하며 줄여서 TOCTOU 공격이라고 합니다.
Associated Posts
관련된 주제를 살펴볼 수 있도록 동일한 Tag를 가진 글들을 모아뒀습니다. 제목을 눌러주세요.-
유용한 표준 Java RuntimeException
( 이미지 출처 : https://openjdk.java.net )Java 프로그래밍을 하면서 예외처리를 발생시켜야하는 경우, 우리는 RuntimeException을 상속하는 예외를 사용하면 됩니다.
그리고 RuntimeException을 상속하는 예외를 새롭게 만드는 것보다는 JDK에서 제공하는 표준 RuntimeExcepton 상속 예외들을 사용하는 것이 바람직합니다.JDK 12 기준으로 RuntimeException을 직접 상속하는 예외는 총 58개입니다. (참고 - OpenJdk 12 RuntimeException)
그리고 그 58개의 예외들을 다시 상속하는 자식 예외들까지 개수를 세면 엄청나게 많습니다.그 중 자주 사용하게 되는 유용한 표준 RuntimeException 들을 기록합니다.
... 더 읽기 -
Java Thread Safe Collections - List, Queue, Set, Map
( 이미지 출처 : https://openjdk.java.net )Thread safe 한 Collection(List, Queue, Set) 그리고 Map의 구현체 사용법에 대해서 기술합니다.
... 더 읽기 -
Java Random - ThreadLocalRandom, SplittableRandom, SecureRandom
( 이미지 출처 : https://openjdk.java.net )Java에서 제공하는 Random 라이브러리에 대해서 알아봅니다.
... 더 읽기 -
Java Validation - null check, Optional
-
How to initialize Java variables - Array, List, Set, Map
-
Java 변수 선언 & 초기화 방법 - Array, List, Set, Map
-
왜 Java 8 을 공부해야 하는가?