[Spring] Formatter/Converter interface를 사용해서 특정 형식의 Request Parameter를 Date type으로 바인딩하기

lsjbh45·2022년 9월 8일
0

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 method

public 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 annotation

public 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를 사용하는 것이 추천된다.

Custom 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> {

}

FormatterObjectString간의 타입 변환에 특화된 데이터 바인딩 인터페이스이다. PrinterParser interface를 상속받은 interface로, Generic으로 처리하려는 타입 인자를 받아 printparse 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로 간단히 정의해주면 된다. 설정을 적용한 뒤 확인해 보면 DateFormatterparse method가 Date type으로 mapping되어야 하는 request parameter를 만나면 자동으로 호출되어 conversion을 진행해 주는 것을 볼 수 있다.

Custom Converter class 정의 (필요시)

DateFormatterparse 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를 전달하게 되므로 DateFormatterprint 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를 구현해 사용할 수 있다. DateFormatterparse method 부분을 convert method로 구현한 StringToDateConverter class를 마찬가지로 addFormatters method에 등록해 주면 동일하게 작동한다.

profile
개발을 공부하며 깊게 고민했던 트러블슈팅 과정을 공유하고자 합니다.

0개의 댓글