본문 바로가기
Language/Java

[Java 8] 날짜 API - 1

by ocwokocw 2021. 2. 11.

- 출처: https://www.oracle.com/java/technologies/jf14-date-time.html

 

- 왜 새로운 날짜 API가 필요한가?

자바8 이전에는 날짜와 시간에 대한 기능 지원이 부족했다. 예를 들어 java.util.Date 와 SimplteDateFormatter 와 같은 클래스들은 스레드-세이프 하지 않아서 사용자에게 잠재적인 병렬성 이슈를 불러일으킬 가능성이 있었다.

 

또한 날짜와 시간 관련 클래스중 일부는 API 디자인 측면에서 부족한 부분이 있었다. 예를 들면 java.util.Date는 1900년 부터 시작하고, 달의 인덱스는 0부터 시작하며, 일의 인덱스는 1부터 시작하여 직관적이지 않은 부분이 있다.

 

이런 이슈들과 다른 문제점들은 Joda-Time 같은 제 3의 라이브러리를 사용하게 만들었다. 자바8에서는 이런 문제를 해결하고 더 나은 지원을 위해 새로운 날짜와 시간 API를 소개한다.


- 핵심 사상

- 불변 객체 클래스

기존에 있던 자바 formatter 의 문제점 중 하나는 스레드-세이프 하지 않다는것이었다. 이런 문제점은 개발자들에게 해당 API를 사용하기 위해서 스레드-세이프한 방법으로 사용하게 강제하는 짐을 주었다. 새로운 API는 코어 클래스가 불변이며, 잘 정의된 값을 표현할 수 있음을 보장하여 이런 문제를 회피하였다.


- DDD(도메인 드리븐 디자인)

새로운 API는 각 Date와 Time에 사용에 대해 엄격하게 나누어 표현하는 클래스로 정확하게 도메인을 모델링하였다. 새로운 API는 앞서 세심함이 부족한 자바 라이브러리들과는 다르다. 예를 들어 java.util.Date는 UNIX 시대에 밀리 세컨드를 감싼 타임라인의 순간을 표현한다. 하지만 toString() 을 했을 때, 그 결과는 타임존을 가지고 있기 때문에 혼란스러움이 존재한다.

 

이런점은 도메인 기반 디자인이 명료성과 이해력 관점에서 장기적인 이익을 제공하지만, 이전 API를 자바 8로 포팅할 때, 날짜의 어플리케이션 도메인 모델을 생각할 필요가 있다.


- 연대의 분리

새로운 API는 전세계의 특정 지역, 예를 들어 일본이나 태국같이 ISO-8601을 따를 필요가 없는 유저들이 필요한 지원을 받을 수 있도록 다른 캘린더 시스템을 제공한다. 


- LocalDate와 LocalTime

날짜 API를 처음사용할 때 LocalDate와 LocalTime을 처음으로 접하게 될 것이다. 이 클래스들은 책상위의 달력이나 벽의 시계같이 관찰자의 관점에서 날짜와 시간을 표현한다는 점에서 지역적인 특성을 가지고있다. 또한 LocalDate와 LocalTime이 결합된 LocalDateTime 이라 불리는 복합 클래스가 있다.

 

새로운 API에서 핵심 클래스들은 팩토리 메소드로 구성되어있다. 구성하는 필드에 의해 값을 생성할 때 팩토리 메소드 of 를 호출하고, 다른 타입에서 변환할 때는 from 을 호출한다. 또 문자열을 인자로 받아 파싱할때는 parse 메소드를 이용한다.

 

LocalDateTime timePoint = LocalDateTime.now();
	
LocalDate localDate1 = LocalDate.of(2012, Month.DECEMBER, 12);
LocalDate localDate2 = LocalDate.ofEpochDay(150);
LocalTime localTime1 = LocalTime.of(17, 18);
LocalTime localTime2 = LocalTime.parse("22:40:30");

System.out.println("localDate1 : " + localDate1);
System.out.println("localDate2 : " + localDate2);
System.out.println("localTime1 : " + localTime1);
System.out.println("localTime2 : " + localTime2);

 

실행결과는 아래와 같다.

 

localDate1 : 2012-12-12
localDate2 : 1970-05-31
localTime1 : 17:18
localTime2 : 22:40:30

 

위와 같이 시간을 정의한 후, 표준적인 getter 메소드를 이용하여 값을 얻을 수도 있다.

 

LocalDateTime timePoint = LocalDateTime.now();
	
LocalDate theDate = timePoint.toLocalDate();
Month month = timePoint.getMonth();
int day = timePoint.getDayOfMonth();
int second = timePoint.getSecond();
	
System.out.println("timePoint: " + timePoint);
System.out.println("theDate: " + theDate);
System.out.println("month: " + month);
System.out.println("day: " + day);
System.out.println("second: " + second);
timePoint: 2020-09-23T22:46:16.238
theDate: 2020-09-23
month: SEPTEMBER
day: 23
second: 16

 

또 계산을 수행하기 위해 객체값을 변경할 수 있다. 새로운 API에서 모든 코어 클래스는 변경 불가능(immutable)이기 때문에, setter 를 사용하기 보다는 with 메소드를 호출하여 새로운 객체를 반환한다.

 

LocalDateTime timePoint = LocalDateTime.now();
	
LocalDateTime thePast = timePoint.withDayOfMonth(1)
		.withMonth(Month.AUGUST.getValue()).withYear(2019);
	
System.out.println("thePast: " + thePast);

 

현재 시간에서 2019년, 8월, 1일로 시간을 돌렸다. 출력하면 아래와 같이 나타난다.

 

thePast: 2019-08-01T22:54:59.334

 

위의 with 메소드들을 계속 사용하였는데, 이렇게 사용할 수 있는 이유는 with 메소드에서 빌더 패턴처럼 자기자신을 반환하기 때문이다. 어떻게 정의되어있는지 내부 메소드를 살펴보자.

 

public LocalDateTime withDayOfMonth(int dayOfMonth) {
    return with(date.withDayOfMonth(dayOfMonth), time);
}

private LocalDateTime with(LocalDate newDate, LocalTime newTime) {
    if (date == newDate && time == newTime) {
        return this;
    }
    return new LocalDateTime(newDate, newTime);
}

 

withDayOfMonth 에서 with 메소드를 호출하고, with 메소드는 LocalDate 와 LocalTime을 인자로 받아 새로운 LocalDateTime 객체를 반환한다.


- TemporalAdjusters

새로운 API는 adjuster(조절자, 조절장치) 개념을 가지고 있다. WithAdjuster는 1개 이상의 필드들을 설정하는데 사용되며, PlusAdjuster 는 몇개의 필드에 더하고 차감하는 연산에 사용된다. Value 클래스들은 adjuster 처럼 행동하는데, 이런 경우에 그들은 표현하는 필드의 값을 갱신한다. 내장된 adjuster들은 새로운 API에 의해 정의되어있지만, 만약 재사용해야하는 특별한 비즈니스 로직이 있다면 자신만의 커스텀 adjuster를 만들수도 있다.

 

LocalDateTime timePoint = LocalDateTime.now();
	
LocalDateTime firstDayOfMonth = timePoint.with(TemporalAdjusters.firstDayOfMonth());
LocalDateTime nextMonth = timePoint.with(TemporalAdjusters.firstDayOfNextMonth());
LocalDateTime prevFriday = timePoint.with(TemporalAdjusters.previous(DayOfWeek.FRIDAY));
LocalDateTime nextFriday = timePoint.with(TemporalAdjusters.next(DayOfWeek.FRIDAY));
	
System.out.println("timePoint: " + timePoint);
System.out.println("firstDayOfMonth: " + firstDayOfMonth);
System.out.println("nextMonth: " + nextMonth);
System.out.println("prevFriday: " + prevFriday);
System.out.println("nextFriday: " + nextFriday);
timePoint: 2020-09-23T23:09:33.512
firstDayOfMonth: 2020-09-01T23:09:33.512
nextMonth: 2020-10-01T23:09:33.512
prevFriday: 2020-09-18T23:09:33.512
nextFriday: 2020-09-25T23:09:33.512

 

메소드 이름과 실행결과를 보면 어떤 기능이나 동작을 하는지 추측하는것은 어렵지 않다는것을 알 수 있다. TemporalAdjusters 클래스에는 이런 편의기능을 제공하는 메소드들이 어느정도 구현되어있어서 복잡한 날짜 조정기능을 직관적으로 사용할 수 있다.

댓글