Spring Request Date Format
👉 해당 포스트를 읽는데 도움을 줍니다.
0. 들어가면서
스프링 프레임워크를 사용하는 레거시 시스템들을 돌이켜 보면 날짜 포맷을 문자열으로 넘겨 받는 경우가 종종 있었습니다.
전달 받은 문자열을 SimpleDateFormat
클래스를 이용해 Date
객체로 변경하거나 문자열 그대로 데이터베이스에 저장하는 시스템도 있었습니다.
이번 포스트에선 스프링 프레임워크를 이용할 때 API 엔드 포인트(end point)에서 시간 문자열 포맷을 쉽게 시간 관련 클래스로 변경하는 방법을 정리하였습니다.
1. @JsonFormat 애너테이션 사용
스프링 프레임워크는 application/json
타입의 요청, 응답 메시지를 만들기 위해 기본적으로 jackson
라이브러리를 사용합니다.
@JsonFormat
애너테이션은 jackson
라이브러리 기능이며, 해당 애너테이션을 사용하면 날짜 데이터를 특정 포맷으로 변경할 수 있습니다.
다음과 같은 상황에 적용할 수 있습니다.
Content-type
이application/json
이며 요청 메시지 클래스 앞에@RequestBody
애너테이션이 붙은 경우@RestController
애너테이션이 붙은 컨트롤러의 응답을 처리하는 경우
Spring Framework Json Formatting
- 스프링 프레임워크은 기본적으로
json
타입 처리를 위해AbstractJackson2HttpMessageConverter
클래스를 사용합니다. AbstractJackson2HttpMessageConverter
클래스 내부에서 다음과 같은 기능을 수행합니다.readJavaType
메소드 -json
문자열을ObjectMapper
객체를 이용하여 특정 클래스로 변경writeInternal
메소드 - 특정 클래스를ObjectMapper
객체를 이용하여json
문자열로 변경
1.1. 구현 코드
JacksonRequest
클래스@RequestBody
애너테이션이 붙어서 요청 메시지를 해당 클래스를 통해 전달받습니다."yyyy-MM-dd HH:mm:ss.SSS"
문자열 날짜 포맷을java.util.Date
클래스로 전달받습니다."yyyy-MM-dd HH:mm:ss.SSS"
문자열 날짜 포맷을java.sql.Timestamp
클래스로 전달받습니다."yyyy-MM-dd HH:mm:ss.SSS"
문자열 날짜 포맷을java.time.LocalDateTime
클래스로 전달받습니다.
JacksonResponse
클래스@RestController
애너테이션이 붙은 컨트롤러 클래스의 리턴 값이므로json
형태로 응답합니다.java.util.Date
객체를"yyyy-MM-dd HH:mm:ss.SSS"
문자열 날짜 포맷으로 응답합니다.- 미지정 시
long
- 미지정 시
java.sql.Timestamp
객체를"yyyy-MM-dd HH:mm:ss.SSS"
문자열 날짜 포맷으로 응답합니다.- 미지정 시
long
- 미지정 시
java.time.LocalDateTime
객체를"yyyy-MM-dd HH:mm:ss.SSS"
문자열 날짜 포맷으로 응답합니다.- 미지정 시
"yyyy-MM-dd'T'HH:mm:ss.SSS"
- 미지정 시
package action.in.blog.controller;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.*;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.util.Date;
@RestController
public class JacksonController {
private static final String datePattern = "yyyy-MM-dd HH:mm:ss.SSS";
@Getter
@Setter
@NoArgsConstructor
public static class JacksonRequest {
@JsonFormat(pattern = datePattern)
private Date date;
@JsonFormat(pattern = datePattern)
private Timestamp timestamp;
@JsonFormat(pattern = datePattern)
private LocalDateTime localDateTime;
}
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class JacksonResponse {
@JsonFormat(pattern = datePattern)
private Date date;
@JsonFormat(pattern = datePattern)
private Timestamp timestamp;
@JsonFormat(pattern = datePattern)
private LocalDateTime localDateTime;
}
@PostMapping("/jackson")
public JacksonResponse getJacksonDto(@RequestBody JacksonRequest request) {
return JacksonResponse.builder()
.date(request.getDate())
.timestamp(request.getTimestamp())
.localDateTime(request.getLocalDateTime())
.build();
}
}
1.2. 테스트 코드
Content-Type
을application/json
.- 요청 메시지 데이터를
ObjectMapper
객체를 이용해json
문자열 값으로 변경합니다.- 날짜, 시간을
"yyyy-MM-dd HH:mm:ss.SSS"
형태의 문자열로 전달합니다.
- 날짜, 시간을
- 응답 메시지에
"yyyy-MM-dd HH:mm:ss.SSS"
형태의 문자열로 전달했던 데이터가 그대로 반환되었는지 확인합니다.
package action.in.blog.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import java.util.HashMap;
import java.util.Map;
import static org.hamcrest.Matchers.equalTo;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
public class JacksonControllerTests {
@Test
void givenStringDateFormat_whenGetJacksonDto_thenReturnJacksonResponse() throws Exception {
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("date", "2022-04-10 10:25:00.000");
requestBody.put("timestamp", "2022-04-10 10:25:00.000");
requestBody.put("localDateTime", "2022-04-10 10:25:00.000");
ObjectMapper objectMapper = new ObjectMapper();
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new JacksonController()).build();
mockMvc.perform(
post("/jackson")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(requestBody))
)
.andExpect(jsonPath("$.date", equalTo("2022-04-10 10:25:00.000")))
.andExpect(jsonPath("$.timestamp", equalTo("2022-04-10 10:25:00.000")))
.andExpect(jsonPath("$.localDateTime", equalTo("2022-04-10 10:25:00.000")));
}
}
1.3. 응답 결과
% curl -X POST --header "Content-type: application/json" --header "X-USER-HEADER: NORMAL" --data "{\"date\": \"2022-04-10 10:25:00.000\", \"timestamp\": \"2022-04-10 10:25:00.000\", \"localDateTime\": \"2022-04-10 10:25:00.000\"}" http://localhost:8080/jackson | jq .
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 233 0 114 100 119 9186 9589 --:--:-- --:--:-- --:--:-- 113k
{
"date": "2022-04-10 10:25:00.000",
"timestamp": "2022-04-10 10:25:00.000",
"localDateTime": "2022-04-10 10:25:00.000"
}
2. @DateTimeFormat 애너테이션 사용
@DateTimeFormat
애너테이션은 스프링 프레임워크에서 제공하고, 해당 애너테이션을 사용하면 날짜, 시간 형태를 쉽게 변경할 수 있습니다.
다음과 같은 상황에 적용할 수 있습니다.
- URL 뒤에 붙는 질의(query)에 날짜 형태의 문자열을 전달받는 경우
- 요청 메시지 클래스에
@ModelAttribute
애너테이션이 붙은 경우- 컨트롤러에서 별도 애너테이션 없이 클래스로 요청 메시지를 받는 경우
@ModelAttribute
애너테이션이 붙은 것과 동일합니다. Content-Type: application/x-www-form-urlencoded
인 경우 요청 메시지에@ModelAttribute
애너테이션을 붙여 처리합니다.
- 컨트롤러에서 별도 애너테이션 없이 클래스로 요청 메시지를 받는 경우
Spring Framework DateTimeFormat
URL
에 붙는 key-value 형태의 질의는AbstractNamedValueMethodArgumentResolver
클래스resolveArgument
메소드에 의해 처리됩니다.@ModelAttribute
애너테이션이 붙은 요청 메시지인 경우ModelAttributeMethodProcessor
클래스resolveArgument
메소드에 의해 처리됩니다.
2.1. 구현 코드
@DateTimeFormat
애너테이션은 문자열을java.sql.Timestamp
타입으로 변환 시 에러가 발생합니다.requestParam
메소드- URL 뒤에 붙은 key-value 형태의 질의를 통해 전달받는 데이터를 처리합니다.
modelAttribute
메소드- URL 뒤에 붙은 key-value 형태의 질의를 통해 전달받는 데이터를 처리합니다.
form
태그를 통해 전달받는 요청 메시지를 처리합니다.
package action.in.blog.controller;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.*;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.*;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.util.Date;
@RestController
public class DateTimeFormatController {
private static final String datePattern = "yyyy-MM-dd HH:mm:ss.SSS";
@Getter
@Setter
public static class ModelAttributeDto {
@DateTimeFormat(pattern = datePattern)
private Date date;
@DateTimeFormat(pattern = datePattern)
private LocalDateTime localDateTime;
}
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class JacksonResponse {
@JsonFormat(pattern = datePattern, timezone = "Asia/Seoul")
private Date date;
@JsonFormat(pattern = datePattern, timezone = "Asia/Seoul")
private LocalDateTime localDateTime;
}
@GetMapping("/request-param")
public JacksonResponse requestParam(
@DateTimeFormat(pattern = datePattern)
@RequestParam("date") Date date,
@DateTimeFormat(pattern = datePattern)
@RequestParam("localDateTime") LocalDateTime localDateTime) {
return JacksonResponse.builder()
.date(date)
.localDateTime(localDateTime)
.build();
}
@PostMapping("/model-attribute")
public JacksonResponse modelAttribute(@ModelAttribute ModelAttributeDto modelAttributeDto) {
return JacksonResponse.builder()
.date(modelAttributeDto.getDate())
.localDateTime(modelAttributeDto.getLocalDateTime())
.build();
}
}
2.2. 테스트 코드
requestParam
메소드 테스트- 날짜 형태 문자열을 요청 파라미터로 추가합니다.
- 전달한 날짜를 그대로 응답으로 전달해주는지 확인합니다.
modelAttribute
메소드 테스트Content-type
을application/x-www-form-urlencoded
으로 지정합니다.- 날짜 형태 문자열을 요청 파라미터로 추가합니다.
- 전달한 날짜를 그대로 응답으로 전달해주는지 확인합니다.
package action.in.blog.controller;
import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import static org.hamcrest.Matchers.equalTo;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
public class DateTimeFormatControllerTests {
@Test
void givenStringDateFormat_whenRequestParam_thenReturnJacksonResponse() throws Exception {
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new DateTimeFormatController()).build();
mockMvc.perform(
get("/request-param")
.param("date", "2022-04-10 10:25:00.000")
.param("localDateTime", "2022-04-10 10:25:00.000")
)
.andExpect(jsonPath("$.date", equalTo("2022-04-10 10:25:00.000")))
.andExpect(jsonPath("$.localDateTime", equalTo("2022-04-10 10:25:00.000")));
}
@Test
void givenStringDateFormat_whenModelAttribute_thenReturnJacksonResponse() throws Exception {
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new DateTimeFormatController()).build();
mockMvc.perform(
post("/model-attribute")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.param("date", "2022-04-10 10:25:00.000")
.param("localDateTime", "2022-04-10 10:25:00.000")
)
.andExpect(jsonPath("$.date", equalTo("2022-04-10 10:25:00.000")))
.andExpect(jsonPath("$.localDateTime", equalTo("2022-04-10 10:25:00.000")));
}
}
2.3. 응답 결과
/request-param
경로로 요청을 보냅니다.- URL 뒤에 요청 파라미터를 전달합니다.
% curl "http://localhost:8080/request-param?date=2020-04-10%2010:25:00.000&localDateTime=2020-04-10%2010:25:00.000" | jq .
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 76 0 76 0 0 5713 0 --:--:-- --:--:-- --:--:-- 38000
{
"date": "2020-04-10 10:25:00.000",
"localDateTime": "2020-04-10 10:25:00.000"
}
/model-attribute
경로로 요청을 보냅니다.- URL 뒤에 요청 파라미터를 전달합니다.
curl -X POST "http://localhost:8080/model-attribute?date=2020-04-10%2010:25:00.000&localDateTime=2020-04-10%2010:25:00.000" | jq .
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 76 0 76 0 0 5748 0 --:--:-- --:--:-- --:--:-- 38000
{
"date": "2020-04-10 10:25:00.000",
"localDateTime": "2020-04-10 10:25:00.000"
}
/model-attribute
경로로 요청을 보냅니다.Content-type: x-www-form-urlencoded
으로 지정합니다.- 요청 메시지를 key-value 형태로 전달합니다.
curl -X POST -H "Content-type: application/x-www-form-urlencoded" -d "date=2022-04-10+10:25:00.000&localDateTime=2022-04-10+10:25:00.000" "http://localhost:8080/model-attribute" | jq .
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 142 0 76 100 66 5937 5156 --:--:-- --:--:-- --:--:-- 71000
{
"date": "2022-04-10 10:25:00.000",
"localDateTime": "2022-04-10 10:25:00.000"
}
댓글남기기