Java의 Date type으로 yyyy-MM-dd
패턴의 request parameter를 바로 받아오려고 하면, 자동으로 변환되어 받아지지 않고 다음과 같은 오류 메시지가 발생하게 된다. 이 글에서는 어떻게 특정 형식의 request parameter를 Date type으로 바인딩할 수 있는지 정리해보고, 특히 Custom Formatter
. Converter
class를 정의해 사용하는 방식을 집중적으로 알아보고자 한다.
{
"timestamp": "2022-06-27T09:38:27.204",
"status": 400,
"code": "C001",
"message": "잘못된 입력값입니다.",
"errors": [
{
"field": "createdBefore",
"value": "2021-12-31",
"reason": "Failed to convert property value of type 'java.lang.String' to required type 'java.util.Date' for property 'createdBefore'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [java.util.Date] for value '2021-12-31'; nested exception is java.lang.IllegalArgumentException"
}
]
}
SimpleDateFormat.parse
methodpublic class DateQuery {
/* ... */
private String createdBefore;
public Date getCreatedBefore() {
SimpleDateFormat dt = new SimpleDateFormat("yyyy-mm-dd");
return dt.parse(createdBefore);
}
/* ... */
}
가장 간단하게 적용할 수 있는 방법은 String으로 받아온 request parameter를 다시 Date type으로 변환해 주는 방식일 것이다. SimpleDateFormat
class의 parse
method를 사용하면 특정 format의 String을 Date type으로 변환 가능하다. Date type으로의 변환이 필요한 field의 getter를 정의할 때나, dto의 변환 과정에서 적용할 수 있을 것이다.
@DateTimeFormat
annotationpublic class DateQuery {
/* ... */
@DateTimeFormat(pattern="yyyy-MM-dd")
private Date createdBefore;
/* ... */
}
String으로의 바인딩 없이 바로 request parameter의 String을 Date type으로 받아오고자 할 때는 @DateTimeFormat
annotation을 사용할 수 있다. 마찬가지로 Date type으로의 변환이 필요한 field에 일일히 annotation을 명시해서 formatting을 적용해 줄 수 있다.
DateTimeFormatterRegistrar
class 적용 (Date
type 불가)public class ViewConfiguration extends WebMvcConfigurerAdapter {
/* ... */
@Override
public void addFormatters(FormatterRegistry registry) {
DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
registrar.setDateTimeFormatter(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
registrar.registerFormatters(registry);
}
/* ... */
}
앞서 살펴본 방식들은 Date type을 사용하는 부분들이 발생할 때마다 개별적으로 적용해 주어야 한다는 단점이 존재한다. 만약 request parameter로 들어오는 날짜 형태의 String format이 모두 일치한다면 Spring configuration에서의 설정을 통해 웹 어플리케이션의 전 범위에서 Date type의 pattern을 바인딩해 줄 수 있다. WebMvcConfigurerAdapter
을 상속받은 configuration class에서 addFormatters
method를 overriding해서 formatter 정보를 설정해 줄 수 있다. DateTimeFormatterRegistrar
instance에 필요한 pattern의 DateTimeFormatter
를 설정해주고 registry
에 등록해 주면 Spring이 기본으로 제공해 주는 DateTimeFormatter
를 사용해 설정하게 되는 것이다.
다만 이 방법을 사용해서 Java의 Date type으로의 conversion은 불가능하다. 테스트를 해 보면 여전히 에러 메시지가 발생하는 것을 확인할 수 있는데, Date type 대신 Java 8 이상의 LocalDateTime
class를 사용해 보면 제대로 작동하게 된다. Java 8 이상에서는 기존의 Date
, Calendar
등의 몇 가지 문제를 해결하기 위해 LocalDateTime
class를 도입했는데, 다양한 conversion method들을 제공하기 때문에 가능하다면 날짜 및 시간 field에는 LocalDateTime
class를 사용하는 것이 추천된다.
Formatter
class 정의하지만 개발 중이던 웹 어플리케이션은 이미 날짜에 대해 Date
type을 내부적으로 사용하도록 설계가 완료된 체계였기 때문에, 위 방법을 사용할 수 없었다. LocalDateTime
type을 사용한다면 Spring이 기본으로 지원하는 DateTimeFormatter
class를 사용할 수 있겠지만, 그것이 불가능한 Date
type이라면 직접 custom Formatter
class를 정의해서 Spring configuration에 추가할 수 있을 것으로 생각했다.
public interface Printer<T> {
String print(T var1, Locale var2)
}
public interface Parser<T> {
T parse(String var1, Locale var2) throws ParseException;
}
public interface Formatter<T> extends Printer<T>, Parser<T> {
}
Formatter
는 Object
와 String
간의 타입 변환에 특화된 데이터 바인딩 인터페이스이다. Printer
와 Parser
interface를 상속받은 interface로, Generic으로 처리하려는 타입 인자를 받아 print
와 parse
method를 구현해 사용할 수 있다. Locale
에 따라 문자열을 다국화하는 기능을 제공하기도 한다. 앞서 살펴본 기본 DateTimeFormatter
와 유사하게 WebMvcConfigurer
인터페이스의 addFormatters
method를 오버라이딩해서 구현한 Formatter
를 등록할 수 있다.
public class DateFormatter implements Formatter<Date> {
@Override
public Date parse(String s, Locale locale) throws ParseException {
SimpleDateFormat dt = new SimpleDateFormat("yyyy-mm-dd", locale);
return dt.parse(s);
}
@Override
public String print(Date date, Locale locale) {
SimpleDateFormat dt = new SimpleDateFormat("yyyy-mm-dd", locale);
return date == null ? null : dt.format(date);
}
}
public class ViewConfiguration extends WebMvcConfigurerAdapter {
/* ... */
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addFormatter(new DateFormatter());
}
/* ... */
}
Formatter
class의 parse
, print
method를 Date
class에 대해 구현한 custom formatter인 DateFormatter
를 Spring configuration의 addFormatters
method에 등록하면 간단히 설정을 완료할 수 있다. DateFormatter
class의 내부적 구현도 기본적으로 사용되는 SimpleDateFormat
class의 parse
, format
method로 간단히 정의해주면 된다. 설정을 적용한 뒤 확인해 보면 DateFormatter
의 parse
method가 Date
type으로 mapping되어야 하는 request parameter를 만나면 자동으로 호출되어 conversion을 진행해 주는 것을 볼 수 있다.
Converter
class 정의 (필요시)DateFormatter
의 parse
method의 작동은 request parameter의 작용으로 쉽게 확인할 수 있었다. 그렇다면 print
method는 어떻게 작동하는 것일까? Spring mvc pattern에서는 modelattribute로 추가된 entity에 대해 jsp에서 <spring:eval>
태그를 포함시켜 print
method가 적용된 값을 view에서 사용할 수 있었다. 하지만 POST
방식의 request에서 json 형식의 body에 value로 포함시키거나, API response로 반환하는 json 형식의 response entity의 경우에는 formatter의 parse
/print
method의 결과가 반영되지 않는 것을 확인할 수 있다. 이는 JSON 형식에 대한 serialization/deserialization 과정에는 formatter가 아닌 별도의 serializer/deserializer(기본적으로 jackson)가 적용되기 때문이다.
따라서 JSON 형식의 데이터에 대해서 Date
type으로의 데이터 바인딩을 적용하고 싶다면 기존의 @DateTimeFormat
annotation이 아닌 @JsonFormat
annotation을 사용해 각각 적용하거나, custom serializer/deserializer를 구현해 application 전체 범위에 적용해야 한다. 만약 Restful API를 통해 요청을 주고받는다면 JSON 형식으로 API response를 전달하게 되므로 DateFormatter
의 print
method가 적용될 일이 없게 된다. 이 경우에는 String
-> Date
로의 변환만이 필요하기 때문에 불필요한 부분을 제외하고 Converter
interface로 구현해줄 수 있다.
public class StringToDateConverter implements Converter<String, Date> {
@Override
public Date convert(String str) {
SimpleDateFormat dt = new SimpleDateFormat("yyyy-mm-dd", Locale.KOREA);
return dt.parse(str);
}
}
public class ViewConfiguration extends WebMvcConfigurerAdapter {
/* ... */
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addFormatter(new StringToDateConverter());
}
/* ... */
}
Converter
interface는 Formatter
interface의 조금 더 general한 형태라고 볼 수 있는데, Generic으로 source와 target type을 받아 convert
method를 구현해 사용할 수 있다. DateFormatter
의 parse
method 부분을 convert
method로 구현한 StringToDateConverter
class를 마찬가지로 addFormatters
method에 등록해 주면 동일하게 작동한다.