프로그래밍/Spring Framework

RequestMappingHandlerMapping 의 setDefaultHandler 문제 발견

모지사바하 2013. 5. 20. 17:41

Spring 3.0 의 DefaultAnnotationHandlerMapping 과 AnnotationMethodHandlerAdapter가  Spring 3.1 에선 @Deprecated 되고, 대신 RequestMappingHandlerMapping과 RequestMappingHandlerAdapter 로 바뀌었다.

 

이유인즉슨, Controller 의 요청이 메소드 단위로 세분화 되면서 , DefaultAnnotationHandlerMapping 이 AnnotationMethodHandlerAdapter로 handler 를 전달해줄 때, 문제가 생겼기 때문이다 .

문제에 대해 아~~주 간략히 설명하자면 DefaultAnnotationHandlerMapping 은 AnnotationMethodHandlerAdapter에 메소드 단위를 전달해줄 방법이 없었기때문에 handler Object 를 통째로 전달해줬고, AnnotationMethodHandlerAdapter 에선 본래 자신이 할 일이 아닌 넘어온 handler Object에서 실행해야 할 메소드를 찾아야 하는 단일책임원칙 을 위반한 구조였다.

원칙을 위반하였기 때문에 당연히 확장성에서도 문제가 발생하였다.. HandlerIntercepter 가 바로 대표적 사례이다.

 

그리하여, RequestMappingHandlerMapping 에선 HandlerMethod 를 생성하여 메소드 요청 정보를 담아 RequestMappingHandlerAdapter 에 전달해주는 방식으로 이 문제를 해결하였다.

 

 

그런데 오늘 RequestMappingHandlerMapping 의 defaultHandler 를 등록하던 중 문제가 발생하였다.

 

defaultHandler를 등록하였을 경우, 웹요청을 처리할 핸들러(Controller) 를 찾지 못했을 경우,

 

default로 사용할 핸들러(컨트롤러)를 등록함으로써 사용자에게 404 에러 페이지 대신, 친절한 안내 페이지를 보여주게 된다.

 

그런데 에러가 발생하였다 ..

 

 

 

위와 같은 오류가 발생하여, 디버깅을 해보았다..

 

AbstractHandlerMapping 의

public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception

 

에 브레이크 포인트를 걸고, 확인해보니 디폴트로 등록된 HandlerMapping 전략

(BeanNameHandlerMapping, SimpleUrlHandlerMapping, RequestMappingHandlerMapping)을 차례로 돌아가며 실행하였고,

if (handler == null) {
   handler = getDefaultHandler();
  }

와 같이 디폴트 핸들러를 가져오도록 돼었다.

 

RequestMappingHandlerMapping 시점에서 등록된 defaultHandler 를 발견하고,

 

@Override
 protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception

메소드를 호출하였고,

 

메소드 내부에서

 

HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);

 

로 HandlerMethod 를 가져왔다 ... lookupHandlerMethod(lookupPath, request) 를 따라가보니 미리 추가된 urlMap

(

private final MultiValueMap<String, T> urlMap = new LinkedMultiValueMap<String, T>();

Set<String> patterns = getMappingPathPatterns(mapping);
  for (String pattern : patterns) {
   if (!getPathMatcher().isPattern(pattern)) {
    this.urlMap.add(pattern, mapping);
   }
  }

위와 같은 방법으로 urlMap 에 값을 add한다.

값은 {/callable/view=[{[/callable/view],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}], /defaultcon=[{[/defaultcon],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}], /fileupload=[{[/fileupload],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}, {[/fileupload],methods=[POST],params=[],headers=[],consumes=[],produces=[],custom=[]}], /home=[{[/home],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}], /filelist=[{[/filelist],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}, {[/filelist],methods=[],params=[],headers=[],consumes=[],produces=[application/xml || application/json],custom=[]}], /fileupload_old=[{[/fileupload_old],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}, {[/fileupload_old],methods=[POST],params=[],headers=[],consumes=[multipart/form-data],produces=[],custom=[]}], /testview=[{[/testview],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}], /myrequest=[{[/myrequest],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}], /jsontest=[{[/jsontest],methods=[],params=[],headers=[],consumes=[],produces=[application/json],custom=[]}], /filedownload/{fileId}=[{[/filedownload/{fileId}],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}], /advicetest=[{[/advicetest],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}], /userinfo=[{[/userinfo],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}, {[/userinfo],methods=[POST],params=[],headers=[],consumes=[],produces=[],custom=[]}]}

이런식으로 들어가 있으며, @requestMapping에 해당하는 요청정보를 add 한 것이다. )

에서 요청 url 에 해당하는 키값으로 value 를 가져와서  List<Match> matches = new ArrayList<Match>(); 에  add하도록 돼어있으나, 난 defaultHandler 작동여부를 확인하기 위해

존재하지 않는 url을 호출하였기에 값이 있을리 없었다.  값이 존재 하지 않아 matches 가 텅텅 비어 있으면

if (matches.isEmpty()) {
   // No choice but to go through all mappings
   addMatchingMappings(this.handlerMethods.keySet(), matches, request);
  }

를 실행한다

addMatchingMappings를 호출 할 때 넘겨준 this.handlerMethods.keySet() 는 

private final Map<T, HandlerMethod> handlerMethods = new LinkedHashMap<T, HandlerMethod>();

Map이고, 데이터는 아래와 같이 들어 있다. 여기엔 단순 url 정보뿐만 아니라 컨트롤러와 메소드, 파라미터 정보까지 들어있다.

{{[/callable/view],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}=public java.util.concurrent.Callable<java.lang.String> me.anna.springmvc.CallableController.callableWithView(org.springframework.ui.Model), {[/defaultcon],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}=public java.lang.String me.anna.springmvc.DefaultController.home(), {[/fileupload],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}=public void me.anna.springmvc.FileUploadController.fileUploadForm(), {[/fileupload],methods=[POST],params=[],headers=[],consumes=[],produces=[],custom=[]}=public void me.anna.springmvc.FileUploadController.processUpload(org.springframework.web.multipart.MultipartFile,org.springframework.ui.Model) throws java.io.IOException, {[/home],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}=public java.lang.String me.anna.springmvc.HomeController.home(me.anna.domain.FileDomain,org.springframework.ui.Model), {[/filelist],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}=public java.lang.String me.anna.springmvc.HomeController.fileList(org.springframework.ui.Model), {[/fileupload_old],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}=public java.lang.String me.anna.springmvc.HomeController.fileupload(org.springframework.ui.Model), {[/testview],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}=public java.lang.String me.anna.springmvc.HomeController.testView(org.springframework.ui.Model), {[/myrequest],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}=public java.lang.String me.anna.springmvc.HomeController.myWelcome(), {[/jsontest],methods=[],params=[],headers=[],consumes=[],produces=[application/json],custom=[]}=public me.anna.domain.Result me.anna.springmvc.HomeController.jsonTest(), {[/filedownload/{fileId}],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}=public java.lang.String me.anna.springmvc.HomeController.fileDownload(int,org.springframework.ui.Model,org.springframework.web.servlet.mvc.support.RedirectAttributes), {[/advicetest],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}=public java.lang.String me.anna.springmvc.HomeController.adviceTest(org.springframework.ui.Model) throws java.io.IOException, {[/filelist],methods=[],params=[],headers=[],consumes=[],produces=[application/xml || application/json],custom=[]}=public java.util.List<me.anna.domain.FileDomain> me.anna.springmvc.HomeController.listWithMarshalling(), {[/fileupload_old],methods=[POST],params=[],headers=[],consumes=[multipart/form-data],produces=[],custom=[]}=public java.util.concurrent.Callable<org.springframework.http.ResponseEntity<me.anna.domain.FileDomain>> me.anna.springmvc.HomeController.handleFormUpload(me.anna.domain.FileDomain,org.springframework.web.multipart.MultipartFile), {[/userinfo],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}=public java.lang.String me.anna.springmvc.UserEditController.getUser(org.springframework.ui.Model), {[/userinfo],methods=[POST],params=[],headers=[],consumes=[],produces=[],custom=[]}=public java.lang.String me.anna.springmvc.UserEditController.updateUser(me.anna.domain.User,org.springframework.web.bind.support.SessionStatus,org.springframework.web.servlet.mvc.support.RedirectAttributes)}

 

위 handlerMethod의 키셋 만 넘겨주니 최종적으로 파라미터로 넘겨준 값은 아래와 같다.

[{[/callable/view],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}, {[],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}, {[/fileupload],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}, {[/fileupload],methods=[POST],params=[],headers=[],consumes=[],produces=[],custom=[]}, {[/home],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}, {[/filelist],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}, {[/fileupload_old],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}, {[/testview],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}, {[/myrequest],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}, {[/jsontest],methods=[],params=[],headers=[],consumes=[],produces=[application/json],custom=[]}, {[/filedownload/{fileId}],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}, {[/advicetest],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}, {[/filelist],methods=[],params=[],headers=[],consumes=[],produces=[application/xml || application/json],custom=[]}, {[/fileupload_old],methods=[POST],params=[],headers=[],consumes=[multipart/form-data],produces=[],custom=[]}, {[/userinfo],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}, {[/userinfo],methods=[POST],params=[],headers=[],consumes=[],produces=[],custom=[]}]

 

private void addMatchingMappings(Collection<T> mappings, List<Match> matches, HttpServletRequest request) {
  for (T mapping : mappings) {
   T match = getMatchingMapping(mapping, request);
   if (match != null) {
    matches.add(new Match(match, handlerMethods.get(mapping)));
   }
  }
 }

따라와 보니 루프를 돌면서 getMatchingMapping 을 호출하고 있다. 키값 을 하나 씩 넘기면서..

getMatchingMapping 를 따라가보니,

@Override
 protected RequestMappingInfo getMatchingMapping(RequestMappingInfo info, HttpServletRequest request) {
  return info.getMatchingCondition(request);
 }

와 같은 형태다 . addMatchingMappings 을 호출할 때 넘겨준 파라미터 this.handlerMethods.keySet() 은

private final Map<T, HandlerMethod> handlerMethods = new LinkedHashMap<T, HandlerMethod>(); 형태고

이 Map 의 키에 해당하는 T가 바로 getMatchingMapping 의 파라미터 인 RequestMappingInfo  타입인것이다.

 

그런데 이 루프를 다 돌아도 여전히 match 는 null이다 ... 왜냐하면 요청 url 에 해당하는 키값을 찾을수가 없는것이다 ...

 

최종적으로 DispatcherServlet 의

 

protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
  for (HandlerAdapter ha : this.handlerAdapters) {
   if (logger.isTraceEnabled()) {
    logger.trace("Testing handler adapter [" + ha + "]");
   }
   if (ha.supports(handler)) {
    return ha;
   }
  }
  throw new ServletException("No adapter for handler [" + handler +
    "]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
 }

 

메소드를 호출하고

throw new ServletException("No adapter for handler [" + handler +
"]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");

익셉션이 발생한다.

내용이 너무 방대하여 관련내용을 모두 작성하지는 못했지만, 스프링 3.0 의 DefaultAnnotationHandlerMapping 과 AnnotationMethodHandlerAdapter 에선 defaultHandler 를 등록하면 잘 작동한다..

 

하지만 3.2에선 작동하지 않는다.

이는 3.1 이상에서 생긴 HandlerMethod 를 생성하지 못했기에, HandlerAdapter 전략에서 support 하지 못하여 익셉션이 발생하는것으로 보인다.