Spring Boot, Spring MVC가 엄청난 설정들을 대신 해주기에 편리하게 사용할 수 있는 것인데요. Spring MVC를 구성하는 주요 요소가 무엇이고 각 구성 요소들이 서로 어떻게 연결되는지 정도는 이해하면 좋을 것 같아서 이번 글에서 정리해보려 합니다.
위의 그림에서 <<spring bean>>
이라고 되어 있는 것들은 Spring Bean
으로 등록해야 하는 것을 의미합니다. 그리고 분홍색
으로 칠해져 있는 것은 개발자가 직접 Spring Bean
으로 등록해야 하는 것을 의미합니다. 예를들어, @Controller
를 통해서 직접 Controller Class를 만든 후에 @Controller로 Bean으로 등록하는 것을 의미합니다.
@Controller가 붙어 있을 때 실행되는 순서를 정리하면 아래와 같습니다.
-
제일 먼저 볼 것은
DispatcherServlet
인데요. 중앙에 위치한DispatcherServlet
은 모든 연결을 담당합니다. 먼저 클라이언트(브라우저)로 요청이 들어오면 그 요청을 처리할 Controller 객체를 검색합니다. -
이 때
DispatcherServlet
은 직접 검색하지 않고HandlerMapping
이라는 빈 객체에서 컨트롤러 검색을 요청합니다. -
DispatcherServlet
은HandlerMapping
이 찾아준 컨트롤러 객체를 처리할 수 있는HandlerAdapter
Bean에게 요청 처리를 위임합니다. -
그리고
HandlerAdapter
는 DispatcherServlet과 Handler 객체 사이의 변환을 알맞게 처리해 줍니다. 즉, 컨트롤러의 알맞은 메소드를 호출해서 요청합니다. -
4번의 결과를
DispatcherServlet
에게 return 합니다. -
HandlerAdapter
는 컨트롤러의 처리 결과를ModelAndView
라는 객체로 변환해서DispatcherServlet
에 return 합니다. -
HandlerAdapter
로 부터 컨트롤러의 요청 처리 결과를ModelAndView
로 받으면DispatcherServlet
은 결과를 보여줄 뷰를 찾기 위해ViewResolver
Bean 객체를 사용합니다. -
응답을 생성하기 위해 JSP를 사용하는
ViewResolver
는 매번 새로운 View 객체를 생성해서DispatcherServlet
에 return 합니다.
@RestController
public class HelloController {
@GetMapping
public String hello() {
return "Gyunny Hello";
}
}
위처럼 @RestController
를 사용하는 컨트롤러 API에 접근할 때 내부적으로 어떤 일이 일어나는지 디버깅을 해보면서 알아보겠습니다.
먼저 DispatcherServlet
에 doService
메소드 첫 번째 줄에 Break Point
를 찍어놓은 후에 디버그 모드로 실행하고, 위에서 작성한 http://localhost:8080
에 요청을 보내보겠습니다. (참고로 디버거 모드에서 F7은 step into로 해당 메소드 내부 코드로 가는 것이고, F8은 다음 줄로 실행하는 것입니다.)
F8을 통해서 계속 다음으로 가다 보면 doDispatch()
메소드가 나오는데요. 여기서 F7
을 통해서 해당 메소드 내부로 가보겠습니다.
메소드 내부 코드를 보면 getHandler
가 보이는데요. 이름으로 추측해보면 위의 그림에서 보았던 HandlerMapping
과 관련된 곳인 것 같습니다.
메소드 내부 코드를 보면 HandlerMappings
관련 코드가 보입니다.
HandlerMappings
변수에 저희가 아무런 설정을 하지 않아도 위와 같이 6개의 HandlerMapping
이 존재하는 것을 볼 수 있습니다. 어떤 핸들러가 사용되는지 보기 위해서 F8로 다음 코드로 계속 이동해보겠습니다.
for문을 돌다보면 RequestMappingHandlerMapping
이 사용된 후에 for문이 종료되는 것을 볼 수 있습니다. 이렇게 HandlerMapping
을 통해서 컨트롤러 Bean 객체를 DispatcherServlet
에게 전달했을 것입니다.
그리고 F8을 누르면서 다음으로 가보면 getHandlerAdapter
를 통해서 위에서 찾은 Handler
를 실행할 수 있는 Adapter
를 찾아오는 것을 볼 수 있습니다.
메소드 내부로 들어가게 되면 이번에도 for문을 통해서 사용할 Adapter를 찾는 과정이 진행되는데요.
Adapter는 기본적으로 4개가 존재하는 것을 볼 수 있습니다.
디버깅을 계속 진행해보면, RequestMappingHandlerAdapter
가 선택되어 실행되는 것을 확인할 수 있습니다. 이제 Handler를 실행할 수 있는 HandlerAdpater
를 찾았습니다.
그리고 위의 코드에서 HandlerAdapter
를 이용해서 요청을 처리하게 되는데요. 좀 더 보기 위해서 F8을 통해서 다음으로 계속 간 후에, 위의 코드에서 다시 F7
을 통해서 내부 코드로 들어가보겠습니다.
여기서도 handleInternal
내부 코드로 가기 위해서 F7을 누르겠습니다.
그리고 F8을 통해서 다음으로 진행되는 코드를 보면 invokeHandlerMethod
가 보입니다. 여기서 자바의 리플렉션을 사용해서 Controller
의 메소드를 실행하게 되는 것입니다.
여기서 이미 handlerMethod
에 실행하고자 하는 Controller Method가 존재하는 것을 볼 수 있습니다.
그리고 다음 F8을 통해서 이동하면 Controller Method
로 이동하는 것을 볼 수 있습니다. 계속 다음으로 이동해보겠습니다.
그리고 return
값이 예상할 수 있듯이 String
인 것도 확인할 수 있습니다.
그리고 F8을 누르면서 다음으로 이동하면 returnValueHandlers.handleReturnValue
가 존재하는 것을 볼 수 있습니다. 현재 Controller는 @Controller를 사용하는 것이 아니라 @RestController를 사용하기 때문에 return 에는 여러가지 값이 들어갈 수 있습니다.
위의 이름이 HandlerMethodReturnValueHandler
인 것을 보면 return 값의 타입에 따라 어떤 것을 선택할 지 정하는 것처럼 보이는데요. 이것도 디버깅을 해보면 RequestResponseBodyMethodProcessor
이 사용됩니다.
그리고 계속 쭉 F8로 실행을 하다 보면 위의 코드까지 오게 되는데요. mv는 ModelAndView
타입의 변수인데, 위의 사진을 보면 null
인 것을 알 수 있습니다. 이유는 현재는 @Controller
가 아닌 @RestController
를 사용하기 때문에 view
를 찾는 과정이 필요 없기 때문입니다.
이렇게 지금까지 @RestController
가 존재하는 Controller에 요청이 오면 Spring MVC에 내부적으로 어떻게 동작하는지 디버거를 통해서 알아보았습니다. 간단하게? 본 것 같은데도 내부적으로 정말 복잡하게 이루어지는 것을 볼 수 있습니다.
@Controller
public class HelloController {
@GetMapping
public String hello() {
return "login";
}
}
이번에는 @Controller
어노테이션을 사용해서 View
를 찾는 과정도 Debug
를 통해서 알아보겠습니다. 위에서 정리한 내용과 비슷하기 때문에 차이점이 있는 부분만 정리해보겠습니다.
똑같이 진행되다가 위에서 보았던 HandlerMethodReturnValueHandler
를 보면 이번에는 ViewNameMethodReturnValueHandler
가 사용되는 것을 볼 수 있습니다.
그리고 이번에는 @Controller
이기 때문에 ModelAndView
에 제가 사용하는 login
이라는 View 이름이 보이는 것을 볼 수 있습니다.
또한 @Controller
이기 때문에 ModelAndView
가 null이 아니라 값을 가지고 있는 것도 볼 수 있습니다.
그리고 계속 디버깅을 해보면 위와 같이 View
와 관련된 코드를 만날 수 있는데요. 여기서 뭔가 ViewResolver
와 관련된 것이 있을 것 같아서 좀 더 자세히 보았습니다.
그러면 위와 같이 ViewResolver
가 사용되는 것을 볼 수 있습니다. 코드를 자세히 이해할 순 없지만.. 그래도 디버깅을 계속 돌려보았습니다.
그러면 ViewResolver
인터페이스 구현체인 ContentNegotiatingViewResolver
가 사용되면서 View를 찾는 과정을 볼 수 있습니다.
이렇게 까지 계속 디버깅을 통해서 따라가본 이유는 내부 코드를 완벽하게 이해할 순 없지만, 대략적으로 위에서 정리했던 내용대로 실행되는 것인지 알아보면서 전체적인 흐름을 정리하기 위해서 보았습니다.