[Spring] xml configuration을 java configuration으로 변경하기 (2) Web.xml, dispatcher-servlet

rin·2020년 7월 14일
2
post-thumbnail

이전글
1. [Spring] xml configuration을 java configuration으로 변경하기 (1) ApplicationContext

지난 글에 이어서 web.xmldispatcher-servlet.xml을 자바 설정으로 변경할 것이다.

WebConfig

dispatcher-servlet.xml에 포함되어 있던 웹 관련 설정을 옮겨줄 것이다. 사실상 서블릿을 "포함"하는 웹 설정은 이 파일에서 정의되기 때문에 핵심 구성이라고 할 수 있다.

WebMvcConfigurer를 구현하되 설정이 필요한 부분만 메소드 오버라이딩을 사용해 재정의해주면된다.

아래는 dispatcher-serlvet.xml의 설정을 다섯가지 섹션으로 나눠 본 것이다.

순서대로 옮겨보자!

Interceptors

addInterceptors(InterceptorResitry registry)를 오버라이딩한다.

// xml
<mvc:interceptors>
    <mvc:interceptor>
        <mvc:mapping path="/**/api/**"/>
        <mvc:exclude-mapping path="/api/users/**"/>
        <bean class="com.freeboard04_java_config.util.interceptor.AuthInterceptor"></bean>
    </mvc:interceptor>
</mvc:interceptors>
    
// java
@Override
public void addInterceptors(InterceptorRegistry registry){
    registry.addInterceptor(new AuthInterceptor())
            .addPathPatterns("/**/api/**")
            .excludePathPatterns("/api/users/**");
}

xml 설정 파일에 보면 인터셉터 내부에서 커스텀 클래스를 빈으로 정의하고 있다. 따라서 java 구성에서 Interceptor을 추가할 때 커스텀 클래스(AuthInterceptor)를 인자로 넣어준다.

MessageConverter & ArgumentResolver

configureMessageConverters(List<HttpMessageConverter<?>> converters)addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers)를 오버라이딩한다.

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
        converters.add(new MappingJackson2HttpMessageConverter(objectMapper()));
    }
    
    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);

        return objectMapper;
    }
    
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(pageableHandlerMethodArgumentResolver());
    }
    
    @Bean
    public PageableHandlerMethodArgumentResolver pageableHandlerMethodArgumentResolver() {
        PageableHandlerMethodArgumentResolver pageableHandlerMethodArgumentResolver = new PageableHandlerMethodArgumentResolver();
        pageableHandlerMethodArgumentResolver.setMaxPageSize(10000);

        return pageableHandlerMethodArgumentResolver;
    }

Component-scan

@ComponentScan 어노테이션을 사용하여 basePackages 내 컨트롤러를 빈으로 등록하도록 정의한다.

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = {"com.freeboard04_java_config.api", "com.freeboard04_java_config.controller"})
public class WebConfig implements WebMvcConfigurer {
	// 생략 ....
}

❗️ NOTE
@EnableWebMvc
어노테이션 기반의 SpringMvc를 구성할 때 필요한 빈 설정들을 자동으로 해주는 어노테이션. 기본적으로 등록해주는 빈 외에도 추가적으로 개발자가 필요로 하는 빈의 등록을 손쉽게 할 수 있도록 지원한다.

web.xml의 <mvc:annotation-driven/> 태그와 동일한 일을 한다.

WebMvcConfigurer 인터페이스는 해당 어노테이션에서 제공하는 빈을 커스터마이징(설정)할 수 있는 기능을 제공하는 인터페이스이다. 인터페이스를 사용했지만 추상 클래스처럼 사용할 수 있는 이유는 각각의 메소드를 default로 작성했기 때문이다.

viewResolver

configureViewResolvers(ViewResolverRegistry registry)를 오버라이딩한다.

ViewResolverRegistry에 원하는 리졸버 인스턴스를 넣어주면 되는데, 필자는 핸들바를 뷰템플릿으로 사용했기 때문에 "HandlebarsViewResolver"로 셋팅하였다.

//xml
    <bean class="com.github.jknack.handlebars.springmvc.HandlebarsViewResolver">
        <property name="prefix" value="/WEB-INF/view/"/>
        <property name="suffix" value=".hbs"/>
    </bean>


//java
    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        HandlebarsViewResolver viewResolver = new HandlebarsViewResolver();
        viewResolver.setPrefix("/WEB-INF/view/");
        viewResolver.setSuffix(".hbs");
        registry.viewResolver(viewResolver);
    }

customExceptionHandler

예외처리를 커스텀 설정으로 다루고 있기때문에 이를 빈으로 등록해줘야한다. 같은 configuration에서 쓰는게 좀 이상하긴 한데..🤔 xml에 있는 내용을 그대로 옮기는 작업을 하고 있으므로 일단은 이 클래스에서 빈으로 등록해주도록 하겠다. (원래는 분리하는게 맞는듯)

//xml
<bean class="com.freeboard04_java_config.util.exception.ExceptionHandler"></bean>

//java
@Bean
public ExceptionHandler exceptionHandler(){
    return new ExceptionHandler();
}

❗️ NOTE
ref. https://galid1.tistory.com/532

어떻게 WebMvcConfigurer 인터페이스를 구현하는 것만으로도 간편하게 설정이 추가되는걸까?
@EnableWebMvc → @Import(DelegatingWebMvcConfiguration.class)
DelegatingWebMvcConfiguration 클래스는 WebMvcConfigurationSupport를 상속받고 있다.
즉, 해당 클래스의 registry를 매개변수로 하는 메소드들은 WebMvcConfigurationSupport로부터 오버라이딩 된 것이다.

registry에서는 해당 빈에 대한 설정 정보를 셋팅하는데 사용하는 클래스 정도로 이해하면 되겠다. 즉 어떤 빈을 셋팅하기 위한 도구일 뿐이고 직접 인스턴스로 어떤 특징을 가지는 것은 아닌 것 같다. 🤔

registry 설정이 끝나면 모든 설정 정보를 반환하고 이를 DispatcherServlet에 등록하는 것이다.

전체 코드는 다음과 같다.

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = {"com.freeboard04_java_config.api", "com.freeboard04_java_config.controller"})
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry){
        registry.addInterceptor(new AuthInterceptor())
                .addPathPatterns("/**/api/**")
                .excludePathPatterns("/api/users/**");
    }

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
        converters.add(new MappingJackson2HttpMessageConverter(objectMapper()));
    }

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(pageableHandlerMethodArgumentResolver());
    }

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        HandlebarsViewResolver viewResolver = new HandlebarsViewResolver();
        viewResolver.setPrefix("/WEB-INF/view/");
        viewResolver.setSuffix(".hbs");
        registry.viewResolver(viewResolver);
    }

    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);

        return objectMapper;
    }

    @Bean
    public PageableHandlerMethodArgumentResolver pageableHandlerMethodArgumentResolver() {
        PageableHandlerMethodArgumentResolver pageableHandlerMethodArgumentResolver = new PageableHandlerMethodArgumentResolver();
        pageableHandlerMethodArgumentResolver.setMaxPageSize(10000);

        return pageableHandlerMethodArgumentResolver;
    }

    @Bean
    public ExceptionHandler exceptionHandler(){
        return new ExceptionHandler();
    }
}

WebInitalizer

web.xml을 대체할 이 설정 파일은 @Configuration 어노테이션이 붙지않는다.
스프링 프레임워크가 시작될 때 WAS는 "WebApplicationInitializer"의 구현체를 찾아 오버라이딩된 onStartup 메소드를 실행시키기 때문에 "WebApplicationInitializer"를 구현하는 클래스로 설정을 옮겨주기만 한다.

config 패키지 하위에 "WebApplicationInitializer"를 구현하는 WebInitializer 클래스를 만들어 준다.
큰 범주에서 서블릿 설정과 필터 설정 두가지이므로 onStartup에서 호출할 메서드를 두 개 작성할 것이다.

registerDispatcherServlet

디스패처 서블릿 설정을 등록하는 코드이다.
xml에서는 따로 명시하지 않는 경우에 dispatcher-servlet.xml 이라는 이름으로 자동 연결하므로 생략된 부분인데 자바 구성에서는 명시해주어야한다.

    private void registerDispatcherServlet(ServletContext servletContext) {
        AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
        context.register(ApplicationContext.class);

        servletContext.addListener(new ContextLoaderListener(context));

        AnnotationConfigWebApplicationContext webContext = new AnnotationConfigWebApplicationContext();
        webContext.register(WebConfig.class);

        ServletRegistration.Dynamic dispatcher = servletContext.addServlet("DispatcherServlet", new DispatcherServlet(webContext));
        dispatcher.setLoadOnStartup(1);
        dispatcher.addMapping("/*");
    }

이 부분을 작성하면서 헷갈렸던 부분이 AnnotationConfigWebApplicationContext 클래스의 register 메소드를 들여다보아도 컴포넌트를 등록한다는 이야기 뿐이지 정확히 어떤 Configuration을 넘겨야하는지 써져있지 않음 😑..

결국 구글링해서 찾아보면 register 메소드의 인자로 RootConfig.classWebConfig.class를 넣는데(왠지 모르겠는데 클래스 이름은 거기서 거기임) 대체 그 설정파일이 뭔지는 안알려주고 있었다.

한참을 뒤진결과, context에 등록될 빈이 어떤 종류냐에 따라서 갈리는 것이었다.
즉, 웹과 상관없는 도메인, 레파지토리와 같은 빈들(ApplicationContext.class에 선언)은 위와같이 context에 등록 + ContextLoaderListener에 사용하며 웹과 관련된 컨트롤러와 같은 빈들(WebConfig.class에 선언)은 서블릿 생성에 사용한다.

사실 이 부분이 아직 어떻게 돌아가는지 헷갈리기 때문에 😢.. 코드를 더 자세히 읽어보아야 할 듯 하다.

registerCharacterEncodingFilter

필터 설정을 위한 메소드이다. web.xml에 있는 설정을 그대로 옮긴 것이기 때문에 어려운 부분은 없을 것이다.

private void registerCharacterEncodingFilter(ServletContext servletContext) {
    FilterRegistration.Dynamic characterEncodingFilter = servletContext.addFilter("encodingFilter", new CharacterEncodingFilter());
    characterEncodingFilter.setInitParameter("encoding", "UTF-8");
    characterEncodingFilter.setInitParameter("forceEncoding", "true");
    characterEncodingFilter.addMappingForServletNames(EnumSet.allOf(DispatcherType.class), true, "/*");
}

결과적으로 WebInitializer 클래스는 다음과 같다.

public class WebInitializer implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        registerDispatcherServlet(servletContext);
        registerCharacterEncodingFilter(servletContext);
    }

    private void registerDispatcherServlet(ServletContext servletContext) {
        AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
        context.register(ApplicationContext.class);

        servletContext.addListener(new ContextLoaderListener(context));

        AnnotationConfigWebApplicationContext webContext = new AnnotationConfigWebApplicationContext();
        webContext.register(WebConfig.class);

        ServletRegistration.Dynamic dispatcher = servletContext.addServlet("DispatcherServlet", new DispatcherServlet(webContext));
        dispatcher.setLoadOnStartup(1);
        dispatcher.addMapping("/*");
    }

    private void registerCharacterEncodingFilter(ServletContext servletContext) {
        FilterRegistration.Dynamic characterEncodingFilter = servletContext.addFilter("encodingFilter", new CharacterEncodingFilter());
        characterEncodingFilter.setInitParameter("encoding", "UTF-8");
        characterEncodingFilter.setInitParameter("forceEncoding", "true");
        characterEncodingFilter.addMappingForServletNames(EnumSet.allOf(DispatcherType.class), true, "/*");
    }
}

Test

dispatcher-servlet.xml 파일을 읽어들이던 테스트 코드 어노테이션을 변경하여 실행해보자.

//변경 전
@ContextConfiguration(locations = {"file:src/main/webapp/WEB-INF/applicationContext.xml", "file:src/main/webapp/WEB-INF/dispatcher-servlet.xml"})

//변경 후
@ContextConfiguration(classes = {ApplicationContext.class, WebConfig.class})

몇 개의 테스트가 깨지는 것을 확인했는데 에러코드는 다음과 같다.

핵심은 Json 문자열로 반환된 createdAt을 LocalDateTime 타입의 Object로 deserialize 할 수가 없다는 것이다.

custom deserializer 추가

ObjectMapper 및 Custom serializer/deserializer와 관련한 기본적인 내용은 [번역] Intro to the Jackson ObjectMapper + 예제 코드에서 확인할 수 있습니다.

config 패키지 하위에 jackson 패키지를 생성하고 LocalDateTimeDeserializer 클래스를 만들도록 하자

public class LocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {

    @Override
    public LocalDateTime deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException, JsonProcessingException {

        JsonNode tree = jsonParser.getCodec().readTree(jsonParser);
        int year = tree.get("year").asInt();
        int month = tree.get("monthValue").asInt();
        int dayOfMonth = tree.get("dayOfMonth").asInt();
        int hour = tree.get("hour").asInt();
        int minute = tree.get("minute").asInt();
        int second = tree.get("second").asInt();
        int nano = tree.get("nano").asInt();

        return LocalDateTime.of(year, month, dayOfMonth, hour, minute, second, nano);
    }

}

이 커스텀 LocalDateTimeDeserializer를 오류가 나는 오브젝트인 CommentDto의 LocalDateTime에 설정해준다.

더이상 테스트 코드가 깨지지 않는 것을 확인 할 수 있을 것이다.

다른 테스트 클래스들의 @ContextConfiguration의 argument를 locations 대신 classes로 변경한 뒤 webapp 폴더 하위의 모든 설정파일을 제거한다.

톰캣을 띄워서 실제 웹에서도 잘 돌아가는지 확인해보자!!

모든 코드는 github에서 확인 하실 수 있습니다.

profile
🌱 😈💻 🌱

1개의 댓글

comment-user-thumbnail
2021년 5월 11일

좋은 글 잘보고 갑니다~

답글 달기