mj@home:~$

Java 8 날짜 관련 Class

Java SE 8 날짜와 시간

Java는 8버전으로 업그레이드하면서 람다, 스트림 등 다양한 기능들이 추가되고 개선되었는데 새로운 날짜관련 API도 대폭 추가되었습니다. 이는 기존에 사용하던 Java가 내장한 날짜관련 API에 많은 문제가 있었기 때문입니다. 이 글에서는 이전까지 사용하던 날짜관련 API에 어떤 문제가 있었는지 간단히 살펴보고 Java8에서 새롭게 추가된 날짜관련 API를 살펴보고자 합니다.

기존 날짜관련 API의 문제점 - Date, Calendar

불변객체가 아니다

Java의 가장 큰 장점 중 하나는 멀티스레딩 프로그래밍을 손쉽게 구현할 수 있다는 것입니다. 그런데 Calendar와 같은 기존의 날짜 API는 이러한 장점을 갉아먹는데 일조하고 있었습니다. 멀티스레딩 환경에서는 경쟁조건과 같은 이슈 때문에 객체의 변경가능성이 매우 중요한 요소가 됩니다. 객체의 정보를 변경하는 것이 불가능하다면 그 객체를 불변객체(immutable object)라고 합니다(반대의 경우는 가변객체). Java에서 대표적인 불편 객체로는 String이 있습니다. 문자열을 다루는 String 객체는 값이 한번 정해지면 그 문자열 객체의 값을 변경할 수 없습니다. 값을 변경할 수 없는 불변객체는 멀티스레딩 환경에서 정보가 변경될 걱정이 없기 때문에 스레드세이프하다고 합니다. 반면, Calendar 클래스는 가변객체이기 때문에 스레드세이프하지 못합니다.

public class JavaDateTest {
    private SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");

    @Test
    public void testMutableDate(){
        Calendar cal = Calendar.getInstance();
        assertEquals("2016-02-10", toString(cal));

        cal.set(2016, Calendar.OCTOBER, 30);
        assertEquals("2016-10-30", toString(cal));
    }

    String toString(Calendar cal){
        return df.format(cal.getTime());

    }
}

위 코드처럼 Calendar 클래스는 언제 어디서든 그 상태가 변할 수 있습니다. 간단한 Calendar.getInstance() 메소드로 현재 시간를 가지고 있는 객체를 얻더라도 이 객체는 set()이라는 간단한 메소드에 의해 그 상태가 변할 수 있습니다. 이 객체가 여러 스레드에서 공유된다면 현 스레드가 아닌 다른 스레드에도 잘못된 영향을 줄 수 있을 것입니다.

지나친 상수 사용

Calendar 클래스는 날짜에 대한 정보를 표현하기 위해서 수 많은 int형 상수를 사용하고 있습니다.

cal.get(Calendar.MONTH);
cal.add(Calendar.SECOND, 10);

위 코드처럼 Calendar 클래스는 특정 날짜에 대한 정보를 얻거나 조작할때 int형 상수를 사용합니다. 각 요일과 월에 대한 것들을 포함한 모든 것들이 int형 상수로 되어 있다보니 종종 혼란을 초래하는 경우가 생깁니다.

cal.add(Calendar.OCTOBER, 2);       // OCTOBER는 날짜 단위가 아니다.
cal.set(Calendar.JUNE, 2014, 1);      // 인자 순서가 바뀌었다.
                                              // 그럼에도 문제없이 실행된다.

add()메소드는 첫번째 인자로 증가하거나 감소할 대상이 되는 년, 월, 일, 초 등 단위를 나타내는 상수가 들어가야 하지만, 위 코드와 같이 엉뚱한 상수필드가 들어가더라도 Java는 이를 아무 문제 없는 코드로 인식합니다. 모든 인자가 int형 상수이니 개발자의 실수로 인자의 순서가 바뀌더라도 아무 문제없이 동작할 것입니다. 단지 버그가 발생할 뿐. 컴파일 단계에서 이러한 코드를 미리 잡아낼 수 없으며, 개발자는 버그의 원인을 찾기 위해 고생을 해야겠지요.

0부터 시작하는 월

년과 일은 모두 우리가 일반적으로 생각하는 날짜와 동일하게 1부터 시작을 합니다. 0년 0일은 없습니다. 그런데 Calendar에서는 0월이 있습니다. 정확히는 0이 1월을 나타내며, 0부터 11까지를 월을 나나타내는데 사용하고 있다. Java에서 날짜를 담당하셨던 분도 이는 좋지 않게 생각하셨는지 Calendar에서 각 요일을 나타내는 상수를 제공하지만 위 ‘지나친 상수 사용’에서의 경우처럼 별 도움은 안되고 있습니다.

일관성 없는 요일

날짜관련 기능이 Date와 Calendar로 나뉘어 있다보니 중복되는 기능도 있다. 그런데 기능과 역할이 동일한데도 불구하고 결과가 다른 경우가 있습니다.

cal.get(Calendar.DAY_OF_WEEK);  // Calendar 객체로 요일 얻기
cal.getTime().getDay();               // Date 객체로 요일 얻기

Calendar 클래스의 getTime()메서드로 Date 객체를 얻을 수 있습니다. Calendar와 Date 두 클래스 모두 요일을 얻을 수 있는 메소드를 제공하는데, Calendar는 일요일이 1이고 Date는 일요일이 0입니다. 서로 동일한 날짜를 나타내며 같은 요일을 표현하는데 다른 상수를 쓰고 있는 것입니다.


JSR-310

Java8에는 JSR-310이라는 표존명세로 새로운 날짜와 시간관련 API가 존재합니다. 기존의 Date, Calendar 등을 대체하기 위해서 지금까지 많이 사용되던 Java 날짜 관련 라이브러리를 참고하여 보다 효율적이고 사용하기 편한 API를 내놓았습니다.

java.time 패키지

java.time 패키지는 이번 Java8과 함께 등장한 패키지로 새로운 날짜 및 시간관련 API를 제공합니다. 기존의 Date, Calendar가 가지고 있던 문제점을 해결하여 보다 효율적이게 날짜와 시간을 다룰 수 있게 돕습니다. 즉, java.time이 제공하는 클래스 객체는 불변객체이며, 이전처럼 int형 상수를 통해 요일 및 월을 구분하지 않습니다. 월, 요일 등은 Enum 객체로서 다루어지기 때문에 보다 명확하게 알 수 있으며 사용자의 실수를 예방할 수 있습니다. 또한, new 같은 키워드를 통해 생성자를 사용하는 것이 아닌 static factory메소드를 통해 객체를 생성합니다. 이펙티브 자바라는 책에서 권장하듯 new 키워드보다는 팩토리메서드를 지향하고 있습니다. 그리고 잘못된 필드, 월, 요일이 지정되는 경우도 검증을 하고 있어 만일 잘못된 정보를 입력하는 경우에는 예외처리를 해줍니다.

Core Ideas

Java8의 날짜 및 시간 API의 핵심 아이디어는 아래의 3가지라고 합니다.

Immutable-value classes

기존 API의 가장 큰 문제점은 제공되는 객체가 가변객체라 스레드로부터 안전하지 않다는 것이었습니다. 새로운 API는 모든 핵심 클래스를 불변객체로 제공합니다.

Domain-driven design

도메인 중심의 모델은 개발자에게 보다 명확하고 이해하기 쉬운 API를 제공합니다.

Separation of chronologies

새로운 API는 ISO-8601가 필수가 아닌 일본이나 태국같이 서로다른 날짜 시스템을 사용하는 나라에서도 문제없이 동작할 수 있도록 해줍니다. 이러한 점은 개발자의 부담을 덜어줄 것입니다.

대표적인 클래스

이름 설명
LocalDate 날짜만을 표현하는 클래스(년, 월, 일 등) 시간 제외.
LocalTime 시간만을 표현하는 클래스(시, 분, 초 등) 날짜 제외.
LocalDateTime 날짜와 시간을 모두 표현하는 클래스
ZonedDateTime 타임존이 지정된 날짜와 시간을 표현하는 클래스.

날짜 및 시간 객체 생성

java.time에서 제공하는 클래스는 모두 팩토리메서드 방식으로 객체를 생성하며, 그 방법이 동일합니다.

// 현재 날짜 및 시간 객체 생성
LocalDate today = LocalDate.now();
LocalDateTime now = LocalDateTime.now();
LocalTime currentTime = LocalTime.now();

// 특정 날짜 및 시간 객체 생성
LocalDate yesterday = LocalDate.of(2016, 2, 9);	// 2016/02/09
LocalTime thatTime = LocalTime.of(18, 30, 34);		// 18:30:34
LocalDateTime theDay = LocalDateTime.of(yesterday, thatTime);		// 날짜와 시간으로 생성
LocalDateTime superDay = LocalDateTime.of(2016, 2, 5, 15, 0, 0);	// 2016/02/05 15:00:00

// 날짜 및 시간을 파싱해서 생성
LocalDate thatDay = LocalDate.parse("2016-01-04");
LocalTime goHomeTime = LocalTime.parse("18:30:00");
LocalDateTime dayAndTime = LocalDateTime.parse("2016-01-04T18:30:00");	// 날짜와 시간을 T로 구분
// 기존 객체를 재활용하여 새로운 객체 생성
LocalDate localDate = today.withDayOfMonth(5);

데이터 얻기

LocalDateTime now = LocalDateTime.parse("2016-01-31T12:30:58");

assertEquals(2016, now.getYear());
assertEquals(1, now.getMonthValue());
assertEquals(31, now.getDayOfMonth());
assertEquals(12, now.getHour());
assertEquals(30, now.getMinute());
assertEquals(58, now.getSecond());

포맷

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd hh:mm:ss");
LocalDateTime localDateTime = LocalDateTime.of(2016, 1, 1, 7, 30, 24);
assertEquals("2016/01/01 07:30:24", formatter.format(localDateTime));

연산

LocalDateTime theDay = LocalDateTime.parse("2016-01-31T12:30:58");
assertEquals(58, theDay.getSecond());

// 10초 후 얻기
LocalDateTime after10Seconds = theDay.plusSeconds(10);
assertEquals(8, after10Seconds.getSecond());

// 10일 후 얻기
LocalDateTime after10Days = theDay.plusDays(10);
assertEquals(2, after10Days.getMonthValue());
assertEquals(10, after10Days.getDayOfMonth());

// 10년 전 얻기
LocalDateTime before10Years = theDay.minusYears(10);
assertEquals(2006, before10Years.getYear());

특정 주기 지정

// 5년, 2달, 10일
Period period = Period.of(5, 2, 10);
LocalDate theDate = LocalDate.of(2016, 10, 20);
LocalDate afterDate = theDate.plus(period);
assertEquals(2021, afterDate.getYear());
assertEquals(12, afterDate.getMonthValue());
assertEquals(30, afterDate.getDayOfMonth());

날짜 간격, 기간

LocalDateTime today = LocalDateTime.now();
LocalDateTime yesterday = today.minusDays(1);
Duration durationOfDays = Duration.between(yesterday, today);
assertEquals(24, durationOfDays.toHours());

JDBC를 위한 SQL 관계

클래스 SQL 자료형
LocalDate DATE
LocalTime TIME
LocalDateTime TIMESTAMP
OffsetTime TIME WITH TIMEZONE
OffsetDateTime TIMESTAMP WITH TIMEZONE

안타깝게도 아직 Java8에서는 새로운 날짜 및 시간 관련 API를 위해 JDBC의 날짜관련 클래스들을 직접 사용할 순 없습니다. 대신 java.sql 패키지의 날짜 및 시간 관련 클래스들에 java.time패키지에 대응하는 메소드가 추가되었습니다.

LocalDate today = LocalDate.now();
Date date = Date.valueOf(today);
LocalDate localDate = date.toLocalDate();

LocalTime currentTime = LocalTime.now();
Time time = Time.valueOf(currentTime);
LocalTime localTime = time.toLocalTime();

LocalDateTime now = LocalDateTime.now();
Timestamp timestamp = Timestamp.valueOf(now);
LocalDateTime localDateTime = timestamp.toLocalDateTime();

Instant instant = Instant.now();
Timestamp timestampFromInstant = Timestamp.from(instant);
Instant intantFromTimestamp = timestampFromInstant.toInstant();

Java7을 위한 백포트

Java8에서 추가된 날짜 및 시간 API를 이전 버전에서도 사용할 수 있도록 백포트를 제공합니다. 백포트는 상위버전의 기능을 하위버전에 반영해 주는 것을 의미합니다.

<dependency>
    <groupId>org.threeten</groupId>
    <artifactId>threetenbp</artifactId>
    <version>1.3.1</version>
</dependency>

References