굉장히 간단한 개념이고 코드도 간결하지만 잘만 사용하면 API 호출이던 MVC이던 꽤나 유용하게 사용할 수 있을 거 같아서 정리해보아요이이.
1. 타입 컨버터
요녀석이 무엇이냐. 간단하게 아래 코드를 보자.
@GetMapping("/hello-v2")
public String helloV2(@RequestParam Integer data){
System.out.println("data = " + data);
return "ok";
}
위와 같은 코드에서 data의 타입이 Integer 일 수 있는 이유는 스프링에서 자체적으로 아래와 같은 역할을 해주기 때문이다.
@GetMapping("/hello-v1")
public String helloV1(HttpServletRequest request){
String data = request.getParameter("data");//문자 타입으로 조회
Integer intValue = Integer.valueOf(data);//숫자 타입으로 변경
System.out.println("intValue = " + intValue);
return "ok";
}
문자열로 들어오는 파라미터 값을 Integer로 해주기 때문. 요 녀석이 타입 컨버터가 해주는 역할이다. 즉 String->Integer.
2. 직접 만들기
타입 컨버터를 만들어서 사용하면 꽤나 유용할 것이다. 무슨 뜻이냐 하면은, 파라미터는 문자열로 들어오는데 이 문자열을 적절히 파싱해서 객체로 만들고 싶다면 String->Object 의 컨버터를 만들어 주면 된다.
간단한 예시로 "ip주소:포트" 와 같이 문자열로서 넘어온 파라미터를 IpPort객체로서 저장하고 싶다면,
@Slf4j
public class StringToIpPortConverter implements Converter<String, IpPort> {
@Override
public IpPort convert(String source) {
log.info("convert source={}", source);
String[] split = source.split(":");
String ip = split[0];
int port = Integer.parseInt(split[1]);
return new IpPort(ip,port);
}
요런 컨버터를 만들어주었다. org.springframework.core.convert.converter.Converter 에 있는 Converter를 상속받아 <S,T>에 타입을 잘 명시해주고 convert 메서드를 구현하면 된다. 그러면 이제 이 컨버터가 필요한 컨트롤러마다 가서 new StringToIp~~ 이렇게 해서 convert 메서드를 호출해야 하겠지?.. ?
이 귀찮은 과정을 매번 할 거면 차라리 직접 하지 뭐하러 컨버터를 사용할까요? 그러하니 컨버터들을 등록받아서 우리가 원하는 역할을 해주는 녀석이 있는데 그 이름은 바로
3. ConversionService
스프링 내부에서 ConversionService를 이용하여 타입을 변환한다. 우리가 흔히 하는 String->Integer도 해주고 있었던 것이다.
그렇다면 ConversionService에 우리의 컨버터를 등록하려면 어떻게 해야할까?
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new StringToIpPortConverter());
}
}
WebMvcConfigurer에는 addFormatters라는 메서드가 있어서 요 메서드를 통해서! 컨버터를 등록하고 @Configuration으로 띄우면 된다.
그렇다면 요청 파라미터,Path Variable 를 사용할때는 감이 오는데 MVC에서는 어떻게 사용 된다는 거야?
@ModelAttribute에 등록해 둘 객체에 IpPort타입 변수를 선언해 두면, 문자열로 넘어올 form 값의 "ip주소:포트"이 StringToIpPortConverter가 자연스럽게 돌아가서 사용 되겠죠오?
3. 포맷터
이처럼 컨버터는 어느 타입이건 변환의 역할을 톡톡이 해내 준다. 하지만 단방향이라는 것과 개발자 입장에서는 객체-문자열 관계를 제외하고는 특별히 사용될 것 같지 않아 보인다. 그 외의 부분은 스프링이 알아서 해주니깐!
그 역할이 주가 되어서 해당 기능을 해주는 녀석이 바로 포맷터이다. 포맷터는 심지어 Locale 정보를 제공받아서 국가별로 다른 정보를 알잘딱깔센으로 바꾸어 준다.
포맷터의 인터페이스는 아래와 같다.
public interface Printer<T> {
String print(T object, Locale locale);
}
public interface Parser<T> {
T parse(String text, Locale locale) throws ParseException;
}
public interface Formatter<T> extends Printer<T>, Parser<T> {
}
print는 객체->문자열, parser는 문자열->객체 역할을 한다.
예를 들어 다음과 같은 포맷터가 필요하다고 해보자. 숫자가 들어오면 10^3단위로 ,를 찍어주는 마치 100000 <->"100,000"으로 해줄 포맷터가 필요하다고 하면 다음과 같은 포맷터를 만들어 보자.
@Slf4j
public class MyNumberFormatter implements Formatter<Number> {
@Override
public Number parse(String text, Locale locale) throws ParseException {
log.info("text={}, locale={}",text,locale);
//"1,000"-> 1000
NumberFormat format = NumberFormat.getInstance(locale);
return format.parse(text);
}
@Override
public String print(Number object, Locale locale) {
log.info("object={}{, locale={}",object,locale);
NumberFormat instance = NumberFormat.getInstance(locale);
return instance.format(object);
}
}
참고로 Number 객체는 Integer, Float 등 별별 숫자란 숫자는 다 품을 수 있고 NumberFormat의 parse 함수는 ,를 찍어주는 함수이다. 직접 구현해도 되지만 이게 더 편하니깐~
타입 컨버터와 마찬가지로 이제 이 포맷터를 등록을 해야만 하는데, 생각해보면 포맷터는 사실상 컨버터의 세부 버전이니 ConversionService에 등록이 가능 해야 한다. 그런 우리의 마음을 알아주는 듯, DefaultFormattingConversionService라는 ConversionService가 존재하는데 이 클래스에서 컨버터를 추가하고 싶으면 addConverter, 포맷터를 추가하고 싶으면 addFormatter를 이용하면 된다. 이후 포맷터이건 컨버터이건 사용법은 똑같다!
...
registry.addFormatter(new MyNumberFormatter());
...
주의할 것은 같은 역할을 하는 컨버터가 있다면 포맷터는 우선순위에서 밀리게 된다.
5. 기본 포맷터
스프링은 개발자를 위해서 자주 쓰일 만한 포맷터를 미리미리 만들어 주었다. 가장 유용해 보이는 것들 중 하나는 @DateTimeFormat이다. 방학간에 어플 개발을 하면서 우연히 검색해서 쓰고 있는 어노테이션이었는데 알고보니 이렇게 깊은 과정이 있는지 생각도 못했다. 그냥 뭐 롬복 정도 되는 줄 ㅎㅎ
사용법은 아래와 같이 객체를 선언하면 된다.
@Data
static class Form{
@NumberFormat(pattern = "###,###")
private Integer number;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime localDateTime;
}
이로써 localDateTime은 웹에 뿌려질때 field 태그라던지
th:text="${{form.localDateTime}}"
과 같이 두번의 {}를 통해 formatter를 적용하여 "yyyy-MM-dd HH:mm:ss"와 같은 형식으로 model에 담게 된다.
6. 주의 사항
하지만 오해하면 안되는 것이 있다. 이 ConversionService는 컨버터를 등록해주다 보니 HttpMessageConverter와 관계가 있어 보이지만 그러하지 않다. 단지 컨버전 서비스는 @RequestParam , @ModelAttribute , @PathVariable , 뷰 템플릿 등에서 사용할 수 있는 것이다. 그렇기에 json으로 객체를 변환할때 이 컨버터는 쓰이지 않는 다는 것이다. HttpMessageConverter 의 역할은 HTTP 메시지 바디의 내용을 객체로 변환하거나 객체를 HTTP 메시지 바디에 입력하는 것이다. 그렇기에 위의 예시도 log를 찍어보면 컨버터가 적용되지 않는 모습을 볼 수 있다.
@PostMapping("/formatter/edit")
public String formatterEdit(@ModelAttribute Form form){
log.info("now: {}",form.getLocalDateTime());
return "formatter-view";
}
위와 같은 상황에서 log를 찍어보면 컨버터가 적용되고 나서의 모습이 아닌 적용 전의 모습이 나타난다. 아래와 같이.
7. 마무리
정말 간단한 개념이고 단순하지만 HttpMessageConverter와 관계가 없다는 것에 충격이었다. 방학 간에 진행하는 프로젝트가 API 통신의 역할만 하면 되는 것인데 날짜-시간 관련해서 변경될 사항들이 있었어서 간단하게 하기위해 포맷터를 사용해야지~ 라는 생각만 하고 있었지만,,, JSON으로 만들어질 결과에 포맷터를 적용하고 싶다면 해당 라이브러리를 까서 제공하는 설정을 보아야 한다. 찾아봐야겠다 흐규..////
'백앤드(스프링)' 카테고리의 다른 글
API 예외 처리 (0) | 2022.08.21 |
---|---|
예외 처리와 오류 페이지 (2) | 2022.08.10 |
서블릿 필터와 스프링 인터셉터 (0) | 2022.08.01 |
세션 방식의 로그인 구현 (0) | 2022.07.31 |
카카오 로그인 구현하기 (0) | 2022.07.31 |