Lilyrs

Long.parseLong ago

프로그래밍은 결국 인간의 일을 컴퓨터가 할 수 있게 옮겨주는 일이다. 그렇게 돌기 위해서는 인간의 규칙을 컴퓨터에도 모방시켜두어야하는데, 그 중에 어려운 일로 손을 꼽으라면 복잡한 인공지능도 있겠지만, 나는 시간을 꼽고 싶다.

시간은 생각보다 골때리는 물건이다. 일반적으로 매일 같이 24시간을 보내지만, 실제로 이 24시간은 지구 자전에 의해 태양빛이 비추는 정도에 따라 달라지고 있으며 자전속도 조금씩 느려짐에 따라 하루 의 정의는 바뀔지도 모른다.[1]

여러 자연 요인에 의해 꾸준히 오차는 쌓여가고 있으며 이 오차를 잡아주기 위해 인류는 역사적으로 많은 노력을 했다. 달력 체계를 바꾸기도 했고, 윤년을 도입하기도 했으며 2015년에는 윤초로 인해 이에 미리 대비하지 못한 JVM 기반 응용 소프트웨어들은 VM이 정상적으로 동작하지 않는 문제로 인해 장애를 겪기도 했다.[2]

온갖 노력을 해서 컴퓨터 세계에 인간의 시간을 표현하려 노력을 한 것을 보면 그만큼 시간은 중요하다는 것을 알 수 있다.

Java에서는 초기에 Date 클래스를 이용해 이 날짜를 표현했으며, 이것만으로는 요일과 달을 다루는데 복잡함이 많아 Calendar 클래스를 Java 1.1에서 도입했다.[3] 하지만 이것으로도 복잡한 날짜 계산에는 어려움이 많았기 때문에 Java 8에서야 드디어 java.time 패키지를 통하여 쓸만한 라이브러리들이 제공되었다. 자세한 역사는 NAVER D2에서 제공하는 이 링크의 내용을 살펴보길 바란다.

어찌되었든, 현재는 대안 라이브러리를 이용해서라도 좋은 대안을 찾을 수 있는 상황이다. 심지어 Java 8부터 제공되는 시간 API를 백포팅한 ThreeTen Backport 라이브러리도 있다. 이 글이 작성된 시점에서 최신 버전은 1.4.0이고, 최소 요구 사항은 Java 1.6이다. 1.8에서도 물론 사용할 수 있으며, 더불어 Spring JPA라면 ThreeTenBackPortJpaConverters 가 제공하는 컨버터를 통해 약간의 수고로움[4]만 거치면 5.0대 Hibernate에서도 불편한 DateCalendar 대신 Threeten 백포트 라이브러리를 통해 최신 API의 혜택을 누릴 수 있다.[5]

이런 라이브러리를 강조하는 이유는, 오늘 본 신기하면서도 납득되면서도 짜증나는 이 날짜 비교 코드 때문이다.

1
2
3
4
5
6
String acceptDate = "20191011";
String keptDate = "20191013";

if (Long.parseLong(keptDate) < Long.parseLong(acceptDate)) {
log.warn("접수일이 보관일보다 앞서있습니다. 이 데이터를 무시합니다.");
}

최근에 구형 Oracle 데이터베이스에 물려 돌아가는 Java 기반 레거시 서비스 코드를 살펴볼 일이 생겼는데, 이런 코드를 목격하고서 잠시 무슨 생각인가 고민했고 생각보다 얼추 돌긴 한다는 점에서 신기한 코드이다.[6] 그러나 이런 방식이 년수/월/일을 증감시키는 불완전하며 테스트되지 않은 괴악한 유틸리티를 만드는데 쓰이기도 해서 문제다.

예를 들어 2019년 5월 10일의 다음달인 6월 10일을 만든다고 치면, Java 8의 API를 이용하면 LocalDate nextMonth = localDate.plusMonths(1L) 와 같이 보기에도 좋고 12월의 다음 달도 잘 처리해주는 검증된 라이브러리를 이용할 수 있지만 이 레거시스러운(?) 코드는 아래와 같이 기가 막히는 방법으로 다음 달을 구한다. 예측되는가?

1
2
String currentDate = "20190510":
String nextMonthDate = String.valueOf(Long.parseLong(currentDate) + 100);

백의 자리와 천의 차리가 월에 예약되어있으므로, 100을 더해서 백의 자리를 1 올려주면 다음달이 계산되는 것이다. 즉 20190510 + 100 = 20190610 이다. 숫자 대 숫자로 계산해보면 그렇다.

만약에 20191210 이면, 충분히 테스트하지 않고 짜게 된다면 20191310 을 결과로 반환하게 될 것이고, 만약 20190531 이면 20190631 로 없는 날짜를 표시하게 된다.[7] 1월부터 12월까지 31일이 있고 없고는 비교적 간단한 조건문을 통해 커버할 수 있지만, 2020년은 윤년이 있어 다행인데 2019년에는 2월에 29일이 없다. 1월 29일의 다음 달을 구하라고 하면 이런 오류가 발생할 수 있다.

조금 더 당해본 경험이 많은 사람이 만든 프로젝트 공용 라이브러리에는 이 문제를 피하기 위해 더 기묘한 코드를 작성해놓는 상황에 빠지는데, SimpleDateFormat 클래스를 이용해서 날짜 처리를 다 해놓은 다음에 다시 YYYYMMDD 꼴로 되돌려 문자열로 저장해버리는 것이다.

2019년 들어서까지 이런 기괴한 선택을 할 이유는 없다. 일전에 언급한 Threeten BackPort 같은 경우는 Java 1.6부터 대응하며 별도의 의존성이 필요하지 않다. Spring boot 1.5대를 이용하는 경우 Java 7을 타게팅해 개발한다면 충분히 선택할 수 있는 좋은 라이브러리다.

물론 잘 돌아가는 레거시 시스템을 건들고 싶진 않을 것이고, 꼼꼼하게 테스트가 어려운 시스템에 개선 좀 하겠다고 날짜 라이브러리 교체했다가 경험과 테스트 부족으로 서비스 장애가 발생해선 안된다. 만약에 도입 의지가 있다면 꼼꼼한 테스트 계획과 도입 가능한 부분에 대한 유닛 테스트 도입을 고민해보길 권한다. 레거시 서비스에 도입이 어렵다면, 이미 이렇게 망한 건 어쩔 수 없고 향후 개발되는 프로젝트에라도 더이상 문자열을 숫자로 바꿔 날짜를 계산하지 않도록 하길 바란다.

이 글의 제목은 A long long time ago 를 비꼰 Long.parseLong ago 이다. 더이상 이런 코드가 양산되지 않아 옛날 이야기가 되길 바라는 마음에 선정한 제목이다.


  1. 1.자전 속도 느려지는 지구… 5년 내 强震 늘어날 수도
  2. 2.구글은 이를 Leap Smear 기법을 통해 예방했다. 한번에 '59분 60초'가 만들어지면 서비스에 큰 문제가 생기므로, 24시간에 거쳐 1초를 조금씩 나눠 동기화시켜 최종적으로 윤초를 반영해 부담을 줄이는 기법이다.
  3. 3.Calendar (Java Platform SE 7) 문서를 살펴보면 'Since: JDK1.1' 이란 내용이 적혀있다. 한편 Date 는 JDK 1.0이 발표될 때부터 포함된 클래스이다.
  4. 4.@Convert 어노테이션과 ThreeTenBAckPortJpaConverters의 컨버터들을 이용해 충분히 컨버팅 가능하다. 예시는 오라클 공식 API 문서의 내용으로 대체한다.
  5. 5.Spring Boot Data JPA Starter 1.5.19.RELEASE 패키지 기준 Hibernate는 5.0.12.FINAL이 제공된다.
  6. 6.개인적 경험으로 Oracle 기반 서비스들은 테이블 선언시 날짜 칼럼의 기본값으로 DEFAULT TO_CHAR(SYSDATE,'YYYYmmdd') 와 같이 문자열로 시각을 저장하는 경우가 좀 많은 것 같다.
  7. 7.6월은 30일까지다.