XML 없이 Java만 사용해서 설정하기

출처 : http://breadmj.wordpress.com/2013/08/04/spring-3-only-java-config-without-xml/


Spring과의 첫만남

내가 스프링을 처음으로 접한 것은 스프링 프레임워크의 버전이 3.0 으로 막 올라간지 얼마 안되었을 때였다.

대략 3년정도 된 것 같은데 그때 작성했던 코드들을 아직도 사용하고 있다. 물론 자바 로직은 기능 변화에 맞추어 많이 변경되었다. 하지만 맨 처음에 작성했던 스프링 설정은 큰 변화없이 지금까지 사용중이다. 어떻게 보면 조금이라도 더 나은 설정을 위한 노력이 부족했다고 생각할 수도 있고 아니면 그 반대로 스프링을 사용했기에 (3년전의 내 코딩 실력에도 불구하고) 지금까지 안정적으로 유지했다고 생각할 수도 있다.

Spring 설정은 어플리케이션의 뼈대를 이룬다

스프링을 사용한다면 스프링 설정은 그 어플리케이션의 뼈대를 이루게 된다. 조금 억지를 넣어서 말해보자면 스프링 설정이 곧 어플리케이션 설계라고 할 수도 있다. 그렇기에 현재 운영중인 어플리케이션의 기반을 건드리는 것이 무서운 점도 어느정도 있었다. 스프링 설정이 조금 알아보기 힘들다거나, 미관상(?) 지저분해 보인다거나, 사소한 실수가 보이더라도 운영이 불가능한 상태가 아닌 이상 웬만하면 건드리지 않았다. 그러다보니 기능은 점점 늘어나는데 설정은 지저분하게 남아있어 변경하기 힘들고, 하위 호환성을 엄청 신경쓰기로 유명한 스프링의 버전을 올리는 것도 꺼려지는 지경에 이르렀다.

공포의 applicationContext.xml

그러나 과연 스프링 설정을 바꾸기 고민되는 것이, 설정 파일을 지저분하게 만들어놓은 것 때문만일까? 물론 아니니까 이 글을 썼겠지. 나는 그 이유의 반 이상이 스프링 설정에 XML 을 이용했기 때문이라고 생각한다.

스프링을 맨 처음 접했을 때, applicationContext.xml 이라는 무시무시한 이름의 파일안에 더 무시무시한 빈 설정들을 보고 식겁했던 기억이 난다. 뭐 무슨 컨버전 어쩌고.. 핸들러.. 리졸버.. 난 그것들이 무엇이고 왜 필요하며, 이 설정들을 스프링이 어떻게 읽어가는지 이해하는데까지 1년 이상의 시간이 걸렸다. 물론 그것들을 완벽하게 이해하지 못해도 어느정도 사용가능하긴 하다. 뭐든 다 삽질하면서 배우는거니까.

왜 꼭 xml 로 설정해야 하죠?

각설하고, 나와 비슷한 생각들을 많이들 했던 것 같다. 오래 전부터 스프링 설정을 XML 이 아닌 오직 Java만으로 할 수 있도록 이런저런 노력들이 이어져왔다. 그것이 근래 들어서 Servlet 3.0 스펙이 확정되고, 또 그 스펙을 구현한 톰캣 7.0 이 나오면서 꽃을 피웠다. xml 설정 단 한줄도 없이 자바만으로 스프링을 사용할 수 있게 된 것이다.

Java로 설정하면 뭐가 좋을까?

그렇다면 XML에 비해 Java만을 사용해서 설정하는 것이 어떤 이득이 있을까? 개인적인 견해도 섞여있다.

  1. 설정 파일을 따로 유지할 필요가 없다. 그냥 자바 클래스이다. 찾기 쉽다.
  2. 보다 명료하다. 어떤 것들이 빈으로 만들어지는지 파악하기 쉽다.
  3. IDE의 자동완성 기능을 사용할 수 있다. 자바 코드이기 때문이다. 그래서 작성과 수정이 빠르다.
  4. 어플리케이션 로직과 설정 코드를 동일한 언어로 만들 수 있다. 한 언어만 쓰는게 간편하니 좋다.
  5. 설정 코드에 break point 를 걸어서 디버깅할 수 있다.

이 정도만 해도 충분히 자바 코드를 이용해서 설정하는 의미가 있다. 나는 개인적으로 XML 로 만들어져있는 스프링 설정 파일을 읽거나 수정하는 것이 고역이었다. 그에 반해 자바 코드로 설정을 하니 이렇게 좋을 수가 없었다. 특히나 스프링을 처음으로 접해보는 초심자라면 XML 설정보다는 자바 설정을 이용하는 것이 더더욱 좋겠다.

그래서 뭘 어떻게 하는거라고?

서론은 이쯤하고, 이제 Java 를 이용해서 스프링 설정을 하는 방법을 알아보자. 어플리케이션은 서블릿을 만든다고 가정한다. 스프링을 이용해서 주로 서블릿을 많이 만들고 또 사용법이 다 비슷비슷하니, 다른 형태의 어플리케이션을 작성한다 하더라도 충분히 참고할만하다. 여기 나오는 모든 코드는 나의 GitHub 프로젝트 중 SpringMVCTest 프로젝트에 다 포함되어있다.

목표는 다음과 같다.

  • Spring 의 application context 설정들을 Java 로 바꾼다. root-context.xml, servlet-context.xml 요런것들 말이다.
  • web.xml 설정을 Java 로 바꾼다.

자, 그럼 시작해보자.

첫번째, pom.xml 설정 (메이븐 설정)

프로젝트 의존성 관리는 메이븐을 이용한다고 가정한다.

1. Spring 버전 3.1 이상 사용한다. 나는 현재 최신버전인 3.2.2.RELEASE 를 사용했다. 

<!-- Spring -->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-context</artifactId>
  <version>3.2.2.RELEASE</version>
</dependency>
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-webmvc</artifactId>
  <version>3.2.2.RELEASE</version>
</dependency> 


2. servlet-api 버전 3.0 이상 사용한다. web.xml 을 없애기 위해서는 서블릿 3.0 이상의 스펙이 필요하다. 

<!-- use Servlet 3.0 spec -->
<!-- Java Config 를 사용하기 위해서는 서블릿 3.0 이상의 스펙이 필요하다. -->
<!-- 톰캣의 경우 7.0 이상을 사용해야 한다. -->
<dependency>
  <groupId>javax.servlet</groupId>
  <artifactId>javax.servlet-api</artifactId>
  <version>3.0.1</version>
  <scope>provided</scope>
</dependency> 


3. Spring 버전 3.1.x 라면 cglib 을 dependency 에 추가한다. @Configuration 어노테이션을 사용하기 위해서 필요하다. 만약 추가해주지 않는다면 런타임 에러가 발생할 것이다. 스프링 버전이 3.2.x 라면 Spring 에 cglib 이 포함되어 있으므로 선언할 필요없다. 

<!-- @Configuration 어노테이션을 쓰기 위해서는 cglib 이 필요하다. -->
<!-- Spring 버전 3.2 이상부터 Spring 에 cglib 이 포함되므로, 버전에 따라 포함할지 말지 결정한다. -->
<dependency>
  <groupId>cglib</groupId>
  <artifactId>cglib</artifactId>
  <version>2.2.2</version>
  <scope>runtime</scope>
</dependency> 


4. maven-war-plugin 에 아래와 같이 failOnMissingWebXml 을 false 로 설정한다. 이 설정이 없다면 web.xml 파일이 존재하지 않는다고 투덜댈 것이다. 

<plugin>

  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-war-plugin</artifactId>
  <version>2.3</version>
  <configuration>
    <failOnMissingWebXml>false</failOnMissingWebXml>
  </configuration>
</plugin>


두번째, root-context.xml 없애기

root-context 에는 주로 프로퍼티 홀더 설정이나 datasource 같이 여러 서블릿에서 공통으로 사용할 설정들이 들어간다. 서블릿을 하나만 띄운다면 root context 와 servlet context 를 굳이 구분할 필요는 없지만, 이 내용은 논점에서 벗어나므로, 일단 root context 와 servlet context 가 구분되어 있다고 가정한다. 하지만 두 context 설정을 바꾸는 것은 근본적으로 동일하다.

그럼 아래 코드를 보자.

// import..

/**
 * 루트 설정용 클래스.
 * 이 클래스는 스프링의 root-context.xml 의 역할을 대신한다.
 * @author mj
 *
 */
@Configuration
public class RootConfig {
 
    @Value("${jdbc.driverClassName}")
    private String jdbcDriverClassName;
 
    @Value("${jdbc.url}")
    private String jdbcUrl;
 
    @Value("${jdbc.username}")
    private String jdbcUsername;
 
    @Value("${jdbc.password}")
    private String jdbcPassword;
 
    private static final String APP_CONFIG_FILE_PATH = "application.xml";
 
    /**
     * 프로퍼티 홀더는 다른 빈들이 사용하는 프로퍼티들을 로딩하기 때문에, static 메소드로 실행된다.
     * 다른 일반 빈들이 만들어지기전에 먼저 만들어져야 한다.
     * @return
     */
    @Bean
    public /* static 메소드에요! */ static PropertyPlaceholderConfigurer propertyPlaceholderConfigurer()
    {
        PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer();
        ppc.setLocations(new Resource[] { new ClassPathResource(APP_CONFIG_FILE_PATH) });
        return ppc;
    }
 
    @Bean
    public DataSource dataSource()
    {
        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setDriverClassName(this.jdbcDriverClassName);
        dataSource.setUrl(this.jdbcUrl);
        dataSource.setUsername(this.jdbcUsername);
        dataSource.setPassword(this.jdbcPassword);
        return dataSource;
    }
 
    // 기타 다른 bean 설정들..
}


  • 클래스를 하나 만든다. 이름은 root context 라는 것을 알아볼 수 있는 적절한 이름이면 된다.
  • 클래스에 @Configuration 어노테이션을 붙여준다. 설정용 클래스라는 것을 스프링에게 알려주는 역할이다.
  • root-context.xml 에 bean 을 등록하듯이, 빈으로 만들어지길 원하는 오브젝트를 리턴하는 메소드를 만든다. 이 메소드 내에서 빈에다가 필요한 설정을 해주고, 그것을 리턴해주면 된다.
  • 빈으로 등록되길 원하는 메소드들에는 @Bean 어노테이션을 붙여준다. 이 어노테이션이 있어야 스프링이 그 메소드들을 실행해서 빈으로 만든다.
  • property placeholder 처럼, 다른 빈보다 먼저 등록되어야 하는 것들은 static 메소드로 만든다. java 에서 static 의 역할이 무엇인지 생각해본다면 static 을 붙여주는 것이 아주 자연스럽다.

세번째, servlet-context.xml 없애기

// import..

 
/**
 * MVC 설정용 클래스.
 * 이 클래스는 스프링의 sevlet-context.xml 의 역할을 대신한다.
 * @author mj
 */
@Configuration
@EnableWebMvc
@EnableAsync // @Async 어노테이션을 사용하기 위함
@ComponentScan(
    basePackages="com.nethru.test",
    excludeFilters=@ComponentScan.Filter(Configuration.class)
)
public class MvcConfig extends WebMvcConfigurerAdapter // 인터셉터를 추가하기 위해 WebMvcConfigurerAdapter 를 상속한다
{
    @Bean
    public ViewResolver viewResolver()
    {
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        resolver.setPrefix("/WEB-INF/views/");
        resolver.setSuffix(".jsp");
        return resolver;
    }
 
    /**
     * 인터셉터 추가
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry)
    {
        registry.addInterceptor(new CorsInterceptor());
    }
}

  • root context와 마찬가지로 적절한 이름의 클래스를 만든 후, @Configuration 어노테이션을 붙여준다.
  • mvc:annotation-driven 은 @EnableWebMvc 어노테이션이 대신한다. 마찬가지로 task:annotation-driven 은 @EnableAsync 가 대신한다. 또한 트랜잭션이나 기타 다른 기능들을 이렇게 어노테이션 하나로 활성화시킬 수 있다.
  • context:component-scan 은 @ComponentScan 어노테이션이 대신한다. xml과 마찬가지로 basePackage 와 filter 를 지정할 수 있다.
  • 빈으로 등록될 오브젝트를 리턴하는 메소드를 만든 후, @Bean 어노테이션을 붙여준다. 위 예제에서는 뷰 리졸버를 빈으로 등록하고 있다.
  • 인터셉터를 등록하기 위해서는 추가적인 작업이 필요하다. WebMvcConfigurerAdapter 라는 스프링 클래스를 상속한 후, addInterceptors() 메소드를 override 한다. 메소드 내에서 필요한 인터셉터와 매핑을 지정해주면 된다.

네번째, web.xml 없애기

일단 코드부터 보자.

// import..

 
/**
 * WebApplicationInitializer 를 상속하면, 서블릿 컨테이너가 실행될 때 onStartup() 메소드가 자동으로 호출된다.
 * 이 클래스는 web.xml 의 역할을 대신하거나 보충한다.
 * @author mj
 *
 */
public class Initializer implements WebApplicationInitializer
{
    @Override
    public void onStartup(ServletContext servletContext)
            throws ServletException
    {
        AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();
        rootContext.register(RootConfig.class);
        servletContext.addListener(new ContextLoaderListener(rootContext));
 
        this.addDispatcherServlet(servletContext);
        this.addUtf8CharacterEncodingFilter(servletContext);
    }
 
    /**
     * Dispatcher Servlet 을 추가한다.
     * CORS 를 가능하게 하기 위해서 dispatchOptionsRequest 설정을 true 로 한다.
     * @param servletContext
     */
    private void addDispatcherServlet(ServletContext servletContext)
    {
        AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
        applicationContext.getEnvironment().addActiveProfile("production");
        applicationContext.register(MvcConfig.class);
 
        ServletRegistration.Dynamic dispatcher = servletContext.addServlet("dispatcher", new DispatcherServlet(applicationContext));
        dispatcher.setLoadOnStartup(1);
        dispatcher.addMapping("/");
        dispatcher.setInitParameter("dispatchOptionsRequest", "true"); // CORS 를 위해서 option request 도 받아들인다.
    }
 
    /**
     * UTF-8 캐릭터 인코딩 필터를 추가한다.
     * @param servletContext
     */
    private void addUtf8CharacterEncodingFilter(ServletContext servletContext)
    {
        FilterRegistration.Dynamic filter = servletContext.addFilter("CHARACTER_ENCODING_FILTER", CharacterEncodingFilter.class);
        filter.setInitParameter("encoding", "UTF-8");
        filter.setInitParameter("forceEncoding", "true");
        filter.addMappingForUrlPatterns(null, false, "/*");
    }
}

  • WebApplicationInitializer 인터페이스를 구현한 클래스를 만든다. 이 클래스는 web.xml의 역할을 대신할 클래스이다.
  • onStartup() 메소드를 override 한다. 이 메소드는 서블릿 컨테이너가 실행될 때 자동으로 호출된다. 이 부분이 Servlet API 3.0 이상 필요한 부분이다. 자세한 메커니즘은 Spring 의 WebApplicationInitialzer javadoc 을 참고한다.
  • 필요한 각종 설정을 Java code 로 구현한다. 예를 들면 DispatcherServlet 이나 CharacterEncodingFilter 같은 것들을 등록해주는 일이다. 알다시피 이것들은 기존에 web.xml 에 기술하던 것들이었다.

결론

  •  한번 java 로 스프링 설정을 해보고나면 xml 설정으로 돌아가기 힘들것이다.
  • 생각보다 어렵지 않다. 익숙하지 않아서 그렇지 예제 코드들을 보고 Spring 문서들을 읽어보면 할 수 있다.


+ Recent posts