Java의 LocalDate, LocalDateTime

Java7까지는 날짜를 처리하기 위한 기본 API로 Date와 Calendar를 제공하였습니다. 하지만 이 클래스들은 기능 부족과 엉성한 설계로 사용하기 어려운 문제점들이 많이 존재하였습니다. 특히 날짜 포맷팅이나 파싱을 위해 함께 사용되는 SimpleDateFormat의 기능부족과 상수값 문제등으로 개발자들은 별도의 유틸리티를 따로 개발해서 사용하거나 아파치에서 제공하는 날짜처리라이브러리를 사용하였습니다. 이를 개선하기 위해 Java8부터는 날짜 처리를 위한 기본 API로 LocalDate, LocalDateTime을 제공합니다. 본 포스트에서는 기존 API의 문제점과 새로운 API의 사용법을 알아보겠습니다.

기존 API의 문제점

Java7과 이전버전들은 날짜 API는 다음과 같은 문제점을 가지고 있었습니다.

  • 멀티 쓰레드에 안전하지 못함
  • 날짜를 구성하는 상수값(월, 요일 등)의 모호성
  • 외부에서 날짜가 변경될 수 있는 위험에 노출

멀티 쓰레드에 취약

문자열을 날짜객체로 파싱하는 코드를 멀티쓰레딩환경에서 실행할 경우 SimpleDateFormat을 공용으로 사용하면 NumberFormatException이 발생합니다. 이는 SimpleDateFormat가 내부적으로 Calandar나 Date 객체를 여러 쓰레드에서 공유해서 사용하기 때문에 값이 오염될 수 있어서 발생시키는 예외입니다.

public static void main(String[] args) throws Exception {

    final DateFormat format = new SimpleDateFormat("yyyyMMdd");
    Callable<Date> task = () -> format.parse("20210301");

    ExecutorService exec = Executors.newFixedThreadPool(5);
    List<Future<Date>> results = new ArrayList<Future<Date>>();

    for(int i=0; i<100; i++){
    	results.add(exec.submit(task));
    }
    exec.shutdown();

    for(Future<Date> result: results) {
    	System.out.println(result.get());
    }
}

위 코드를 살펴보면 SimpleDateFormat를 처음 생성하고 여러쓰레드에서 동시에 접근하여 사용합니다. 이런 경우가 NumberFormatException을 발생시키는 상황입니다. 이경우 다음과 같이 각각의 쓰레드에 다른 SimpleDateFormat을 사용하도록 변경해야 합니다.

public static void main(String[] args) throws Exception {
        
	Callable<Date> task = () -> new SimpleDateFormat("yyyyMMdd").parse("20210301");

    ExecutorService exec = Executors.newFixedThreadPool(5);
    List<Future<Date>> results = new ArrayList<Future<Date>>();

    for(int i=0; i<100; i++){
    	results.add(exec.submit(task));
    }
    exec.shutdown();

    for(Future<Date> result: results) {
    	System.out.println(result.get());
    }
}

명명규칙

유틸리티를 사용하지 않고 Calendar 클래스를 사용할 때 가장 큰 문제점은 내부적으로 사용하고 있는 상수값의 의미를 파악하기 어렵다는 것입니다. 예로들면 Calendar.MONDAY와 Calendar.MARCH의 상수값이 2라는 같은 값을 가져서 컴파일러가 같은 값으로 인식한다는 것입니다. 또한 요일같은 경우 SUNDAY를 시작으로 1부터 SATURDAY가 7을 가지게 되지만 월같은 경우 JANUARY가 0부터 시작하고 DECEMBER가 11의 값을 가집니다(한국의 경우 월을 숫자로 표현하기 때문에 이러한 모호성으로 인해 원치않는 오류가 발생할 수 있습니다). 이는 상수값 부여에 대해 일관성이 없다는 것을 나타냅니다.

Date객체는 더 혼란스러운 설계를 가지고 있습니다. 예를 들면 year는 연도-1900의 값을 가지도록 지정하고 있고 month는 0~11, day는 1~31의 값을 가지는 모든 값이 따로 노는 설계를 가지고 있습니다(연,월,일로 Date를 생성하는것은 현재 deprecatd되어있습니다).

데이터 불변성

변경되지 않아야할 값을 불변객체로 생성해 사용합니다. 날짜도 불변으로 사용되는 주 항목인데 Date나 Calendar는 언제든지 변경될 수 있는 위험성이 존재합니다. 즉 생성된 Date객체가 다른 메소드의 매개변수로 사용되어 외부에서 변경이 될 수 있습니다. 이를 방지하기 위해서는 새로운 객체를 생성하는 방법등이 존재하지만 완전히 안전한 방법이라고는 할 수 없기에 자바에서는 새로운 날짜 API를 제공합니다.

새로운 날짜와 시간

이전 날짜 API의 문제를 개선하기 위해 새로운 API는 다음과 같은 4가지 설계원칙을 가지고 있습니다.

  • 명확성: API 명명 규칙과 사용에 사용일관성을 제공하여 이름만으로도 이해할 수 있는 코드작성을 제공
  • 풍부함: 부족한 기능을 보완하고 기본 API만으로도 원하는 기능을 개발할 수 있도록 풍부한 기능을 제공
  • 불변성: 값이 외부에서 원치않는 변경이 발생하지 않도록 불변성을 제공
  • 확장성: 개발자가 날짜 처리 API를 확장할 수 있는 구조를 제공

이를 위해 기존의 java.util패키지가 아닌 java.time패키지로 분리된 날짜관련 API를 제공하고 있습니다.

날짜

날짜의 기본 구성인 연, 월, 일, 요일을 처리하는 4개의 클래스를 제공합니다.

  • LocalDate: 현재 로컬 기반으로 날짜 정보 표현
  • YearMonth: 특정 연도와 월 정보를 표현
  • MonthDay: 특정 월과 일정보를 표현(연도와 무관된 날짜 정보를 표현할때 사용)
  • Year: (연도를 표현)

날짜와 시간

날짜와 시간을 함께 처리하는 2개의 클래스를 제공합니다.

  • LocalTime: 설정된 타임존과 로컬 기반의 시간정보를 표현, 날짜와 타임존 정보는 포함하지 않음
  • LocalDateTime: 설정된 타임존과 로컬 기반의 날짜와 시간정보 제공, 타임존 정보는 포함하지 않음

파싱과 포맷팅

  • DateTimeFormatter클래스를 이용해서 포맷정보를 설정, 변환 작업을 수행하지는 않음
  • LocalDate, LocalDateTime등 날짜와 시간 객체에서 선언한 포맷정보를 사용해 파싱 또는 포맷팅 수행

타임존과 오프셋

다국어 서비스를 제공하거나 글로벌하게 배포되는 소프트웨어를 위한 타임존과 오프셋 기능을 제공합니다.

두 날짜와 시간의 차이

기간을 처리하기 위한 두 날짜의 연산을 위해 2개의 클래스를 제공합니다.

  • Period: 두 날짜와 시간차이를 1년 2개월 3일과 같은 형태로 계산할 때 유용
  • Duration: 두 날짜의 시간차이를 계산할 때 사용

Temporal 패키지

java.time.temporal 패키지에는 날짜와 시간을 계산하고 활용하는데 도움을 주는 인터페이스, 클래스, 열거형을 제공합니다. Temporal는 저수준 패키지로 설계되어 있어서 이를 직적활용해도 되고, 상속 또는 구현해서 사용할 수 있습니다.

  • 연, 월, 일, 시간과 같은 날짜와 시간을 구성하고 있는 항목관리
  • 월 표현, 요일 오전/오후 등의 날짜와 시간을 구성하고 있는 필드를 관리
  • 날짜와 시간을 구성하는 항목과 필드 값을 이용해서 날짜 시간을 조정하는 기능을 제공

과거 버전과의 호환성

기존의 Date, Calendar, GregorianCalendar, TimeZone 클래스와 호환을 위한 메서드를 제공

  • Calendar.toInstant(): Calendar객체를 Instant객체로 변환
  • GregorianCalendar.toZonedDateTime(): GregorianCalendar 객체를 ZonedDateTime 객체로 변환
  • GregorianCalendar.format(ZonedDateTime): 기본 로케일을 사용하는 ZonedDateTime 인스턴스값을 기반으로 GregorianCalendar객체를 생성
  • Date.time(Instant): Instant객체로부터 Date객체를 생성
  • Date.toInstant(): Date 객체를 Instant 객체로 변환
  • TimeZone.toZoneId(): TimeZone객체를 ZoneId객체로 변환

References