티스토리 뷰

Java

Java8#06. time package

LichKing 2017. 3. 4. 21:29

보통 JDK에 기본적으로 내장된 클래스는 완전 무결할 것이라고 믿고 쓰는 경우가 많다. 설령 그 라이브러리에서 문제가 발생하더라도 그건 그걸 잘못활용한 내 잘못이지 JDK내부의 라이브러리가 잘못됐다고 생각하는 경우는 거의 없다. 물론 JDK내의 클래스들은 뛰어난 개발자들이 만든것이니 그럴(완벽할) 확률이 매우 높지만 어쨋든 그 클래스들도 개발자들이 만든지라 문제가 있는 경우도 존재한다.


자바의 날짜 관련 클래스는 그 역사가 참 깊은데 그것들을 모두 묻고 자바8에 다시 새로운 날짜클래스가 추가되었다. 기존에 잘 쓰고있는 클래스들이 있었는데 새로나오게된 이유가 뭔지부터 간단하게 살펴보고 신규 클래스에 대해 알아보자.


1. Date

자바에서 가장 역사가 깊은 날짜 클래스다. 객체를 생성하는데 2017년 3월 1일로 초기화하면서 생성하고싶다. 어떻게 하면 될까?


public Date(int year, int month, int date) {
this(year, month, date, 0, 0, 0);
}

오 마침 적절한 생성자가 이미 존재하고있다! 인자 명칭을 보니 매우 직관적이다. 뭐 생각할 필요도없이 객체를 생성하고 날짜를 확인해보자.


public static void main(String[] arg) throws IllegalAccessException, InstantiationException {
Date date1 =
new Date(2017, 3, 1);

System.out.println(date1.toString());
}

Sun Apr 01 00:00:00 KST 3917

??? 3917년 4월 1일이 출력되고있다. Date 클래스는 1900년부터 시작을하기때문에 1900년 기준으로 년도를 입력해줘야 원하는 값이 나온다. 또한 월 인덱스는 0부터 시작하기때문에 3월을 원한다면 2를 넣어줘야 한다.


Date date1 = new Date(117, 2, 1);

System.out.println(date1.toString());
Wed Mar 01 00:00:00 KST 2017

원하는 결과가 나오긴했지만 생성자부터 결코 직관적이지않다. 또한 해당 생성자는 이런 이유때문인지 이미 Deprecate 되어있다.


@Deprecated
public Date(int year, int month, int date) {
this(year, month, date, 0, 0, 0);
}


2. Calendar

Date 클래스는 위에 작성한 문제 말고도 long으로 날짜를 관리하는 등 VO(Value Object)라고는 보기힘든 설계의 클래스였다. 이런 문제를 해결하기위해 자바는 Calendar 클래스를 추가로 제공하기 시작했다. Calendar 클래스는 1900년부터 시작하는 설계는 해소되었지만 달은 여전히 0부터 시작하고있었다. 또한 Date를 완전히 포기하지 못했는지 여러가지로 Date와 엮이는 일이 많았다.

이 때문에 초급자같은경우 Date와 Calendar를 어떤 경우에 구분해서 써야하는지 혼동하는 경우도 많았다. 실제  실무 프로젝트의 프로덕션 코드에도 Date와 Calendar가 무분별하게 사용되고있는걸 목격한적도 있다.


또한 Calendar 클래스는 무분별한 숫자 상수의 남발로 API사용에 매우 큰 혼란이 오기도했다.


3. 가변(Mutable) 클래스

일반적으로 VO로 사용되는 클래스들은 불변이다. 대표적인 클래스로는 String 클래스가 있는데 String클래스는 내부 값을 변경할 수 없다. String 외에도 Integer, Long 등은 모두 불변클래스로 우리가 값을 바꿨다고 얘기하는 경우는 새로운 래퍼런스를 생성하여 반환하는 경우이지. 동일 래퍼런스 내의 값을 변경하진 못한다. 하지만 Date 클래스나 Calendar 클래스는 가변클래스로 그냥 무작정 값으로 쓰기에는 문제가 있었다.


class Period{
private Date start;
private Date end;

public Period(Date start, Date end){
// 30일을 넘기면 안된다는 표현
if(start - end > 30){
throw new IllegalArgumentException();
}

this.start = start;
this.end = end;
}
}

특정 조회기간을 담는 클래스이다. 시작일자와 종료일자를 인자로 받고있는데 생성자에서 30일 차이가 나는지 유효성 체크를 한뒤 유효하지않으면 예외를 발생한다.(실제 Date 객체간의 - 연산은 가능하지않다.)


정말 철저하게 테스트하려면 여러경우를 더 체크해야겠지만 예제코드니 저런 규칙이 있다는것만 알고 넘어가자.


Date date1 = new Date(117, 2, 1);
Date date2 = new Date(117, 2, 30);

Period period = new Period(date1, date2);

3월 1일부터 3월 30일까지를 조회하고자한다. 두 객체간의 날짜 차이는 30일을 초과하지않으므로 성공적으로 Period 객체가 생성될 것이다. 하지만 객체 생성 이후 이런 코드가 나타난다면 어떻게 될까?


Date date1 = new Date(117, 2, 1);
Date date2 = new Date(117, 2, 30);

Period period = new Period(date1, date2);

date2.setMonth(3);

객체를 정상적으로 생성하고 그 뒤에가서 date2 객체의 달을 4월로 변경시켰다. 그럼 date1과 date2의 날짜차이는 30일을 넘어서게되고, period의 end 필드와 date2는 서로 같은 객체를 참조하고있으므로 period 객체의 불변식도 깨지게된다. 하지만 그렇다고해서 또 다시 생성자가 실행되는건 아니므로 아무런 예외도 발생하지않고 코드는 실행되게 될것이다.


저건 내가 period클래스의 생성자에 아무리 철저히 유효성 체크를 한다해도 어쩔수가없는 부분이므로 방지하려면 내부에서 새로운 객체를 생성하도록 함으로써 해결해야한다.


class Period{
private Date start;
private Date end;

public Period(Date start, Date end){
// 30일을 넘기면 안된다는 표현
if(start - end > 30){
throw new IllegalArgumentException();
}

this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
}
}

지금이야 예제코드니 생성자만 수정해줬음 됐지만 복잡한 실무코드라면 Period내의 필드들이 사용되는 모든 곳에 객체 복사코드를 넣어줘야한다.


4. Joda Time(time package)

자바 API가 날짜부분에서 자꾸 문제를 야기하자 개발된 외부 라이브러리였다. 하지만 자바8에서 기본 API로 받아들이게됐다.


4-1. LocalDate, LocalTime, LocalDateTime

3개의 클래스를 나열했지만 직관적인 이름으로 한방에 알아차릴수 있을 것이다. LocalDate는 시간 정보 없이 날짜정보만을 다루며 LocalTime은 반대로 시간만을, LocalDateTime은 날짜와 시간을 함께 다룬다. 1900부터 시작한다거나 월이 0에서 시작하지않으며, 위에서 그렇게 단점이라고 설명했던 만큼 이 객체들은 불변 객체로서 성가시게 항상 복사 객체를 만들어야할 일도 없다.


LocalDate localDate = LocalDate.of(2017, 3, 1);
LocalTime localTime = LocalTime.of(17, 30, 30);
LocalDateTime localDateTime = LocalDateTime.of(2017, 3, 1, 17, 30, 30);

System.out.println(localDate);
System.out.println(localTime);
System.out.println(localDateTime);
2017-03-01
17:30:30
2017-03-01T17:30:30

매우 깔끔하게 출력도 포맷팅이 되어 나온다!


localDate1.withMonth(5);
localDate1.withDayOfMonth(10);
localDate1.plusDays(10);
localDate1.minusDays(10);

날짜의 각종 연산을 지원하는데 메서드명칭도 직관적이다. 메서드 명을 왜 잘 지어야하는지를 보여주는 좋은 예라고 생각되기도 한다.

누누히 말하지만 신규 추가된 날짜 API들은 모두 불변객체이다. Date나 Calendar의 set() 메서드처럼 실행객체를 변환하는게 아니므로 localDate1 변수는 아무리 저런 연산을 해도 날짜/시간이 변하지 않는다.


4-2. Duration, Period

기존 날짜 API로는 두 날짜가 서로 같은지 다른지 정도를 비교할 수 있을뿐 정확히 얼마나 차이나는지를 알려면 long으로 연산을 하고 다시 long을 Date로 변환하는 작업을 했어야 했었다. 하지만 Joda Time에서는 Duration과 Period라는 비교 클래스를 제공하고있다. Duration은 시간 비교를, Period는 날짜 비교를 지원한다.

LocalDate localDate1 = LocalDate.of(2017, 3, 1);
LocalDate localDate2 = LocalDate.of(2017, 3, 30);
LocalTime localTime1 = LocalTime.of(17, 30, 30);
LocalTime localTime2 = LocalTime.of(17, 31, 0);

System.out.println(Period.between(localDate1, localDate2).getDays());
System.out.println(Duration.between(localTime1, localTime2).getSeconds());


이정도가 실제 날짜 클래스 사용시 자주쓰는 API들이 아닐까 한다. 이 외에도 각종 API들을 제공하지만 API를 일일이 다 설명하는건 큰의미가 없을것같아 이정도로만 요약하고자한다. 현재 회사가 시간 클래스를 상대적으로 많이 사용해야하는 특성이 있는데 Date 클래스나 Calendar 클래스는 사용할때마다 사용법이 맞는지를 확인했어야했다. 메서드 명칭이나 상수의 의미들이 항상 헷갈렸기때문이다. 하지만 이제 신규 추가된 Joda Time API를 사용하여 개발을 진행하면 그런 불편함들이 많이 사라지지않을까 생각한다. 포스팅을 마치겠다.

'Java' 카테고리의 다른 글

DTO와 VO  (8) 2017.07.01
jackson custom serializer, deserializer 만들기  (3) 2017.04.19
Java8#06. time package  (1) 2017.03.04
servlet mapping /와 /* 차이점  (3) 2016.12.27
Reflections 라이브러리를 이용한 패키지 탐색  (0) 2016.12.24
Junit, Hamcrest 테스트케이스  (0) 2016.12.23
댓글
댓글쓰기 폼