Java 8 - LocalTime, LocalDate, LocalDateTime
업데이트:
아마 경력이 좀 있으시거나 Java 8 이전 버전을 사용하신 분들이라면 java.util.Date
, java.util.Calendar
를 주로 사용하셨을 겁니다.
Date
, Calendar
를 대신하여 Java 8에서는 LocalTime
, LocalDate
, LocalDateTime
가 있는데요.
오늘은 이 LocalTime
, LocalDate
, LocalDateTime
에 대해 알아보려 합니다.
Java 7 이전 - Date, Calendar
상당히 많은 곳에서 Date
, Calendar
를 사용하지 말 것을 권장하고 있습니다. 실제로 Date
클래스를 보시면 생성자 및 몇개의 메소드가 deprecated
가 되어있는 것을 확인할 수 있습니다.
@Deprecated
public Date(int year, int month, int date, int hrs, int min) {
this(year, month, date, hrs, min, 0);
}
// ...
@Deprecated
public int getYear() {
return normalize().getYear() - 1900;
}
// ...
그럼 사용하지 말란 이유는 뭘까요?
지금부터 하나씩 알아보도록 하겠습니다.
월 상수
Calendar
내부에 있는 상수를 보면 1월을 인덱스처럼 0부터 시작하는 것을 알 수 있습니다. (대체 왜 이렇게 했는지…)
/**
* Value of the {@link #MONTH} field indicating the
* first month of the year in the Gregorian and Julian calendars.
*/
public static final int JANUARY = 0;
요일 상수
Date
와 Calendar
사이에는 요일을 상수화한 값의 차이가 있습니다.
간단한 테스트 코드를 작성해봤습니다.
@Test
void 날짜_테스트() {
Calendar calendar = Calendar.getInstance();
calendar.set(2022, Calendar.FEBRUARY, 27);
int calendarSunday = calendar.get(Calendar.DAY_OF_WEEK);
System.out.println("calendarSunday = " + calendarSunday);
Date date = calendar.getTime();
int dateSunday = date.getDay();
System.out.println("dateSunday = " + dateSunday);
}
💡참고로 세팅한 날짜는 2022년 3월 27일, 일요일입니다.
결과를 보시면 이상한 현상을 볼 수 있습니다.
Calendar
는 1로 출력되는데, Date
는 0으로 출력됩니다.
Calendar
와 Date
를 사용해보신분들 아시겠지만 서로 변환할 일이 자주 있는데, 이렇게 사용하는데 있어 일관적이지 않습니다.
동등성
💡동등성(equality)란? 먼저 동일성(identity)부터 알아보겠습니다. 동일성이란 정말 똑같은 객체인지 주소값으로 비교하는 것을 말합니다. 이에 반해 동등성이란 객체 주소값을 다르더라도 정보가 같은 것을 말합니다.
equals
의 사용 의미를 모르시는 분들은 없으실겁니다. Effective Java에선 equals
의 규약에 대해 설명합니다.
반사성, 대칭성 등 여러가지가 있지만 결국 동등성을 지키란 얘기고, 누구나 생각하는 당연한 얘기입니다. (어려운 말을 개인적으로 안좋아합니다.😅)
그런데 equals
의 규약은 상속관계에서 자주 깨지곤 하는데 Date
와 Timestamp
가 그 대표적인 예입니다.
java.sql.Timestamp
을 보면 java.util.Date
를 상속하는 것을 알 수 있습니다.
public class Timestamp extends java.util.Date {
}
다음 테스트 코드를 보면 이 둘은 서로 다르게 equals
의 결과를 반환하는 것을 알 수 있습니다.
@Test
void Date_동등성_비교_테스트() {
long now = System.currentTimeMillis();
Date date = new Date(now);
Timestamp timestamp = new Timestamp(now);
boolean dateCompareTimestamp = date.equals(timestamp);
boolean timestampCompareDate = timestamp.equals(date);
System.out.println("dateCompareTimestamp = " + dateCompareTimestamp);
System.out.println("timestampCompareDate = " + timestampCompareDate);
}
이유는 Date
와 Timestamp
가 equals
를 서로 다르게 동작하도록 재정의했기 때문입니다.
Timestamp
의 equals
는 다음과 같습니다.
public boolean equals(Timestamp ts) {
if (super.equals(ts)) {
if (nanos == ts.nanos) {
return true;
} else {
return false;
}
} else {
return false;
}
}
보면 nano초를 비교하는 부분이 있는데, Date
에는 nano초가 없습니다. (Calendar
역시 마찬가지입니다. 밀리초까지만 설정 가능합니다.)
따라서 Date
와 Timestamp
는 서로 equals
의 규약이 깨졌다고 할 수 있습니다.
계산이 어렵다
Date
로 변수를 선언 및 사용하고자 하지만 계산 관련된 메소드가 없기 때문에 Calendar
로 변환하여 계산 후 Date
로 다시 반환해야하는 수고스러움이 있습니다. (그리고 Calendar
도 필드(년, 월, 일..)로 지정하여 계산해야하기 때문에 직관적이지 않습니다.)
Calendar calendar = Calendar.getInstance();
calendar.setTime(new Date());
calendar.add(Calendar.YEAR, 1);
calendar.add(Calendar.MONTH, 2);
calendar.add(Calendar.DATE, 3);
Date date = calendar.getTime();
불변이 아니다.
Calendar calendar = Calendar.getInstance();
calendar.set(2022, Calendar.FEBRUARY, 27);
위 코드를 보시면 set
메소드를 통해 값을 변경할 수 있음을 알 수 있습니다.
이를 통해 Calendar
객체는 불변이 아님을 알 수 있으며, 공유되는 객체라면 사용에 있어 Side-Effect가 발생할 소지가 많고, 멀티 스레드에서도 안전하지 못하다는 것 역시 알 수 있습니다.
이 밖에도 java.util.Date
, java.sql.Date
처럼 겹치는 클래스명, time-zone 설정에 대한 버그 등 다른 단점들이 존재합니다.
이를 대체하기 위해 Java 8에선 java.time
패키지에 다양한 클래스들이 등장했습니다.
Java 8 이후 - LocalTime, LocalDate, LocalDateTime
이름에서 알 수 있듯이 LocalTime
은 시간만, LocalDate
는 날짜만, LocalDateTime
은 날짜, 시간 모두 갖고 있는 클래스입니다.
장점
Date
,Calendar
처럼 혼용해서 사용할일 없이 단독으로 사용가능합니다.- 객체 생성 시
static factory
메소드(of
,from
,now
)를 사용해서 직관적이다. - 날짜 계산하는 메소드 역시 직관적이다. (
plusYears
,plusMonths
,plusDays
등) - 잘못된 time-zone이 설정될 경우
illegalArgumentException
이 발생한다. Date
,Calendar
와 달리 나노초까지 다룰 수 있다.- 불변이다.
불변
LocalDate
을 예로 들어보겠습니다.
LocalDate
에는 Calendar
와 같이 set
메소드가 없습니다.
plusDays
같이 상태 변환 메소드가 있는데?
저도 처음에 그런 생각을 했고, 그래서 메소드를 따라 들어가봤습니다.
public LocalDate plusDays(long daysToAdd) {
if (daysToAdd == 0) {
return this;
}
long dom = day + daysToAdd;
if (dom > 0) {
if (dom <= 28) {
return new LocalDate(year, month, (int) dom);
} else if (dom <= 59) { // 59th Jan is 28th Feb, 59th Feb is 31st Mar
long monthLen = lengthOfMonth();
if (dom <= monthLen) {
return new LocalDate(year, month, (int) dom);
} else if (month < 12) {
return new LocalDate(year, month + 1, (int) (dom - monthLen));
} else {
YEAR.checkValidValue(year + 1);
return new LocalDate(year + 1, 1, (int) (dom - monthLen));
}
}
}
long mjDay = Math.addExact(toEpochDay(), daysToAdd);
return LocalDate.ofEpochDay(mjDay);
}
위에서 보시는 바와 같이 모두 new
를 사용하여 새로운 객체를 생성하고 있습니다. (맨 밑에 ofEpochDay
메소드도 들어가 보시면 new
로 새로 생성하는 걸 확인할 수 있습니다.)
Local은 무슨 뜻일까?
그럼 Local이란 이름은 왜 들어가게 된 것일까요?
그 답은 클래스 상단에 설명에 기재돼있었습니다.
설명을 보면 다음과 같은 문장이 있습니다.
This class does not store or represent a time-zone.
Instead, it is a description of the date, as used for birthdays,
combined with the local time as seen on a wall clock.
It cannot represent an instant on the time-line without additional information such as an offset or time-zone.
LocalTime
, LocalDate
, LocalDateTime
모두 time-zone이나 offset 등 다른 정보가 없으면 Local 기준의 시간을 나타내기 때문에 이런 이름이 붙은 것으로 보입니다.
마무리
막연히 LocalTime
, LocalDate
, LocalDateTime
을 사용하고 있었지만, 이번 기회를 통해 안쓸 이유가 없어진 것 같습니다. 🤣
생성, 계산 메소드도 직관적이고 무엇보다 불변이기 때문에 안전하게 사용할 수 있기 때문입니다.
오늘도 제 글을 읽어주셔서 감사합니다.😄
댓글남기기