날짜와 시간 API
Java의 기본 SDK에서 날짜와 시간을 다루는 java.util.Date 클래스와 java.util.Calendar 클래스의 불편함을 해소하고자 JDK 8에서는 개선된 날짜와 시간 API가 제공됨
기존의 날짜와 시간 API의 문제점
1️⃣ 불변객체가 아님
이 때문에 Calendar 객체나 Date 객체가 여러 객체에서 공유되면 한 곳에서 바꾼 값이 다른 곳에 영향을 미치는 부작용이 생길 수 있음
2️⃣ int 상수 필드의 남용
calendar.add(Calendar.SECOND, 2);
Calendar를 사용한 날짜 연산은 int 상수 필드를 사용
첫 번째 파라미터에 Calendar.JUNE과 같이, 전혀 엉뚱한 상수가 들어가도 이를 컴파일 시점에서 확인할 방법이 없음
이 뿐만 아니라 Calendar 클래스에는 많은 int 상수가 쓰였는데, 이어서 설명할 월, 요일 지정 등에서도 많은 혼란을 유발
3️⃣ 헷갈리는 월 지정
calendar.set(1582, Calendar.OCTOBER , 4);
calendar.set(1582, 10 , 4);
1582년 10월 4일을 지정하는 코드같지만 월에 해당하는 Calendar.OCTOBER 값은 실제로는 '9'임
JDK 1.0에서 Date 클래스는 1월을 0으로 표현했고, JDK 1.1부터 포함된 Calendar 클래스도 이러한 관례를 답습함
그래서 1582년 10월 4일을 표현하는 코드를 위와 같이 쓰는 실수를 많은 개발자들이 반복하고 있음
✔️ 1582년 10월 4일을 지정하는 코드
calendar.set(1582, 10 - 1 , 4);
4️⃣ 일관성 없는 요일 상수
Calendar.get(Calendar.DAY_OF_WEEK) 함수에서 반환한 요일은 int 값으로, 일요일이 1로 표현됨
따라서 수요일은 4이고, 보통 Calendar.WEDNESDAY 상수와 비교해서 확인함
그런데 calendar.getTime() 메서드로 Date 객체를 얻어와서 Date.getDay() 메서드로 요일을 구하면 일요일은 0, 수요일은 3이 됨
두 개의 클래스 사이에 요일 지정값에 일관성이 없는 것임
5️⃣ java.util.Date 하위 클래스의 문제
java.sql.Date 클래스는 상위 클래스인 java.util.Date 클래스와 이름이 같음
이 클래스를 두고 Java 플랫폼 설계자는 클래스 이름을 지으면서 깜빡 존 듯하다는 조롱까지 나왔음
그리고 이 클래스는 Comparable 인터페이스에 대한 정의를 클래스 선언에서 하지 않았기 때문에 Comparable과 관련된 Generics 선언을 복잡하게 만들었음
java.sql.TimeStamp 클래스는 java.util.Date 클래스에 나노초(nanosecond) 필드를 더한 클래스임
이 클래스는 equals() 선언의 대칭성을 어김
Date 타입과 TimeStamp 타입을 섞어 쓰면 a.equals(b)가 true라도 b.equals(a)는 false인 경우가 생길 수 있음
Java의 개선된 날짜, 시간 API
이런 문제점 때문에 JDK의 날짜, 시간 API를 대체하는 라이브러리가 많이 나옴
대표적으로 다음과 같은 것들이 있음
✔️ Joda-Time: http://www.joda.org/joda-time
✔️ Time and Money Code Library: http://timeandmoney.sourceforge.ne(DDD의 저자 Eric Evans가 참여한 것으로 유명)
✔️ CalendarDate: http://calendardate.sourceforge.net
✔️ date4j: http://www.date4j.net
Joda-Time
Joda-Time은 기본 JDK를 대체하는 날짜와 시간 API 중 가장 널리 쓰임
public class JodaTimeTest {
@Test // 예제1: 1일 후 구하기
public void shouldGetAfterOneDay() {
Chronology chrono = GregorianChronology.getInstance();
LocalDate theDay = new LocalDate(1582, 10, 4, chrono);
String pattern = "yyyy.MM.dd";
assertThat(theDay.toString(pattern)).isEqualTo("1582.10.04");
LocalDate nextDay = theDay.plusDays(1);
assertThat(nextDay.toString(pattern)).isEqualTo("1582.10.05");
}
@Test // 예제1: 1일 후 구하기
public void shouldGetAfterOneDayWithGJChronology() {
Chronology chrono = GJChronology.getInstance();
LocalDate theDay = new LocalDate(1582, 10, 4, chrono);
String pattern = "yyyy.MM.dd";
assertThat(theDay.toString(pattern)).isEqualTo("1582.10.04");
LocalDate nextDay = theDay.plusDays(1);
assertThat(nextDay.toString(pattern)).isEqualTo("1582.10.15");
}
@Test // 예제2: 1시간 후 구하기
public void shouldGetAfterOneHour() {
DateTimeZone seoul = DateTimeZone.forID("Asia/Seoul");
DateTime theTime = new DateTime(1988,5,7,23,0, seoul);
String pattern = "yyyy.MM.dd HH:mm";
assertThat(theTime.toString(pattern)).isEqualTo("1988.05.07 23:00");
assertThat(seoul.isStandardOffset(theTime.getMillis())).isTrue();
DateTime after1Hour = theTime.plusHours(1);
assertThat(after1Hour.toString(pattern)).isEqualTo("1988.05.08 01:00");
assertThat(seoul.isStandardOffset(after1Hour.getMillis())).isFalse();
}
@Test // 예제 3: 1분 후 구하기
public void shouldGetAfterOneMinute() {
DateTimeZone seoul = DateTimeZone.forID("Asia/Seoul");
DateTime theTime = new DateTime(1961, 8, 9, 23, 59, seoul);
String pattern = "yyyy.MM.dd HH:mm";
assertThat(theTime.toString(pattern)).isEqualTo("1961.08.09 23:59");
DateTime after1Minute = theTime.plusMinutes(1);
assertThat(after1Minute.toString(pattern)).isEqualTo("1961.08.10 00:30");
}
@Test // 예제 4: 2초 후 구하기
public void shouldGetAfterTwoSecond() {
DateTimeZone utc = DateTimeZone.forID("UTC");
DateTime theTime = new DateTime(2012, 6, 30, 23, 59, 59, utc);
String pattern = "yyyy.MM.dd HH:mm:ss";
assertThat(theTime.toString(pattern)).isEqualTo("2012.06.30 23:59:59");
DateTime after2Seconds = theTime.plusSeconds(2);
assertThat(after2Seconds.toString(pattern)).isEqualTo("2012.07.01 00:00:01");
}
@Test // 예제 5: 1999년 12월 31일을 지정하는 코드
public void shouldGetDate() {
LocalDate theDay = new LocalDate(1999, 12, 31);
assertThat(theDay.getYear()).isEqualTo(1999);
assertThat(theDay.getMonthOfYear()).isEqualTo(12);
assertThat(theDay.getDayOfMonth()).isEqualTo(31);
}
@Test (expected=IllegalFieldValueException.class) // 예제 5: 1999년 12월 31일을 지정하는 코드의 실수
public void shouldNotAcceptWrongMonth() {
new LocalDate(1999, 13, 31);
}
@Test // 예제 6: 요일 확인하기
public void shouldGetDayOfWeek() {
LocalDate theDay = new LocalDate(2014, 1, 1);
int dayOfWeek = theDay.getDayOfWeek();
assertThat(dayOfWeek).isEqualTo(DateTimeConstants.WEDNESDAY);
assertThat(dayOfWeek).isEqualTo(3);
}
@Test(expected=IllegalArgumentException.class) // 예제 7: 잘못 지정한 시간대 ID
public void shouldThrowExceptionWhenWrongTimeZoneId(){
DateTimeZone.forID("Seoul/Asia");
}
}
참고자료 👇
'프로그래밍 > JAVA' 카테고리의 다른 글
[Java] 시간 지연 방법(Thread, TimeUnit) (0) | 2022.04.04 |
---|---|
[Java] Java API Reference (0) | 2022.03.25 |
[Java] Stream 예제 (0) | 2022.03.23 |
[Java] Stream 생성 방법 (0) | 2022.03.22 |
[Java] Collection과 Stream 비교 (0) | 2022.03.21 |