
회사에서 동료 책임님과 함께 Controller unit test 를 개선시킨 경험이 있다. 이때, 오랜만에 마주한 Spring 내부 구조는 생소했던 기억이 있다.
따라서, 관련 개념인 DispathcerSevlet, 그리고 Handler, View 관련 개념을 정리해보고자 한다.
전체적인 프로세스는 다음과 같다.
1) 사용자가 HTTP 요청을 보낸다.
2) Spring boot 는 Dispatcher Servlet 을 Servlet 으로 자동 등록 후, 모든 경로인 urlPatterns="/") 에 대해 등록한다.
Concept
Servlet 이란 Java 기반의 server 측 프로그램으로, HTTP 요청과 응답을 처리해 웹 페이지를 동적으로 생성하기 위한 데이터 입출력 창구(인터페이스)이며, 실제 내용(동적 페이지)은 Servlet(코드)에서 처리해 이 Response에 담겨 브라우저로 전송된다.HttpServletRequest, HttpServletResponse 객체를 제공Code
DispatcherServlet.doDispatch() ➡️ FrameworkServlet ➡️ HttpServletBean ➡️ HttpServlet.service()3) Handler mapping 을 통해 요청 URL 에 매핑된 핸들러인 Controller 를 조회한다.
4) Controller 를 실행할 수 있는 Handler Adapter 를 조회한다.
5) Handler Adapter 를 실행한다.
6) Handler Adapter 가 Controller 를 실행한다.
Code
HandlerAdapter ➡️ Controller7) HandlerAdapter 가 Controller 가 반환하는 정보를 ModelAndView 로 변환해서 반환한다.
8) ViewResolver 를 조회 후 실행한다.
9) ViewResolver 는 뷰의 논리 이름(컨트롤러가 반환하는 화면 이름) 을 물리 이름(실제 파일 경로와 이름) 으로 바꾸고, 렌더링 역할을 담당하는 View Object를 반환한다.
Concept
Randering 이란, 서버에서 동적으로 생성된 화면(HTML, JSON 등) 데이터를 사용자(웹 브라우저 등)가 볼 수 있게 출력하는 작업이다.10) View (object) 를 통해 렌더링한다.
Concept
HTTP 응답 본문에 넣어 클라이언트로 전송하는 역할을 한다. Spring MVC 구조를 실무에선 어떻게 직접적으로 맞닥뜨릴 수 있을까?
그리 멀지 않게 발견할 수 있다.
바로, HTTP 요청 처리를 담당하는 핸들러인 Controller Unit Test 에서 볼 수 있다.
만약, Controller Unit Test 가 실패했을 경우 우리는 다음과 같은 화면을 심심치 않게 마주할 수 있다.

로그만 보더라도 이미 원인을 추측할 수 있지만, 우리가 볼 것은 해당 예외가 던져진 구간이 HttpServlet.service()인 점이다.
DispatcherServlet 은 부모 클래스에서 HttpServlet 을 상속 받아서 사용하고, Servlet 으로 동작하기에 최종적으로 호출된 부분은 HttpServlet.service() 이다.

그리고 HttpServlet 의 요청 정보인 Request 에 대해 친절하게 알려준다.

그리고 실질적인 요청을 처리할 수 있는 핸들러에 대한 정보도 알려준다.

실질적인 예외의 원인을 알려주는 부분이다.

그리고, 최종적으로 DispatcherSevlet 이 반환하려고 했던 Response 에 대한 정보도 알려준다.
Controller 관련 테스트에서 에러가 터졌을 경우, 핵심은 이 부분이다.
Simple Example
NoSuchBeanDefinitionExceptionorg.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.example.demo.controller.ExampleController' available
위 예외는 실실적인 요청을 처리할 핸들러인 Controller 를 직접 조회(테스트)하는데, 해당 컨트롤러가 스프링 빈으로 등록되지 않거나, 의존성이 주입되지 않은 경우 발생한다.
UnsatisfiedDependencyExceptionorg.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'exampleController' defined in file [...] Unsatisfied dependency expressed through constructor parameter 0
위 예외는 Controller가 필드 또는 생성자를 통해 의존하는 Service 및 Repository Bean 이 없어서 매핑 자체가 실패하는 경우 발생한다.
@MockBean 을 통해 Spring 컨테이너에 mock 객체를 Bean 으로 등록하고 DI로 주입하면 된다.NoHandlerFoundExceptionorg.springframework.web.servlet.NoHandlerFoundException: No handler found for GET /invalid-path
예시로 알려준 예외이다.
즉, 간단하게 Controller Unit Test 에서도 Spring MVC 의 구조를 이해하고 있어야 어떤 원인으로 테스트가 실패했는지 파악할 수 있기 때문에 빠르게 감을 잡을 수 있다.
다음은 Spring Filter 와도 연관지어서, 개선한 Spring Controller Unit Test 에 대해 저술해보도록 하겠다!
성능 테스트와 구현도 중요하지만, 이를 뒷받침하는 기본기도 중요하다는 걸 느끼는 요즘이다.