업데이트:

아마 경력이 좀 있으시거나 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;

요일 상수

DateCalendar 사이에는 요일을 상수화한 값의 차이가 있습니다.

간단한 테스트 코드를 작성해봤습니다.

@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일, 일요일입니다.

결과를 보시면 이상한 현상을 볼 수 있습니다.

different_day_value

Calendar는 1로 출력되는데, Date는 0으로 출력됩니다.

CalendarDate를 사용해보신분들 아시겠지만 서로 변환할 일이 자주 있는데, 이렇게 사용하는데 있어 일관적이지 않습니다.

동등성

💡동등성(equality)란? 먼저 동일성(identity)부터 알아보겠습니다. 동일성이란 정말 똑같은 객체인지 주소값으로 비교하는 것을 말합니다. 이에 반해 동등성이란 객체 주소값을 다르더라도 정보가 같은 것을 말합니다.

equals의 사용 의미를 모르시는 분들은 없으실겁니다. Effective Java에선 equals의 규약에 대해 설명합니다.

반사성, 대칭성 등 여러가지가 있지만 결국 동등성을 지키란 얘기고, 누구나 생각하는 당연한 얘기입니다. (어려운 말을 개인적으로 안좋아합니다.😅)

그런데 equals의 규약은 상속관계에서 자주 깨지곤 하는데 DateTimestamp가 그 대표적인 예입니다.

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

different_equals_value

이유는 DateTimestampequals를 서로 다르게 동작하도록 재정의했기 때문입니다.

Timestampequals는 다음과 같습니다.

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 역시 마찬가지입니다. 밀리초까지만 설정 가능합니다.)

따라서 DateTimestamp는 서로 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은 날짜, 시간 모두 갖고 있는 클래스입니다.

장점

  1. Date, Calendar 처럼 혼용해서 사용할일 없이 단독으로 사용가능합니다.
  2. 객체 생성 시 static factory 메소드(of, from, now)를 사용해서 직관적이다.
  3. 날짜 계산하는 메소드 역시 직관적이다. (plusYears, plusMonths, plusDays 등)
  4. 잘못된 time-zone이 설정될 경우 illegalArgumentException이 발생한다.
  5. Date, Calendar와 달리 나노초까지 다룰 수 있다.
  6. 불변이다.

불변

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을 사용하고 있었지만, 이번 기회를 통해 안쓸 이유가 없어진 것 같습니다. 🤣

생성, 계산 메소드도 직관적이고 무엇보다 불변이기 때문에 안전하게 사용할 수 있기 때문입니다.

오늘도 제 글을 읽어주셔서 감사합니다.😄

📌참고


java 날짜 관련 Date,Calendar,SimpleDateFormat 문제점

Java에서 날짜, 시간 제대로 사용하기(LocalDate, LocalTime, LocalDateTime)

댓글남기기