본문 바로가기

Advance I/Spring MVC

22.09.15

포맷터 - Formatter

Converter 는 입력과 출력 타입에 제한이 없는, 범용 타입 변환 기능을 제공한다.

일반적인 웹 애플리케이션 환경에서는 불린 타입을 숫자로 바꾸는 것 같은 범용 기능 보다는 문자를 다른 타입으로 변환하거나, 다른 타입을 문자로 변환하는 상황이 대부분이다.

웹(문자) <-> 서버(객체)

 

웹 애플리케이션에서 객체를 문자로, 문자를 객체로 변환하는 예

  • 1000 단위 쉼표: 숫자 1000 -> 문자 "1,000", 또는 그 반대의 상황
  • 날짜 객체 -> 문자 "2021-01-01 10:50:11" 또는 그 반대의 상황 (추가로 날짜 숫자의 표현 방법은 Locale 현지화 정보에 따라 결정)

이렇게 객체를 특정한 포멧에 맞추어 문자로 출력하거나 또는 그 반대의 역할을 하는 것에 특화된 기능이 바로 포맷터( Formatter )이다. 포맷터는 컨버터의 특별한 버전으로 이해하면 된다.

 

Converter vs Formatter

Converter 는 범용(객체 객체)

Formatter 는 문자에 특화(객체 문자, 문자 객체) + 현지화(Locale)

Formatter 인터페이스

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

Formatter 는 아래 두 인터페이스를 상속한다.

String print(T object, Locale locale) : 객체 -> 문자

T parse(String text, Locale locale) : 문자 -> 객체

 

1000 단위 쉼표 formatter

@Slf4j
public class MyNumberFormatter implements Formatter<Number> {
    @Override
    public Number parse(String text, Locale locale) throws ParseException {
        log.info("text={}, locale={}", text, locale);
        NumberFormat numberFormat = NumberFormat.getInstance(locale);
        return numberFormat.parse(text);
    }

    @Override
    public String print(Number object, Locale locale) {
        log.info("object={}, locale={}", object, locale);
        NumberFormat numberFormat = NumberFormat.getInstance();
        return numberFormat.format(object);
    }
}

"1,000" 처럼 숫자 중간의 쉼표를 적용하려면 자바가 기본으로 제공하는 NumberFormat 객체를 사용하면 된다. 이 객체는 Locale 정보를 활용해서 나라별로 다른 숫자 포맷을 만들어준다.

참고로 Number 타입은 Integer , Long 과 같은 숫자 타입의 부모 클래스이다.

 

참고: 스프링은 용도에 따라 다양한 방식의 포맷터를 제공한다. (org.springframework.format)

Formatter: 단순한 포맷터

AnnotationFormatterFactory: 필드의 타입이나 애노테이션 정보를 활용할 수 있는 포맷터

포맷터를 지원하는 컨버전 서비스

컨버전 서비스에는 컨버터만 등록할 수 있고, 포맷터를 등록할 수는 없다. 그런데 포맷터는 객체->문자, 문자->객체로 변환하는 데 특화된 컨버터일 뿐이다.

포맷터를 지원하는 컨버전 서비스를 사용하면 컨버전 서비스에 포맷터를 추가할 수 있다. 내부에서 어댑터 패턴을 사용해서 Formatter 가 Converter 처럼 동작하도록 지원한다.

 

  • FormattingConversionService: 포맷터를 지원하는 컨버전 서비스
  • DefaultFormattingConversionService: FormattingConversionService 에 기본적인 통화, 숫자 관련 몇가지 기본 포맷터를 추가하여 제공

DefaultFormattingConversionService 상속 관계

FormattingConversionService 는 ConversionService 관련 기능을 상속받기 때문에 컨버터도 포맷터도 모두 등록할 수 있다. ConversionService 가 제공하는 컨버터나 포맷터는 동일하게 convert() 를 통해 변환한다.

추가로 스프링 부트는 DefaultFormattingConversionService 를 상속 받은 WebConversionService 를 내부에서 사용한다. 즉, WebMvcConfigurer 에서 등록한 포맷터들은 실제로 WebConversionService 에 등록된다.

포맷터 적용하기

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        //우선순위 때문에 주석처리
        //registry.addConverter(new StringToIntegerConverter());
        //registry.addConverter(new IntegerToStringConverter());
        registry.addConverter(new StringToIpPortConverter());
        registry.addConverter(new IpPortToStringConverter());

        registry.addFormatter(new MyNumberFormatter());
    }
}

참고: 컨버전 서비스에 등록된 컨버터와 포맷터가 적용되는 우선순위는 컨버터가 포맷터보다 높다.

따라서 문자를 다른 타입으로 변환하는 컨버터가 이미 등록되어 있을 경우 포맷터보다 우선 적용된다.

스프링이 제공하는 기본 포맷터

스프링은 자바에서 기본으로 제공하는 타입들에 대해 수 많은 포맷터를 기본으로 제공한다.

IDE에서 Formatter 인터페이스의 구현 클래스를 찾아보면 수많은 날짜나 시간 관련 포맷터가 제공되는 것을 확인할 수 있다. 그런데 포맷터는 기본 형식이 지정되어 있기 때문에, 객체의 각 필드마다 다른 형식으로 포맷을 지정하기는 어렵다.

예를 들면, 같은 타입으로 변환하되 다른 형식으로의 포맷

 

스프링은 이런 문제를 해결하기 위해 애노테이션 기반으로 원하는 형식을 지정해서 사용할 수 있는 포맷터 두 가지를 기본으로 제공한다.

  • @NumberFormat : 숫자 관련 형식 지정 포맷터 사용, NumberFormatAnnotationFormatterFactory
  • @DateTimeFormat : 날짜 관련 형식 지정 포맷터 사용, Jsr310DateTimeFormatAnnotationFormatterFactory
@GetMapping("/formatter/edit")
public String formatterForm(Model model){
    Form form = new Form();
    form.setNumber(10000);
    form.setLocalDateTime(LocalDateTime.now());
    model.addAttribute("form", form);
    return "formatter-form";
}

@PostMapping("/formatter/edit")
public String formatterEdit(@ModelAttribute Form form){
    return "formatter-view";
}

@Data
static class Form {

    @NumberFormat(pattern = "###,###")
    private Integer number;

    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime localDateTime;
}

GET /formatter/edit : Form 객체에 데이터를 채워서 뷰 템플릿의 폼에 전달

  th:field="*{number}" 와 th:field="*{localDateTime}" 에서 애노테이션으로 지정한 포맷터를 통해 각 타입에서 문자로 변환

POST /formatter/edit : @ModelAttribute 를 통해 파라미터가 생성될 때 애노테이션으로 지정한 포맷터를 통해 문자에서 각 타입으로 변환

 

정리

컨버터를 사용하든, 포맷터를 사용하든 등록 방법은 다르지만, 사용할 때는 컨버전 서비스를 통해서 일관성 있게 사용할 수 있다.

 

주의! 메시지 컨버터( HttpMessageConverter )에는 컨버전 서비스가 적용되지 않는다.

특히 객체를 JSON으로 변환할 때 메시지 컨버터를 사용하면서 이 부분을 많이 오해하는데, HttpMessageConverter 의 역할은 HTTP 메시지 바디의 내용을 객체로 변환하거나 객체를 HTTP 메시지 바디에 입력하는 것이다.

예를 들어서 JSON을 객체로 변환하는 메시지 컨버터는 내부에서 Jackson 같은 라이브러리를 사용한다. 객체를 JSON으로 변환한다면 그 결과는 이 라이브러리에 달린 것이다. 따라서 JSON 결과로 만들어지는 숫자나 날짜 포맷을 변경하고 싶으면 해당 라이브러리가 제공하는 설정을 통해서 포맷을 지정해야 한다. (Jackson Library Converter)

결과적으로 이것은 컨버전 서비스와 전혀 관계가 없다. 컨버전 서비스는 @RequestParam , @ModelAttribute , @PathVariable , 뷰 템플릿 등에서 사용할 수 있다.

파일 업로드

HTML 폼 전송 방식

  • application/x-www-form-urlencoded
  • multipart/form-data

application/x-www-form-urlencoded 방식

 

HTML 폼 데이터를 서버로 전송하는 가장 기본적인 방법

Form 태그에 별도의 enctype 옵션이 없으면 웹 브라우저는 요청 HTTP 메시지의 헤더에 다음 내용을 추가한다.

Content-Type: application/x-www-form-urlencoded

그리고 폼에 입력한 전송할 항목을 HTTP Body에 문자로 username=kim&age=20 와 같이 & 로 구분해서 전송한다.

 

파일을 업로드 하려면 문자가 아니라 바이너리 데이터를 전송해야 한다. 문자를 전송하는 이 방식으로 파일을 전송하기는 어렵다. 그리고 보통 폼을 전송할 때 파일만 전송하지 않는다. 문자와 파일(바이너리) 을 동시에 전송해야 한다.

 

이 문제를 해결하기 위해 HTTP는 multipart/form-data 라는 전송 방식을 제공한다.

multipart/form-data 방식

 

이 방식을 사용하려면 Form 태그에 별도의 enctype="multipart/form-data" 를 지정해야 한다.

multipart/form-data 방식은 다른 종류의 여러 파일과 폼의 내용 함께 전송할 수 있다.

 

폼의 입력 결과로 생성된 HTTP 메시지를 보면 각각의 전송 항목이 구분이 되어있다.

Content-Disposition 이라는 항목별 헤더가 추가되어 있고 여기에 부가 정보가 있다. 예제에서는 username , age , file1 이 각각 분리되어 있고, 폼의 일반 데이터는 각 항목별로 문자가 전송되고, 파일의 경우 파일 이름과 Content-Type이 추가되고 바이너리 데이터가 전송된다.

 

Part

multipart/form-data 는 application/x-www-form-urlencoded 와 비교해서 매우 복잡하고 각각의 부분( Part )로 나누어져 있다. 이렇게 복잡한 HTTP 메시지를 서버에서 어떻게 사용할 수 있을까?

서블릿과 파일 업로드1

@GetMapping("/upload")
public String newFile(){
    return "upload-form";
}

@PostMapping("/upload")
public String saveFileV1(HttpServletRequest request) throws ServletException, IOException {
    log.info("request={}", request);

    String itemName = request.getParameter("itemName");
    log.info("itemName={}", itemName);

    Collection<Part> parts = request.getParts();
    log.info("parts={}", parts);

    return "upload-form";
}
<form th:action method="post" enctype="multipart/form-data">
    <ul>
        <li>상품명 <input type="text" name="itemName"></li>
        <li>파일<input type="file" name="file" ></li>
    </ul>
    <input type="submit"/>
</form>

 

== 실행 로그 ==

request=org.springframework.web.multipart.support.StandardMultipartHttpServletRequest@4f9bd3f0
itemName=상품A
parts=[org.apache.catalina.core.ApplicationPart@24a09e82, org.apache.catalina.core.ApplicationPart@7615be39]

 

참고: logging.level.org.apache.coyote.http11=debug 옵션을 추가하면 HTTP 메세지를 확인할 수 있다.

 

멀티파트 사용 옵션

업로드 사이즈 제한

spring.servlet.multipart.max-file-size=1MB

spring.servlet.multipart.max-request-size=10MB

 

큰 파일을 무제한 업로드하게 둘 수는 없으므로 업로드 사이즈를 제한할 수 있다.

사이즈를 넘으면 예외( SizeLimitExceededException )가 발생한다.

max-file-size : 파일 하나의 최대 사이즈, 기본 1MB

max-request-size : 멀티파트 요청 하나에 여러 파일을 업로드 할 수 있는데, 그 전체 합이다. 기본 10MB

 

spring.servlet.multipart.enabled

spring.servlet.multipart.enabled=false (기본 true)

 

== 실행 로그 ==

request=org.apache.catalina.connector.RequestFacade@xxx

itemName=null

parts=[]

 

해당 옵션을 끄면 서블릿 컨테이너는 멀티파트와 관련된 처리를 하지 않는다. 

반대로, 해당 옵션을 켜면 스프링 부트는 서블릿 컨테이너에게 멀티파트 데이터를 처리하라고 설정한다.

멀티파트는 일반적인 폼 요청인 application/x-www-form-urlencoded 보다 훨씬 복잡하다.

 

*HttpServletRequest 객체*

옵션 false: RequestFacade

옵션 true: StandardMultipartHttpServletRequest

 

참고: spring.servlet.multipart.enabled 옵션을 켜면 스프링의 DispatcherServlet 에서 멀티파트 리졸버( MultipartResolver )를 실행한다.

멀티파트 리졸버는 멀티파트 요청인 경우 서블릿 컨테이너가 전달하는 일반적인 HttpServletRequest 를 MultipartHttpServletRequest 로 변환해서 반환한다.

MultipartHttpServletRequest 는 HttpServletRequest 의 자식 인터페이스이고, 멀티파트와 관련된 추가 기능을 제공한다.

 

스프링이 제공하는 기본 멀티파트 리졸버는 MultipartHttpServletRequest 인터페이스를 구현한 StandardMultipartHttpServletRequest 를 반환한다.

이제 컨트롤러에서 HttpServletRequest 대신에 MultipartHttpServletRequest 를 주입받을 수 있는데, 이것을 사용하면 멀티파트와 관련된 여러가지 처리를 편리하게 할 수 있다. 그런데 이후 강의에서 설명할 MultipartFile 이라는 것을 사용하는 것이 더 편하기 때문에 MultipartHttpServletRequest 를 직접적으로 잘 사용하지는 않는다. 더 자세한 내용은 MultipartResolver 를 검색해보자.

 

 

'Advance I > Spring MVC' 카테고리의 다른 글

22.09.16  (0) 2022.09.16
22.09.14  (0) 2022.09.14
22.09.13  (0) 2022.09.13
22.09.12  (0) 2022.09.12
22.09.09  (0) 2022.09.09