티스토리 뷰
이번에는 @EnableWebMvc 어노테이션과 WebMvcConfigurer 인터페이스에 대해 알아보도록 하자.
1. Spring에서 제공하는 설정의 자동화와 변경
[ @Enable~ 을 이용한 설정 자동화 ]
Spring 기반의 프로젝트를 구축하려고 하면 우리는 메세지를 변환하는 메세지 컨버터나 뷰를 렌더링 하기 위한 뷰 리졸버 등을 일일이 설정해주어야 했다. 하지만 매번 프로젝트를 생성할 때마다 동일한 설정들을 하는 것은 개발자들에게 비용의 낭비였고, 그래서 스프링은 이러한 부분들에 대해 최신 전략들을 기반으로 설정을 자동화하는 기능을 제공하기 시작하였는데, 그것이 바로 @Enable~ 로 시작하는 어노테이션이다.
@Enable로 시작하는 애노테이션을 @Configuration이 붙은 설정 클래스에 붙임으로써 이와 관련된 기능들을 편리하게 제공하고 있다. 그 중에서 @EnableWebMvc가 대표적인데, 이를 붙이면 스프링이 제공하는 웹과 관련된 최신 전략 빈들이 등록된다.
@Configuration
@EnableWebMvc
public class WebMvcConfig {
}
하지만 그럼에도 불구하고 여러 개의 @Enable 어노테이션을 붙여주는 것도 상당히 번거롭기 때문에, Spring에서는 이러한 설정들을 자동화해주는 SpringBoot라는 프로젝트를 만들었다.
[ SpringBoot의 AutoConfiguration(자동 설정) ]
Spring에서 SpringBoot라는 프레임워크를 내놓기 시작하면서 SpringBoot의 AutoConfigure(자동 구성) 기능을 통해 많은 설정들이 자동화되기 시작하였다.
SpringBoot 애플리케이션을 생성하면 main 클래스에 @SpringBootApplication 어노테이션이 자동으로 붙어있는데, 이 어노테이션은 내부에 @EnableAutoConfiguration이라는 어노테이션을 갖고 있다.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
...
}
@EnableAutoConfiguration은 내부적으로 @EnableWebMvc과 동일한 기능을 사용하기 때문에 우리는 메세지 컨버터(Message Converter)나 뷰 리졸버(View Resolver) 또는 인터셉터(Interceptor) 등을 따로 설정해주지 않아도 되고, 추가로 @EnableWebMvc 기반의 설정도 추가하지 않아도 된다.
즉, SpringBoot 프로젝트를 생성만 하면 이러한 부분을 자동으로 이용할 수 있게 된 것이다.
[ ~Configurer 인터페이스을 통한 설정의 변경 ]
하지만 우리는 스프링이 제공해주는 자동 설정들 외에 추가의 설정이 필요할 수 있다. 그래서 스프링에서는 @Enable로 적용되는 인프라 빈에 대해 추가적인 설정을 할 수 있도록 ~Configurer로 끝나는 인터페이스(빈 설정자)를 제공하고 있다. 이를 구현한 클래스를 만들어 빈으로 등록하면 @Enable 전용 어노테이션을 처리하는 단계에서 설정용 빈을 활용해 인프라 빈의 설정을 마무리한다.
대표적으로 @EnableWebMvc의 빈 설정자는 WebMvcConfigurer이며, 이를 구현한 클래스를 만들고 @Configuration을 붙붙여 빈으로 등록해주면 된다.
2. @EnableWebMvc와 WebMvcConfigurer
[ WebMvcConfigurer 인터페이스 ]
@EnableWebMvc를 통해 자동 설정되는 빈들의 설정자는 WebMvcConfigurer이며, 이를 구현한 클래스를 만들어야 한다. WebMvcConfigurer는 다음과 같은 구조를 가지고 있으며, 메소드의 이름은 대부분 add나 configure로 시작하는데, 각각은 다음의 의미를 갖고 있다.
- add~: 기본 설정이 없는 빈들에 대하여 새로운 빈을 추가함
- configure~: 수정자를 통해 기존의 설정을 대신하여 등록함
- extend~: 기존의 설정을 이용하며 추가로 설정을 확장함
public interface WebMvcConfigurer {
default void configurePathMatch(PathMatchConfigurer configurer) {
}
default void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
}
default void configureAsyncSupport(AsyncSupportConfigurer configurer) {
}
default void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
}
default void addFormatters(FormatterRegistry registry) {
}
default void addInterceptors(InterceptorRegistry registry) {
}
default void addResourceHandlers(ResourceHandlerRegistry registry) {
}
default void addCorsMappings(CorsRegistry registry) {
}
default void addViewControllers(ViewControllerRegistry registry) {
}
default void configureViewResolvers(ViewResolverRegistry registry) {
}
default void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
}
default void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) {
}
default void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
}
default void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
}
default void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
}
default void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
}
@Nullable
default Validator getValidator() {
return null;
}
@Nullable
default MessageCodesResolver getMessageCodesResolver() {
return null;
}
}
예를 들어 인터셉터와 같은 것들은 기본적으로 등록되어 있지 않기 때문에 addInterceptors라는 메소드가 제공되고 있고, 메세지 컨버터와 같은 것들은 기본적으로 제공되는 것이 있기 때문에 configureMessageConverters와 extendMessageConverters 메소드가 있는 것이다.
Spring의 Configurer 인터페이스는 상당히 직관적인 이름을 제공하기 때문에 각각의 메소드의 의미를 쉽게 파악할 수 있다.
[ WebMvcConfigurer를 통한 설정의 변경 예시 ]
예를 들어 스프링이 기본적으로 제공해주는 Json 기반의 메세지 컨버터 구성에 더해 XML 기반의 메세지 컨버터가 필요한 상황이라고 하자.
그러면 우선 우리는 기존의 메세지 컨버터를 확장해야 하는 상황이므로 extendMessageConverters를 오버라이딩하면 된다. 이 메소드의 이름이 extend로 시작하는 이유는 새로운 메세지 컨버터를 추가하여도 기존의 메세지 컨버터도 모두 등록이 되기 때문이다. 만약 기존의 메세지 컨버터를 모두 비활성화하고 새로운 메세지 컨버터만 추가하고 싶다면 configureMessageConverters를 이용하면 된다.
우리는 XML 기반의 메세지 컨버터를 사용하기 위해 다음과 같이 설정 클래스를 작성할 수 있다.
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
messageConverters.add(createXmlHttpMessageConverter());
}
private HttpMessageConverter<Object> createXmlHttpMessageConverter() {
MarshallingHttpMessageConverter xmlConverter = new MarshallingHttpMessageConverter();
XStreamMarshaller xstreamMarshaller = new XStreamMarshaller();
xmlConverter.setMarshaller(xstreamMarshaller);
xmlConverter.setUnmarshaller(xstreamMarshaller);
return xmlConverter;
}
}
여기서 주의해야 할 점은 @EnableWebMvc를 빼주어야 한다는 것이다. 기본적으로 스프링에서 제공해주는 웹 기능들에 추가적으로 커스터마이징을 하기를 원한다면 @EnableWebMvc없이 WebMvcConfigurer를 구현한 설정 파일만 등록해주어야 하며, 만약 WebMvcConfigurer를 구현하면서 @EnableWebMvc를 같이 붙여주면 스프링의 기본 설정들이 일부 무시된다. 이와 관련된 내용은 스프링부터 공식 문서를 통해 참고하도록 하자.
실제로 다음과 같은 WebMvcConfig에 @EnableWebMvc를 켜고 실행했을때와 끄고 실행했을 때 몇개 빈들이 등록이 되지 않는 것을 확인할 수 있다.
//@EnableWebMvc
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
}
@Component
public class WebConfigTest {
@Autowired
private List<ViewResolver> viewResolvers;
@PostConstruct
public void temp() {
viewResolvers.forEach(System.out::println);
System.out.println(viewResolvers.size());
}
}
ViewResolver의 경우에는 @EnableWebMvc와 @Configuration을 함께 사용하면 @Configuration만 있을 때 등록되어 있었던 ContentNegotiatingViewResolver와 InternalResourceViewResolver가 ViewResolver에서 빠져있음을 확인할 수 있었다. ContentNegotiatingViewResolver는 HTTP 헤더나 API Suffix 등으로 어떻게 자원을 반환할 지 결정하는 빈이고, InternalResourceViewResolver는 디스패처 서블릿이 기본적으로 사용하는 ViewResolver이다. 하지만 HttpMessageConverter나 HandlerMethodArgumentResolver는 그대로이므로 API 서버를 개발하는데 크게 중요한 빈들이 빠졌던 것 같지는 않다.
과거에 Java의 인터페이스가 default 메소드를 제공하지 않을 때는 인터페이스인 WebMvcConfigurer를 구현하기 위해 모든 메소드를 오버라이딩해주어야 했다. 그래서 이러한 불편함을 해결하고 필요한 메소드만을 오버라이딩하도록 WebMvcConfigurerAdpater라는 클래스를 제공하여 상속하도록 하였는데, 인터페이스에 default 메소드를 제공하면서 Deprecated되었다.