REST API 날짜/시간 표현 정하기

2달전 제 뒤에 앉아았는 개발자님께서 왜 알아보기 어렵게 날짜를 숫자로 사용해야하냐고 의문을 가진 적이 있다. API를 처음 구성할 때 참고했던 API가 사용했다는 이유로, spring 400에러 응답에서도 long 형태의 시간표현을 한 사례, Timezone과 DB처리에서 장점이 있다는 이유로 적용했던 Unix Time형태의 날짜 표현방식이 문제가 될 수도 있다는 생각이 들었다. 그렇게 불편하다면 한번 바꿔볼까? 하는 생각과 함께 고민을 시작했다.

clock

API 설계를 위해 REST에 대해 학습하는 과정에서 우리는 URI, Method, Http Status 에만 집중한다. 하지만 API를 개발하고 사용하는 목표는 결국 데이터를 주고 받는데 있기 때문에 데이터가 담기는 Parameter, Request/Response Body의 형태를 정하고 Interface로 약속해야 한다.

  • 문자열
  • 숫자
  • 논리(Boolean)
  • 날짜 / 시간

보통 데이터를 표현하기 위해서는 위의 4가지 형태가 필요하고 그 중에서 날짜와 시간은 프로그래밍 언어마다 그 구성형태가 다르다.  json 응답에서 이 날짜와 시간의 표현방법을 고민하고 적용방안에 대해 이야기하고자 한다.


Unix Time vs ISO 8601

timestamp

API를 위한 시간표현에서 가장 인기있는 것은 Unix TimeISO-8601이고 이 외에도 다양한 방법이 존재하지만 자신들만의 형태를 가질 것이 아니라면 의미가 없기 때문에 이 두가지만 고려했다.

표현방식에 따른 가독성과 변환

Unix Time은 1970-01-01 UTC 에서의 경과시간을 초(or 밀리초[1])로 환산해서 숫자로 나타내고 ISO 8601은 사람이 인식할 수 있는 문자로 나타낸다.

1
2
3
4
5
6
{
  "unix_time":1521739975,
  "unix_time_milisecond":1521739975123,
  "iso-8601":"2018-03-22T17:32:55+09:00",
  "iso-8601_timestamp":"2018-03-22T17:32:55.123+09:00"
}

Unix Time은 사람이 인지할 수 있는 형식이 아니기 때문에 바로 이해하기 어려우나 ISO 8601은 즉시 인지가 가능하다. 반면 프로그램 상에서는 각 언어와 환경마다 대부분 가지고 있는 Date 처리에서 unix time은 즉시 변환이 가능지만 ISO 8601은 언어와 버전에 따라서 지원되지 않는 경우가 존재한다.

javascript를 예로 ECMAScript 5부터 Date에서 RFC2822 또는 ISO 8601를 지원하는데 IE9 미만에서는 ECMAScript 5를 하지 못하기 때문에 바벨과 같은 트랜스파일러를 사용하는 것과 같은 주의가 필요하다. 그나저나 아직도 IE7,8을 대응하냐고 하면..

유저테스트와 문서화에서도 이 가독성이 영향을 주는데, 동료인 김창묵님의 도움으로 Swagger에 날짜변환 Tool을 임베드해 그 단점을 보완하며 사용하고 있지만 사실 입력할 때마다 불편함을 느낀다.

swagger

타임존과 정확성

국내에서만 제공하는 서비스를 하고 있다면 그다지 중요한 부분이 아니지만 region에 따라서 시간대가 다른 서비스를 하고 있을 경우 ISO는 약점으로 작용할 수 있다.

1
2
3
4
5
{
  "local":"2018-03-23T13:10:10",
  "utc":"2018-03-23T13:10:10Z",
  "utc_offset":"2018-03-23T13:10:10+09:00"
}

Unix Time의 경우 UTC로부터의 Offset이기 때문에 타임존과 상관없이 일정한 값을 가지므로 그 표현과 사용에 큰 제약이 없다. IOS 8601은 위와 같이 명시하지 않으면 로컬타임으로 인식하는데 다른 시간대를 사용하는 지역에 동시에 서비스를 하거나 서머타임을 적용하는 경우 시간에 대한 혼동이 올 수 있다. 입력받은 시간을 다른 타임존으로 인식해서 사용해야 하는 경우에도 단순한 덧셈, 뺄셈이지만 시간 확인에 대한 어려움을 경험하게 된다.

제한된 정보표현

시간 정보가 필요없고 날짜만 사용하는 경우 Unix Time은 표현법이 애매해진다. 무조건 시간의 정보를 포함해서 사용해야하기 때문에 이것이 날짜만 포함된 데이터인지 시간과 함께 있는 데이터인지 구분할 수 없다. ISO는 시간영역을 제거하면 되기 때문에 년/월/일, 년/월, 년 등의 제한된 정보를 간편하게 표현하는데 좋다.

1
2
3
4
5
6
{
  "unix_time_date":1521763200000,
  "iso_8601_date":"2018-03-23",
  "iso_8601_month":"2018-03",
  "iso_8601_year":"2018"
}


Spring Boot 에서 사용방법

그래서 그 두가지를 실제로 개발하려면 뭘 어떻게 해야할까? 개발환경에 대한 영향을 보기 위해 Spring Boot를 기반으로 두 방식의 사용방법을 확인했다.

기본적으로 LocalDate를 사용해서 json 응답하면 지나치게 친절하고 장황한 왜 이런 응답을 할까 싶은 응답을 보게 된다. 이 응답과 Parameter로 시간을 받을 때 처리를 바꿔보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
  "localDate":{
    "year":2018,
    "month":"JANUARY",
    "era":"CE",
    "dayOfYear":1,
    "dayOfWeek":"MONDAY",
    "leapYear":true,
    "dayOfMonth":1,
    "monthValue":1,
    "chronology":{
      "id":"ISO",
      "calendarType":"iso8601"
    }
  } 
}

Parameter 처리

1
2
3
4
5
6
@GetMapping("/orders")
public ResponseEntity<List> getOrdersByDate(
    @RequestParam("date")
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) {
    // Do
}

DateTimeFormat으로 요청 Parameter의 형태를 지정하면 별다른 변환처리 없이 ISO 형태의 데이터 수신이 가능하다.

1
2
3
4
5
6
@GetMapping("/orders")
public ResponseEntity<List> getOrdersByDate(
    @RequestParam("date") long dateNumber) {
    LocalDate date = Instant.ofEpochMilli(dateNumber).atZone(ZoneId.systemDefault()).toLocalDate();
    // Do
}

반면 Spring 에서 제공하는 기능이 없는 Unix Time은 별도로 long을 LocalDate로 바꾸는 작업이 필요하다.

Spring Boot 1.5 (ISO 8601)

1.5 버전에서는 Java 8의 Date/Time API를 처리하기 위해 아래의 작업이 필요하다.

1
2
3
4
<dependency>
  <groupId>com.fasterxml.jackson.datatype</groupId>
  <artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
1
dependencies { compile group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: '2.9.4' }
1
spring.jackson.serialization.WRITE_DATES_AS_TIMESTAMPS = false

maven pom.xml 또는 Gradle에 위의 디펜던시를 추가하고 application.properties에 serialize 설정을 추가하면 예상할 수 있는 결과로 응답된다.

1
2
3
4
5
6
{  
  "localDate":"2018-01-01",
  "localTime":"10:24",
  "localDateTime":"2018-01-01T10:24:00",
  "zonedDateTime":"2018-01-01T10:24:00+09:00"
}

Spring Boot 2.0 (ISO 8601)

2.0이 되면서 훨씬 간편해졌다. Java 8을 최소 버전으로 지원하면서 별다른 설정없이 아래의 응답을 제공한다.

1
2
3
4
{
  "localDateTime": "2018-01-01T10:24:00.445428",
  "offsetDateTime": "2018-01-01T10:24:00.445428+09:00",
}

Unix Time 응답설정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@JsonComponent
public class JsonConverter {
    public static class LocalDateTimeJsonSerializer extends JsonSerializer<LocalDateTime> {
        @Override
        public void serialize(LocalDateTime localDateTime, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
            Date out = Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
            jsonGenerator.writeNumber(out.getTime());
        }
    }
    public static class LocalDateTimeJsonDeserializer extends JsonDeserializer<LocalDateTime> {
        @Override
        public LocalDateTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
            Date in = new Date(jsonParser.getValueAsLong());
            return LocalDateTime.ofInstant(in.toInstant(), ZoneId.systemDefault());
        }
    }
}

Spring에서 처리해주는 부분은 없고 Json 응답의 serialize/deserialize 를 등록해서 전역적으로 변경하도록 지정하면 모든 LocalDateTime에 대해서 long 형으로 변환하고 long 데이터는 LocalDateTime 으로 받을 때 변환되어 받아진다.


글을 마치며 나름의 결론

Spring을 기반으로 이야기하다보니 각 표현방식의 장단점을 넘어 ISO 8601이 더 효과적으로 보인다. 하지만 API를 개발하는 개발자와 그것을 사용하는 클라이언트 간에 어떠한 형태로든 약속이 되어 있다면 그 방법이 일관된 커뮤니케이션을 위해서 효과적이라고 생각한다.

이미 정해진 인터페이스는 굳이 변경하지 말자

저의 경우처럼 비록 2년동안 사용해왔던 표현형식이 있지만 각 부분들 담당하는 개발자와 시스템간에 변경에 대한 이해가 있거나 아직 API 구성을 시작하지 않은 상황이라면 Spring 환경에서는 ISO 8601을 추천하고 싶다.

또한 Spring의 기본 제공되는 것 외에도 직접 Unix Time을 사용하도록 할 수 있으나 자신이 사용하는 기술진영과 프레임워크에서 주도하는 방식을 이용해서 설계하는 것이 향후 버전대응에도 유리할 것으로 기대한다.

인터페이스는 약속이다. 각자의 상황과 이해에 따른 합의된 선택을 하면 후회가 없지 않을까?

주석

[1]: javascript의 Date는 milisecond 단위로 표현한다.


Popit은 페이스북 댓글만 사용하고 있습니다. 페이스북 로그인 후 글을 보시면 댓글이 나타납니다.