스프링부트만 믿고 개발에 정진해왔다. 그러다가 시연해야할 떄가 되어 젠킨스를 통해서 개발서버에 빌드-배포된 war 파일을 실행하여 테스트하려고 하니 Thymeleaf template engine에서 template file 을 찾지못하는 문제가 발생했다.

이 문제가 왜 생기는지를 고민하고 검색해봐도 별다른 내용을 찾을 수가 없었다.

스프링부트 튜토리얼을 살펴보다가, 컨트롤러 영역에서 조금 다른 차이점을 찾아냈다.

@Controller
public class TestController {

    @RequestMapping("/test")
    public String test() {
        return "test";
    }
}

문제가 발생하는 화면의 컨트롤러에서는

@Controller
public class TestController {

    @RequestMapping("/test")
    public String test() {
        return "/test";
    }
}

와 같은 형태로 정의가 되어 있는 것이다.

차이를 발견했는가???

저 문제를 찾지 못해서 헤매였다. 1시간여를…. 흙… Orz…

해결방법

return "/test";return "test"; 으로 정의하면 된다.
bootRepackage로 빌드된 애플리케이션 배포본은 경로에 민감하게 반응하는 것으로 보인다.

저 ‘/‘ 때문에 ‘/test’ 에 대한 접근이 아니라 ‘//test’로 처리하기 때문에 나타나는 문제가 아닐까 추측해본다.

  1. 2017.06.01 21:21

    비밀댓글입니다

    • 바보...
      스프링 부트에 있는 설정을 찾아봐요... 라고 말해주고 싶네요.

      지금 버전(1.5.X.RELEASE 이후)은 모르겠네...

스프링부트 기본배너

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::   v1.3.0.BUILD-SNAPSHOT

스프링애플리케이션에서 배너와 관련된 설정

Class 정의

public static void main(String[] args) {
    SpringApplication app = new SpringApplication(MySpringConfiguration.class);
    app.setShowBanner(false);
    app.run(args);
}

외부설정 정의

spring.main.show_banner=false

배너 변경

SpringBoot가 실행될때 배너를 보여주게 되는데, 이를 변경하고 싶을 때는

  • banner.txt 생성
    • banner.txt 파일을 root classpath(src/main/resource)에 위치
    • 별도의 위치에 두고 외부설정(properties or yaml)으로 banner.location 을 선언하고 위치를 정의

배너변경 결과

11:26:18,781 |-INFO in ch.qos.logback.classic.gaffer.ConfigurationDelegate@1c4e4471 - About to instantiate appender of type [ch.qos.logback.core.ConsoleAppender]
11:26:18,783 |-INFO in ch.qos.logback.classic.gaffer.ConfigurationDelegate@1c4e4471 - Naming appender as [STDOUT]
11:26:18,921 |-WARN in ch.qos.logback.core.ConsoleAppender[STDOUT] - This appender no longer admits a layout as a sub-component, set an encoder instead.
11:26:18,921 |-WARN in ch.qos.logback.core.ConsoleAppender[STDOUT] - To ensure compatibility, wrapping your layout in LayoutWrappingEncoder.
11:26:18,921 |-WARN in ch.qos.logback.core.ConsoleAppender[STDOUT] - See also http://logback.qos.ch/codes.html#layoutInsteadOfEncoder for details
11:26:18,924 |-INFO in ch.qos.logback.classic.gaffer.ConfigurationDelegate@1c4e4471 - Setting level of logger [org.thymeleaf] to INFO
11:26:18,931 |-INFO in ch.qos.logback.classic.gaffer.ConfigurationDelegate@1c4e4471 - Setting level of logger [org.springframework.web] to DEBUG
11:26:18,931 |-INFO in ch.qos.logback.classic.gaffer.ConfigurationDelegate@1c4e4471 - Setting level of logger [org.springframework] to INFO
11:26:18,931 |-INFO in ch.qos.logback.classic.gaffer.ConfigurationDelegate@1c4e4471 - Setting level of logger [org.hibernate] to INFO
11:26:18,931 |-INFO in ch.qos.logback.classic.gaffer.ConfigurationDelegate@1c4e4471 - Setting level of logger [org.eclipse.jetty] to INFO
11:26:18,931 |-INFO in ch.qos.logback.classic.gaffer.ConfigurationDelegate@1c4e4471 - Setting level of logger [jndi] to INFO
11:26:18,934 |-INFO in ch.qos.logback.classic.gaffer.ConfigurationDelegate@1c4e4471 - Setting level of logger [ROOT] to DEBUG
11:26:18,937 |-INFO in ch.qos.logback.classic.gaffer.ConfigurationDelegate@1c4e4471 - Attaching appender named [STDOUT] to Logger[ROOT]

  _   _     U  ___ u  _   _   U _____ u __   __  __  __    U  ___ u  _   _     
 |'| |'|     \/"_ \/ | \ |"|  \| ___"|/ \ \ / /U|' \/ '|u   \/"_ \/ | \ |"|    
/| |_| |\    | | | |<|  \| |>  |  _|"    \ V / \| |\/| |/   | | | |<|  \| |>   
U|  _  |u.-,_| |_| |U| |\  |u  | |___   U_|"|_u | |  | |.-,_| |_| |U| |\  |u   
 |_| |_|  \_)-\___/  |_| \_|   |_____|    |_|   |_|  |_| \_)-\___/  |_| \_|    
 //   \\       \\    ||   \\,-.<<   >>.-,//|(_ <<,-,,-.       \\    ||   \\,-. 
(_") ("_)     (__)   (_")  (_/(__) (__)\_) (__) (./  \.)     (__)   (_")  (_/  


11:26:19 DEBUG o.s.w.c.s.StandardServletEnvironment - Adding [servletConfigInitParams] PropertySource with lowest search precedence
11:26:19 DEBUG o.s.w.c.s.StandardServletEnvironment - Adding [servletContextInitParams] PropertySource with lowest search precedence
11:26:19 DEBUG o.s.w.c.s.StandardServletEnvironment - Adding [jndiProperties] PropertySource with lowest search precedence
11:26:19 DEBUG o.s.w.c.s.StandardServletEnvironment - Adding [systemProperties] PropertySource with lowest search precedence
11:26:19 DEBUG o.s.w.c.s.StandardServletEnvironment - Adding [systemEnvironment] PropertySource with lowest search precedence

위와 같은 결과를 볼 수 있다.

참고사항

SpringBoot

이미지 to ASCII CODE image


SpringBoot 1.2.0.RELEASE 부터 추가된 @SpringBootApplication.

/**
 * Indicates a {@link Configuration configuration} class that declares one or more
 * {@link Bean @Bean} methods and also triggers {@link EnableAutoConfiguration
 * auto-configuration} and {@link ComponentScan component scanning}. This is a convenience
 * annotation that is equivalent to declaring {@code @Configuration},
 * {@code @EnableAutoConfiguration} and {@code @ComponentScan}.
 *
 * @author Phillip Webb
 * @since 1.2.0
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Configuration
@EnableAutoConfiguration
@ComponentScan
public @interface SpringBootApplication {

}

1.1.9.RELEASE 까지는 Application Endpoint에 클래스에

@Configuration
@EnableAutoConfiguration
@ComponentScan
public class Application{
//...
}

3개 애노테이션을 사용해야했던 것을 하나로 줄여주었다.


1. 문제발생

스프링부트 1.1.9.RELEASE 버전을 사용할 때, 정상적으로 동작하던 파일업로드 기능이 스프링부트 1.2.1로 업그레이드하면서 동작하지 않는 문제가 발생했다. 다음과 같은 형태로 multipartConfigElement와 multipartResolver 빈을 설정해두었고, ajax 파일업로드를 처리하기 위한 목적으로 jQuery Form plugin(https://github.com/malsup/form)을 사용하고 있다.

@Bean
MultipartConfigElement MultipartConfigElement() {
    MultipartConfigFactory factory = new MultipartConfigFactory();

    factory.setMaxFileSize(getMaxUploadFileSize());
    factory.setMaxRequestSize(getMaxUploadFileSize());

    return factory.createMultipartConfig();
}

@Bean
public CommonsMultipartResolver multipartResolver() {
    return new CommonsMultipartResolver();
}

컨트롤러는 다음과 같이 메서드를 구현했다.

@RequestMapping(value = "/file-upload", method = RequestMethod.POST)
    public ModelAndView fileUpload(@RequestParam(value = "file") MultipartFile file) throws IOException {
}

다음과 같은 예외가 발생한다.

org.springframework.web.bind.MissingServletRequestParameterException: Required MultipartFile parameter 'file' is not present
    at org.springframework.web.method.annotation.RequestParamMethodArgumentResolver.handleMissingValue(RequestParamMethodArgumentResolver.java:253) ~[spring-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver.resolveArgument(AbstractNamedValueMethodArgumentResolver.java:94) ~[spring-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:77) ~[spring-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:162) ~[spring-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:129) ~[spring-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:110) ~[spring-webmvc-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandleMethod(RequestMappingHandlerAdapter.java:777) ~[spring-webmvc-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:706) ~[spring-webmvc-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85) ~[spring-webmvc-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:943) [spring-webmvc-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:877) [spring-webmvc-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:966) [spring-webmvc-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:868) [spring-webmvc-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:707) [javax.servlet-api-3.1.0.jar:3.1.0]
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:842) [spring-webmvc-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:790) [javax.servlet-api-3.1.0.jar:3.1.0]
    at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:800) [jetty-servlet-9.2.4.v20141103.jar:9.2.4.v20141103]
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1669) [jetty-servlet-9.2.4.v20141103.jar:9.2.4.v20141103]
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:88) [spring-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) [spring-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1652) [jetty-servlet-9.2.4.v20141103.jar:9.2.4.v20141103]
    at org.springframework.boot.actuate.autoconfigure.EndpointWebMvcAutoConfiguration$ApplicationContextHeaderFilter.doFilterInternal(EndpointWebMvcAutoConfiguration.java:288) [spring-boot-actuator-1.2.0.RELEASE.jar:1.2.0.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) [spring-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1652) [jetty-servlet-9.2.4.v20141103.jar:9.2.4.v20141103]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330) [spring-security-web-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:118) [spring-security-web-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:84) [spring-security-web-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342) [spring-security-web-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:113) [spring-security-web-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342) [spring-security-web-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:103) [spring-security-web-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342) [spring-security-web-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:113) [spring-security-web-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342) [spring-security-web-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:154) [spring-security-web-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342) [spring-security-web-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:45) [spring-security-web-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342) [spring-security-web-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at org.springframework.security.web.authentication.www.BasicAuthenticationFilter.doFilter(BasicAuthenticationFilter.java:150) [spring-security-web-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342) [spring-security-web-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:199) [spring-security-web-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342) [spring-security-web-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:110) [spring-security-web-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342) [spring-security-web-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:57) [spring-security-web-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) [spring-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342) [spring-security-web-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:87) [spring-security-web-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342) [spring-security-web-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:50) [spring-security-web-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) [spring-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342) [spring-security-web-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:192) [spring-security-web-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:160) [spring-security-web-3.2.5.RELEASE.jar:3.2.5.RELEASE]
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1652) [jetty-servlet-9.2.4.v20141103.jar:9.2.4.v20141103]
    at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:77) [spring-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) [spring-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1652) [jetty-servlet-9.2.4.v20141103.jar:9.2.4.v20141103]
    at org.springframework.boot.actuate.trace.WebRequestTraceFilter.doFilterInternal(WebRequestTraceFilter.java:100) [spring-boot-actuator-1.2.0.RELEASE.jar:1.2.0.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) [spring-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1652) [jetty-servlet-9.2.4.v20141103.jar:9.2.4.v20141103]
    at org.springframework.boot.actuate.autoconfigure.MetricFilterAutoConfiguration$MetricsFilter.doFilterInternal(MetricFilterAutoConfiguration.java:90) [spring-boot-actuator-1.2.0.RELEASE.jar:1.2.0.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) [spring-web-4.1.3.RELEASE.jar:4.1.3.RELEASE]
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1652) [jetty-servlet-9.2.4.v20141103.jar:9.2.4.v20141103]
    at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:585) [jetty-servlet-9.2.4.v20141103.jar:9.2.4.v20141103]
    at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:143) [jetty-server-9.2.4.v20141103.jar:9.2.4.v20141103]
    at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:577) [jetty-security-9.2.4.v20141103.jar:9.2.4.v20141103]
    at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:223) [jetty-server-9.2.4.v20141103.jar:9.2.4.v20141103]
    at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1125) [jetty-server-9.2.4.v20141103.jar:9.2.4.v20141103]
    at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:515) [jetty-servlet-9.2.4.v20141103.jar:9.2.4.v20141103]
    at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:185) [jetty-server-9.2.4.v20141103.jar:9.2.4.v20141103]
    at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1059) [jetty-server-9.2.4.v20141103.jar:9.2.4.v20141103]
    at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141) [jetty-server-9.2.4.v20141103.jar:9.2.4.v20141103]
    at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:97) [jetty-server-9.2.4.v20141103.jar:9.2.4.v20141103]
    at org.eclipse.jetty.server.Server.handle(Server.java:497) [jetty-server-9.2.4.v20141103.jar:9.2.4.v20141103]
    at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:310) [jetty-server-9.2.4.v20141103.jar:9.2.4.v20141103]
    at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:248) [jetty-server-9.2.4.v20141103.jar:9.2.4.v20141103]
    at org.eclipse.jetty.io.AbstractConnection$2.run(AbstractConnection.java:540) [jetty-io-9.2.4.v20141103.jar:9.2.4.v20141103]
    at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:620) [jetty-util-9.2.4.v20141103.jar:9.2.4.v20141103]
    at org.eclipse.jetty.util.thread.QueuedThreadPool$3.run(QueuedThreadPool.java:540) [jetty-util-9.2.4.v20141103.jar:9.2.4.v20141103]
    at java.lang.Thread.run(Thread.java:745) [na:1.7.0_67]

컨트롤러에서 파라메터로 지정한 @RequestParam(value = "file") MultipartFile file 받지 못하는 증상이 생긴 것이다. 이걸 보고 계속 헛다리를 짚은 것이 jetty 9.x 를 사용해서 그런 것인가 하는 말도 안되는 추측을 하면서 문제의 해결방법에서 멀어지는 상황에 놓이게 된다.

ㅡ_-);; 이때 조금만 더 곰곰히 생각해봤으면 어땠을까 하지만, 정작 문제가 발생했을 때는 ‘이런 상황이 왜 생기는거야?’라는 해결책을 찾는데에만 함몰되어 있어다.

2. 문제해결방법

난 이 문제가 Jetty에 문제이거나 스프링부트 1.2.0 의 문제라고 생각했다. 그래서 얼마전 출시된 1.2.1 로 업그레이드를 했는데도 동일한 문제가 발생하는 것을 접하면서 답답함을 금치못했다. 그런데, 딱히 이와 관련된 버그나 이슈는 없었다. 왜?
그래서 인터넷 검색을 들어간다.

이럴 떄는 발생한 예외로그를 그대로 복사해서 놓는 것도 하나의 방법이라면 방법이다.
세계 어디에서나 같은 개발환경을 기반으로 개발하는 개발자들이 겪는 오류는 같다. ㅎㅎ

그러다가 스택오버플로우에서 내가 겪고 있는 상황과 같은 문제를 겪고 있는 어느 개발자의 질문을 발견한다.
Spring mvc: HTTP Status 400 - Required MultipartFile parameter ‘file’ is not present - Stackoverflow

그 질문에 달린 답변은 간단했다.

<bean id="multipartResolver" class="**org.springframework.web.multipart.commons.CommonsMultipartResolver**" />

<bean id="multipartResolver" class="org.springframework.web.multipart.support.StandardServletMultipartResolver" />

으로 변경하면 된다고 한다. 그래서 그렇게 했다.

@Bean
public MultipartResolver multipartResolver() {
    return new StandardServletMultipartResolver();
}

그랬더니 된다. 됐다. 문제해결!

3. 문제원인

스프링부트 1.2.0.RELEASE부터 서블릿 3.1이 적용되었다.

  • Spring Boot 1.2.0 released

    Spring Boot now uses Servlet 3.1 when running with an embedded servlet container. Tomcat 8, Jetty 9 and Undertow 1.1 are all supported options. In addition, WebSocket support has been improved and is now automatically configured for all supported servers. If you need to stick to Servlet 3.0, Tomcat 7 and Jetty 8 are still supported.

서블릿 3.1….
이 부분이 중요하다. +_+)
문제해결하는데 사용된 org.springframework.web.multipart.support.StandardServletMultipartResolver을 열어보면 다음과 같은 주석이 있다.

/**
 * Standard implementation of the {@link MultipartResolver} interface,
 * based on the Servlet 3.0 {@link javax.servlet.http.Part} API.
 * To be added as "multipartResolver" bean to a Spring DispatcherServlet context,
 * without any extra configuration at the bean level (see below).
 *
 * <p><b>Note:</b> In order to use Servlet 3.0 based multipart parsing,
 * you need to mark the affected servlet with a "multipart-config" section in
 * {@code web.xml}, or with a {@link javax.servlet.MultipartConfigElement}
 * in programmatic servlet registration, or (in case of a custom servlet class)
 * possibly with a {@link javax.servlet.annotation.MultipartConfig} annotation
 * on your servlet class. Configuration settings such as maximum sizes or
 * storage locations need to be applied at that servlet registration level;
 * Servlet 3.0 does not allow for them to be set at the MultipartResolver level.
 *
 * @author Juergen Hoeller
 * @since 3.1
 */

정리하면,

  • Servlet 3.0 Part API를 바탕으로 한 MultipartResolver 인터페이스 구현체
  • bean 을 선언할 때 별다른 확장설정을 하지 않고 “multipartResolver”로 선언
  • multipart와 관련된 업로드 파일 최대크기, 저장위치 등 설정을 하고 싶다면
    • web.xml에서 하거나
    • Servlet 등록하는 과정에서 MultipartConfigElement을 선언하면서 설정
    • @MultipartConfig라는 애노테이션 사용
  • Servlet 3.0 에서는 MultipartResolver 레벨에서 설정하는 것을 허용치 않는다.

이다. 결국은

@Bean
public MultipartConfigElement multipartConfigElement() {
    MultipartConfigFactory factory = new MultipartConfigFactory();

    factory.setMaxFileSize(getMaxUploadFileSize());
    factory.setMaxRequestSize(getMaxUploadFileSize());

    return factory.createMultipartConfig();
}

@Bean
public MultipartResolver multipartResolver() {
    return new StandardServletMultipartResolver();
}

의 형태로 정의하면 된다는 것이다. ㅇㅋ~


  1. 2blikecaesar 2016.04.11 15:56 신고

    너무 감사합니다!
    덕분에 해결했네요^^

  2. lebe 2016.07.06 14:53 신고

    정보 감사합돠~~

  3. 초보 개발자 2017.08.22 23:00 신고

    와 정말 감사드립니다. 해당 버그는 찾기도 어렵고... 한시간 정도 해맸는데 감사드립니다.
    좋은 하루 되세요



<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>myproject</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.0.BUILD-SNAPSHOT</version>
    </parent>

    <!-- Additional lines to be added here... -->
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

    <!-- (you don't need this if you are using a .RELEASE version) -->
    <repositories>
        <repository>
            <id>spring-snapshots</id>
            <url>http://repo.spring.io/snapshot</url>
            <snapshots><enabled>true</enabled></snapshots>
        </repository>
        <repository>
            <id>spring-milestones</id>
            <url>http://repo.spring.io/milestone</url>
        </repository>
    </repositories>
    <pluginRepositories>
        <pluginRepository>
            <id>spring-snapshots</id>
            <url>http://repo.spring.io/snapshot</url>
        </pluginRepository>
        <pluginRepository>
            <id>spring-milestones</id>
            <url>http://repo.spring.io/milestone</url>
        </pluginRepository>
    </pluginRepositories>
</project>

위의 pom.xml을 만들고나서

$ mvn dependency:tree

를 실행하면,

honeymon@test $ mvn dependency:tree
[INFO] Scanning for projects...
[INFO]                                                                         
[INFO] ------------------------------------------------------------------------
[INFO] Building myproject 0.0.1-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO] 
[INFO] --- maven-dependency-plugin:2.9:tree (default-cli) @ myproject ---
[INFO] com.example:myproject:jar:0.0.1-SNAPSHOT
[INFO] \- org.springframework.boot:spring-boot-starter-web:jar:1.2.0.BUILD-SNAPSHOT:compile
[INFO]    +- org.springframework.boot:spring-boot-starter:jar:1.2.0.BUILD-SNAPSHOT:compile
[INFO]    |  +- org.springframework.boot:spring-boot:jar:1.2.0.BUILD-SNAPSHOT:compile
[INFO]    |  +- org.springframework.boot:spring-boot-autoconfigure:jar:1.2.0.BUILD-SNAPSHOT:compile
[INFO]    |  +- org.springframework.boot:spring-boot-starter-logging:jar:1.2.0.BUILD-SNAPSHOT:compile
[INFO]    |  |  +- org.slf4j:jcl-over-slf4j:jar:1.7.7:compile
[INFO]    |  |  |  \- org.slf4j:slf4j-api:jar:1.7.7:compile
[INFO]    |  |  +- org.slf4j:jul-to-slf4j:jar:1.7.7:compile
[INFO]    |  |  +- org.slf4j:log4j-over-slf4j:jar:1.7.7:compile
[INFO]    |  |  \- ch.qos.logback:logback-classic:jar:1.1.2:compile
[INFO]    |  |     \- ch.qos.logback:logback-core:jar:1.1.2:compile
[INFO]    |  \- org.yaml:snakeyaml:jar:1.14:runtime
[INFO]    +- org.springframework.boot:spring-boot-starter-tomcat:jar:1.2.0.BUILD-SNAPSHOT:compile
[INFO]    |  +- org.apache.tomcat.embed:tomcat-embed-core:jar:8.0.15:compile
[INFO]    |  +- org.apache.tomcat.embed:tomcat-embed-el:jar:8.0.15:compile
[INFO]    |  +- org.apache.tomcat.embed:tomcat-embed-logging-juli:jar:8.0.15:compile
[INFO]    |  \- org.apache.tomcat.embed:tomcat-embed-websocket:jar:8.0.15:compile
[INFO]    +- com.fasterxml.jackson.core:jackson-databind:jar:2.4.4:compile
[INFO]    |  +- com.fasterxml.jackson.core:jackson-annotations:jar:2.4.4:compile
[INFO]    |  \- com.fasterxml.jackson.core:jackson-core:jar:2.4.4:compile
[INFO]    +- org.hibernate:hibernate-validator:jar:5.1.3.Final:compile
[INFO]    |  +- javax.validation:validation-api:jar:1.1.0.Final:compile
[INFO]    |  +- org.jboss.logging:jboss-logging:jar:3.1.3.GA:compile
[INFO]    |  \- com.fasterxml:classmate:jar:1.0.0:compile
[INFO]    +- org.springframework:spring-core:jar:4.1.3.RELEASE:compile
[INFO]    +- org.springframework:spring-web:jar:4.1.3.RELEASE:compile
[INFO]    |  +- org.springframework:spring-aop:jar:4.1.3.RELEASE:compile
[INFO]    |  |  \- aopalliance:aopalliance:jar:1.0:compile
[INFO]    |  +- org.springframework:spring-beans:jar:4.1.3.RELEASE:compile
[INFO]    |  \- org.springframework:spring-context:jar:4.1.3.RELEASE:compile
[INFO]    \- org.springframework:spring-webmvc:jar:4.1.3.RELEASE:compile
[INFO]       \- org.springframework:spring-expression:jar:4.1.3.RELEASE:compile
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 3.397s
[INFO] Finished at: Mon Jan 05 23:11:55 KST 2015
[INFO] Final Memory: 12M/152M
[INFO] ------------------------------------------------------------------------
honeymon@test $

의 형태로 추가된 의존성을 확인할 수 있다. WEB MVC를 사용하여 웹 애플리케이션을 만드는데 필요한 스프링부트의 기본적인 요소들을 살펴볼 수 있다. 스프링부트 web 에서는 내장형 컨테이너로 톰캣을 기본탑재하고 있다.

요런식으로 스프링부트에 추가되는 'Starter POMs'들이 가지고 있는 의존성을 엿볼 수 있다.

+ Recent posts