spring - WebApplicationContextUtils return null
이 정도는 스프링을 제대로 공부하고 쓰는 사람이면 상식이어야겠다고 생각하고 있지만… 욕심이겠지.
정리해보자.
스프링은 WAR 독립 웹 모듈(애플리케이션)으로 주로 개발된다. 서블릿 컨테이너에 올라가는 웹 애플리케이션이다.
독립 웹 모듈로 만들어진 하나의 스프링 애플리케이션에 애플리케이션 컨텍스트는 몇 개가, 어떻게 만들어질까? 스프링 웹을 제대로 공부했다면 보통 두 개가 만들어진다는 것을 알 수 있다.
하나는 ContextLoaderListener에 의해서 만들어지는 Root WebApplicationContext이고 다른 하나는 DispatcherServlet에 의해서 만들어지는 WebApplicationContext이다. 전자는 보통 서비스계층과 DAO를 포함한, 웹 환경에 독립적인 빈 들을 담아두고 후자는 DispatcherServlet이 직접 사용하는 컨트롤러를 포함한 웹 관련 빈을 등록하는 데 사용한다. 그리고 이 둘이 parent-child ApplicationContext 관계로 맺어진다. 그래서 ContextLoaderListener로 만들어지는 컨텍스트를 Root WAC라고 부르는 것이다.
이론적으로 DispatcherServlet는 여러 개 등록될 수 있다. 왜 그래야 하는지는.. 생각해보면 많은 이유가 있겠지만, 아무튼 기술적으로 가능하고 그런 의도를 가지고 설계되었다. 그리고 각각 DispatcherServlet은 독자적인 WAC를 가지고 있고 모두 동일한 Root WAC를 공유한다. 스프링의 AC는 ClassLoader와 비슷하게 자신과 상위 AC에서만 빈을 찾는다. 따라서 같은 레벨의 형제 AC의 빈에는 접근하지 못하고 독립적이다.
이는 마치 JavaEE의 엔터프라이즈 애플리케이션 축소판을 보는 듯 하다. 공유되는 EJB나 JAR와 이를 사용하는 여러 개의 Web모듈이 존재하는 것과 비슷하다. JavaEE의 모듈개념을 AC단위로 웹 모듈 하나 안에 넣은 것이라고 봐도 좋다. 물론 EE앱처럼 유연한 리로딩이 보장된 건 아니지만, 서블릿 단위로 생각해보면 핫-디플로이도 된다니까 불가능 할 것도 없다.
그런데 Root WAC는 사실 없어도 된다. 당연히 스프링 앱에는 Root WAS를 등록하는 ContextLoaderListener가 필요한 것 같지만 실제론 강제되지 않는다. 대신 서블릿 레벨의 WAC에 웹용 빈들은 물론이고 서비스, DAO 계층의 빈들도 모두 넣으면 된다. 실제로 여러 개의 DispatcherServlet이 만들어질 것이 아니라면 Root WAC를 따로 등록하나 아니나 그게 그거다.
DispatcherServlet 단위의 WAC는 보통 서블릿이름-servlet.xml이라는 이름규칙을 가지고 설정파일을 찾는다. 하지만 이는 얼마든지 바꿀 수 있다. 서블릿 이름과 상관없이 직접 지정한 여러 개의 설정파일을 읽도록 만들어도 그만이다.
아래는 내가 스프링 웹 기능 테스트용으로 만든 애플리케이션의 web.xml 설정의 일부다. init-param이 없다면 action-servlet.xml 만을 읽겠지만, 직접 지정해주면 지정된 설정파일을 사용한다.
<servlet>
<servlet-name>action</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/applicationContext.xml
/WEB-INF/action-servlet.xml
</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
물론 ContextLoaderListener는 필요없다. 결국 하나의 WAC만 만들어진다. 이 경우는 DispatcherServlet이 만든 WAS가 설정파일 계층구조에서 Root위치를 차지한다. 결국 Root WAC가 되는 것이다.
나는 특별한 이유가 아니라면 후자의 방법을 사용할 생각이다. 사실 WAC가 두 개 만들어지기 때문에 발생하는 매우 미묘한 문제가 많다는 것을 그동안 KSUG에 올라온 질문을 통해서 많이 확인했다. 두 개의 parent-child 구조의 WAC이기 때문에 AOP의 적용부터, 상호참조를 비롯해서 만은 부분에서 혼란을 줄 수 있다. 그럴 바에는 차라리 WAC를 하나만 만드는 게 속편하지 않을까? 빈 스캐닝도 excludes 같은 옵션 없이 한방에 해도 된다. 물론 개발과 테스트에서는 원하는 XML파일의 조합을 가져가게 해도 되니까 SoC와 의존관계 관리에서는 문제가 없다.
이렇게 DispatcherServlet 단독 WAC 방식을 사용했을 때 한가지 문제가 발생한다. 그것은 바로
WebApplicationContextUtils.getWebApplicationContext()을 그냥 사용하지 못한다는 점이다.
WebApplicationContextUtils.getWebApplicationContext는 매우 유용한 유틸리티 메소드이다. 스프링 빈이 아닌 뷰(JSP…)나 스프링 빈이 아니고 AC에 바로 접근 불가능한 Struts같은 코드에서 스프링의 빈을 사용하고 싶을 때 ServletContext만 있다면 파라미터로 제공해서 바로 WAC를 가져올 수 있고, 이를 이용해서 getBean()해주면 스프링 빈에 접근 가능하다.
그런데 이 WebApplicationContextUtils.getWebApplicationContext()가 돌려주는 사실 WAC는 듀얼 WAC 방식이라면 ContextLoaderListener가 등록해주는 Root WAC뿐이다. 즉, DispatcherServlet이 등록한 WAC를 돌려주지 않는다. 게다가 계층구조의 ROOT이므로 하위 WAC에 접근하지도 못한다. 내가 아는 한 방법이 없다.
첫 번째 문제는 스프링 밖의 코드에서 WebApplicationContextUtils.getWebApplicationContext()를 통해서 DispatcherServlet의 WAC에 접근하는 것이 가능한가 이다.
두 번째도 사실 비슷한데 듀얼 WAC방식이 아니라 DispatcherServlet에 등록된 WAC만 생성하는 경우에 이 WAC를 찾는 일이다. 이 때는 DS의 WAC가 Root WAC라고 했으니 당연히 WebApplicationContextUtils.getWebApplicationContext()로 가져올 수 있을 것 같다. 하지만 해보면 못가져온다. null만 리턴될 뿐이다. WebApplicationContextUtils.getWebApplicationContext()는 결국 ContextListenerLoader가 등록한 WAS밖에 못돌려준다. 설령 Root WAC로 등록되도 말이다.
따라서 WebApplicationContextUtils.getWebApplicationContext()가 Root WAC를 돌려준다는 것은 반만 맞는 얘기다.
이 문제를 해결하려면 WAC가 어떻게 ServletContext를 통해서 찾아지는지 그 원리를 알아야 한다. 스프링이 만드는 WAC는 모두 ServletContext의 attribute로 등록된다. ContextLoaderListener가 만드는 것은 웹 애플리케이션 당 하나뿐이다. 따라서 항상 일정하게 찾을 수 있다.
반면에 DS가 만드는 WAC는 한 개 이상이 등록가능하다. 따라서 다른 방식으로 저장되기 때문에 WebApplicationContextUtils.getWebApplicationContext가 바로 찾지 못하는 것이다. 이 WAC를 찾으려면 그에 맞는 attribute name을 파라미터에 전달해줘야 한다.
WebApplicationContextUtils.getWebApplicationContext(ServletContext) 메소드는 이미 그 Attr. name을 알고 있다. 따라서 그 이름을 신경쓰지 않는다. 바뀔 염려도 없다.
DS의 WAS는 다른 이름으로 등록된다. 따라서 attr name을 지정해줘야 한다.
이 이름을 찾는 방법은 다음과 같이 작성 하면 된다.
FrameworkServlet.SERVLET_CONTEXT_PREFIX + 서블릿이름
DS의 WAC는 FrameworkServlet.SERVLET_CONTEXT_PREFIX에 지정된 문자열에 서블릿이름을 추가해서 저장한다. 그래서 여러 개의 DS가 등록되도 서로 충돌없이 찾을 수 있다. 서블릿이름이 action이라면 다음과 같이 하면 WAC를 찾을 수 있다.
String attr = FrameworkServlet.SERVLET_CONTEXT_PREFIX + "action";
WebApplicationContext ac = WebApplicationContextUtils.getWebApplicationContext(sc, attr);
저 FrameworkServlet은 참 친근한 이름이다. OS도 만든다는 유명한 땡땡사의 네모네모 프레임워크를 역컴파일 한 소스를 Y군이 검토해달라고 보여준 적이 있는데, 그 안에보니 당당하게도 스프링의 FrameworkServlet을 비롯한 핵심 클래스를 그대로 배껴서 사용하고 있는 것을 확인했다. 과연 아파치 라이선스를 제대로 지켜서 코드의 사용여부를 밝히고 제품을 팔아먹고 있는지 그것이 궁금했지만, 애정이 없는 관계로 사실확인까지는 못해봤다. 단, 기억나는 것은 기술영업사원들이나 엔지니어들이 "스프링과 같은 오픈소스는 불안해서 못쓴다. 세계적인 기술력을 가진 우리 제품을 쓰라"고 얘기하고 다녔다는 것. 그런데 몰래 오픈소스를 배껴쓰나. 라이선스도 허용되는데 당당히 가져다 쓰지 패키지이름은 싹 바꾸고 자기들이 만든 것처럼 넣었다는 데서 살짝 짜증이 나기도 했다. 아무튼 그때 발견했던 대표적인 스프링 클래스가 바로 저 FrameworkServlet이다. 그 안에 attr. name에 쓸 문자열이 담겨있다는 것. 왜 저 클래스의 이름을 쓰냐 하면 바로 DispatcherServlet의 상위 클래스이기 때문이다.
아무튼 문제 해결.
보너스로 하나 더.
Spring 3.0에선 XML이 전혀 없는 웹 애플리케이션을 간단히 만들 수 있다. 물론 2.5부터 가능했던 것이지만 더 간편해졌다. @Configuration을 이용한 코드에 의한 빈 등록이 가능하기 때문이다. 스프링이 주류 기술이 된 이후로 스프링을 공격하는 많은 안티들이 생겼다. 그들의 주장의 99%쯤은 근거에 없는 비방이거나(스프링은 단일 DB밖에 못쓴다는 둥, JCA는 안된다는 둥, 단지 MVC 프레임워크라는 둥) 아니면 2004년에 나온 스프링 1.0에나 해당되는 철지난 기능에 대한 비난이다. 마치 자바는 느려서 실전에서는 못쓴다는 얘기나 하이버네이트는 네이티브 SQL을 쓸 수 없어서 쓰면 안된다는 것과 비슷한 수준이다.
이들 비난의 대표선수들은 "스프링은 XML 설정밖에 안되서 XML hell" 이라는 것과 "스프링은 Stateless 방식 밖에 안된다"는 것이다.
간단히 반박하자면 스프링은 초기부터 지금까지 단 한번도 XML설정으로 설정방식이 제한된 적이 없다. 애초부터 설정정보는 설정포맷에 독립적으로 설계되었고 리더만 만들어주면 어떤 방식으로든 빈 설정을 만들 수 있었다. 비공식적으로는 1.2시절부터(스캐닝 방식은 2.0 시절부터 있었고), 공식적으로는 2.1이 만들어지는 시점부터 애노테이션을 통한 설정을 지원했고, 이미 그 이전에 로드 존슨이 직접 시작한 JavaConfig을 통해서 자바코드에 의한 설정도 지원했다. 이미 최신 스프링 앱에서는 XML의 사용이 극소화 되었고, 스프링의 설정방식에 대한 확장기능을 통해서 점점 더 단순해지고 있다.
또한 스프링은 2.0부터 Scope개념을 지원해서 원하는 단위와 방식의 conversation과 stateful bean을 가지는 애플리케이션을 만들 수 있도록 지원해오고 있다. 벌써 4년이 넘었다. Scope를 가지고 stateful app용인 SWF도 만들고, 데이터그리드로 확장해서 Coherence-Spring도 만들고, 심지어 Seam은 스스로 스프링에서 동작하도록 하는 기능도 만들었다. Stateful app에서 사용할 수 있도록 JPA의 extended EntityManager도 사용하도록 오래전부터 지원하고 있다. 그런데도 아직도 스프링은 statelesss 밖에 안된다고 떠들고 다니는 사람은 사기꾼 아니면 자신이 비판하는 대상에 대한 최소한의 확인의지 조차없는 나태한 사람일 뿐이다.
얘기가 샜는데.. 아무튼 3.0의 비 XML 설정방식을 적극 사용하겠다고 하면 정말이지 모든 설정에서 XML을 완벽하게 제거할 수 있다. 이미 컴포넌트 스캔이나 @Autowired는 잘 알려진 기법이다. 하지만 DataSource 같은 서비스 빈의 등록은 설정자체가 중요하기 때문에 보통 XML로 남겨둔다. 애노테이션을 극대화 해서 쓴다는 다른 기술도 사실은 그런 환경과 관련된 설정을 위해서 이런 저런 XML을 잔뜩 사용하기 일수다. 그나마 서비스나 인프라스트럭처 빈도 대부분 namespace와 전용 tag를 제공하고 있기 때문에 <bean>과 같은 식으로 등록할 일도 거의 없다.
하지만 원하면 그것도 XML을 사용하지 않도록 할 수 있다. @Configuration을 이용한 자바코드에 의한 설정은 사실 그런 용도로 유용하다. 코드에 의한 설정이므로 얼마든지 원하는 방식으로 3-rd party 빈을 정의할 수 있다.
결국 그런 방식까지 쓴다면 아예 웹 애플리케이션에서 WAC가 필요로 하는 XML파일의 등록을 없앨 수 있다.
원래 DS나 ContextLoader가 만드는 WAC는 XmlWebApplicationContext이다. 물론 디폴트일 뿐이다. 스프링은 언제나 그렇듯이 간단히 확장하면 된다. ListenerContextLoader나 DispatcherServlet이 사용할 contextClass 값을 AnnotationConfigWebApplicationContext으로 바꿔주기만 하면 된다. 그러면 XML은 아예 생략할 수 있다. 결국 web.xml을 제외한 모든 스프링 XML을 웹 애플리케이션에서 제거하는 것이 가능하다. 이 WAC는 JSR-330으로 표준화된 @Inject도 지원한다.
블로그를 너무 오래 방치해 두는 것 같아서 시작한 끄적거리기는 여기서 끝.
사실은 요즘 마무리 하고 있는 스프링 3.0 책의 AC설정 부분에 나오는 내용을 조금 배낀 것이다. 책에는 이보다 훨씬 친절하게 AC 등록방법, 설정방법과 선택에 관한 설명이 나올 것이다.
출처 - http://toby.epril.com/?p=934