스프링 MVC - 구조 이해

뚝딱이·2022년 7월 29일
0

스프링 MVC

목록 보기
5/23

스프링 MVC 전체 구조

앞서 스프링 프레임워크를 만들어보았다. 프론트 컨트롤러 패턴으로 구현해보았는데 스프링 MVC 또한 프론트 컨트롤러 패턴으로 구현되어있으며 FrontController의 역할이 바로 DispatcherServlet이다.

DispatcherServlet -> FrameworkServlet -> HttpServletBean -> HttpServlet

위와 같은 상속관계를 가진다. 따라서 서블릿으로 동작한다.

스프링 MVC는 서블릿이 호출되면 FrameworkServlet에서 오버라이드한 service, 즉 FrameworkServlet.service() 가 호출되면서 여러 메서드와 함께 핵심인 DispacherServlet.doDispatch() 가 호출된다. doDispatch에 대해 분석해보자.

protected void doDispatch(HttpServletRequest request, HttpServletResponse 
response) throws Exception {
	HttpServletRequest processedRequest = request;
	HandlerExecutionChain mappedHandler = null;
	ModelAndView mv = null;
	// 1. 핸들러 조회
	mappedHandler = getHandler(processedRequest);
	if (mappedHandler == null) {
		noHandlerFound(processedRequest, response);
		return;
	}
	// 2. 핸들러 어댑터 조회 - 핸들러를 처리할 수 있는 어댑터
	HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
	// 3. 핸들러 어댑터 실행 -> 4. 핸들러 어댑터를 통해 핸들러 실행 -> 5. ModelAndView 반환
	mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
	processDispatchResult(processedRequest, response, mappedHandler, mv,
dispatchException);
}

private void processDispatchResult(HttpServletRequest request,
HttpServletResponse response, HandlerExecutionChain mappedHandler, ModelAndView 
mv, Exception exception) throws Exception {
	// 뷰 렌더링 호출
	render(mv, request, response);
}
    
protected void render(ModelAndView mv, HttpServletRequest request,
HttpServletResponse response) throws Exception {
	View view;
	String viewName = mv.getViewName();
	// 6. 뷰 리졸버를 통해서 뷰 찾기, 7. View 반환
	view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
	// 8. 뷰 렌더링
	view.render(mv.getModelInternal(), request, response);
}

동작 원리

  1. 핸들러 조회: 핸들러 매핑을 통해 요청 URL에 매핑된 핸들러(컨트롤러)를 조회한다.

  2. 핸들러 어댑터 조회: 핸들러를 실행할 수 있는 핸들러 어댑터를 조회한다.

  3. 핸들러 어댑터 실행: 핸들러 어댑터를 실행한다.

  4. 핸들러 실행: 핸들러 어댑터가 실제 핸들러를 실행한다.

  5. ModelAndView 반환: 핸들러 어댑터는 핸들러가 반환하는 정보를 ModelAndView로 변환해서 반환한다.

  6. viewResolver 호출: 뷰 리졸버를 찾고 실행한다.
    (JSP의 경우: InternalResourceViewResolver 가 자동 등록되고, 사용된다.)

  7. View 반환: 뷰 리졸버는 뷰의 논리 이름을 물리 이름으로 바꾸고, 렌더링 역할을 담당하는 뷰 객체를 반환한다.
    (JSP의 경우 InternalResourceView(JstlView) 를 반환하는데, 내부에 forward() 로직이 있다.)

  8. 뷰 렌더링: 뷰를 통해서 뷰를 렌더링 한다.

웹 애플리케이션을 만들 때 필요로 하는 대부분의 기능이 이미 다 구현되어 있다.
그래도 이렇게 핵심 동작방식을 알아두어야 향후 문제가 발생했을 때 어떤 부분에서 문제가 발생했는지 쉽게 파악하고, 문제를 해결할 수 있다. 그리고 확장 포인트가 필요할 때, 어떤 부분을 확장해야 할지 감을 잡을 수 있다.


핸들러 매핑과 핸들러 어댑터

BeanNameUrlHandlerMapping

@Component("/springmvc/old-controller")//스프링 빈의 이름
public class OldController implements Controller {
    @Override
    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
        System.out.println("OldController.handleRequest");
        return new ModelAndView("new-form");
    }
}

어노테이션 기법 전에 사용하던 컨트롤러 방식이다.

이 컨트롤러가 호출되기 위해서는 다음의 두가지가 필요하다.

  • HandlerMapping(핸들러 매핑)
    핸들러 매핑에서 이 컨트롤러를 찾을 수 있어야 한다.

    0순위 = RequestMappingHandlerMapping : 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
    1순위= BeanNameUrlHandlerMapping : 스프링 빈의 이름으로 핸들러를 찾는다.

  • HandlerAdapter(핸들러 어댑터)
    핸들러 매핑을 통해서 찾은 핸들러를 실행할 수 있는 핸들러 어댑터가 필요하다.

    0순위 = RequestMappingHandlerAdapter : 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
    1순위 = HttpRequestHandlerAdapter : HttpRequestHandler 처리
    2순위 = SimpleControllerHandlerAdapter : Controller 인터페이스(애노테이션X, 과거에 사용)
    처리

동작 원리

  1. 핸들러 매핑으로 핸들러 조회

    1. HandlerMapping 을 순서대로 실행해서, 핸들러를 찾는다.
    2. 이 경우 빈 이름으로 핸들러를 찾아야 하기 때문에 이름 그대로 빈 이름으로 핸들러를 찾아주는 BeanNameUrlHandlerMapping 가 실행에 성공하고 핸들러인 OldController 를 반환한다.
  2. 핸들러 어댑터 조회

    1. HandlerAdapter 의 supports() 를 순서대로 호출한다.
    2. SimpleControllerHandlerAdapter 가 Controller 인터페이스를 지원하므로 대상이 된다.
  3. 핸들러 어댑터 실행

    1. 디스패처 서블릿이 조회한 SimpleControllerHandlerAdapter 를 실행하면서 핸들러 정보도 함께넘겨준다.
    2. SimpleControllerHandlerAdapter 는 핸들러인 OldController 를 내부에서 실행하고, 그 결과를 반환한다

HttpRequestHandler

@Component("/springmvc/request-handler")
public class MyHttpRequestHandler implements HttpRequestHandler {
    @Override
    public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("MyHttpRequestHandler.handleRequest");
    }
}
  1. 핸들러 매핑으로 핸들러 조회
    1. HandlerMapping 을 순서대로 실행해서, 핸들러를 찾는다.
    2. 이 경우 빈 이름으로 핸들러를 찾아야 하기 때문에 이름 그대로 빈 이름으로 핸들러를 찾아주는 BeanNameUrlHandlerMapping 가 실행에 성공하고 핸들러인 MyHttpRequestHandler 를 반환한다.
  2. 핸들러 어댑터 조회
    1. HandlerAdapter 의 supports() 를 순서대로 호출한다.
    2. HttpRequestHandlerAdapter 가 HttpRequestHandler 인터페이스를 지원하므로 대상이 된다.
  3. 핸들러 어댑터 실행
    1. 디스패처 서블릿이 조회한 HttpRequestHandlerAdapter 를 실행하면서 핸들러 정보도 함께 넘겨준다.
    2. HttpRequestHandlerAdapter 는 핸들러인 MyHttpRequestHandler 를 내부에서 실행하고, 그 결과를 반환한다.

뷰 리졸버

@Component("/springmvc/old-controller")
public class OldController implements Controller {
	@Override
   	public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
   		System.out.println("OldController.handleRequest");
   		return new ModelAndView("new-form");
 }
}

위의 코드를 실행하면 OldController.handleRequest는 출력되어 컨트롤러가 실행됨을 알 수 있지만, 웹 브라우저엔 에러 페이지가 뜬다. 이것은 new-form이라는 논리이름을 물리이름으로 바꿔서 적당한 view를 찾아주지 못했기 때문이다.

따라서 prefix와 suffix를 추가해주자. apllication.properties에 추가하면 된다.

spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp

스프링 부트는 InternalResourceViewResolver 라는 뷰 리졸버를 자동으로 등록하는데, 이때 application.properties 에 등록한 spring.mvc.view.prefix , spring.mvc.view.suffix 설정 정보를 사용해서 등록한다

스프링 부트가 자동 등록하는 뷰 리졸버

1 = BeanNameViewResolver : 빈 이름으로 뷰를 찾아서 반환한다. (예: 엑셀 파일 생성 기능에 사용)
2 = InternalResourceViewResolver : JSP를 처리할 수 있는 뷰를 반환한다.

동작 원리

  1. 핸들러 어댑터 호출
    핸들러 어댑터를 통해 new-form 이라는 논리 뷰 이름을 획득한다.
  2. ViewResolver 호출
    new-form 이라는 뷰 이름으로 viewResolver를 순서대로 호출한다.
    BeanNameViewResolver 는 new-form 이라는 이름의 스프링 빈으로 등록된 뷰를 찾아야 하는데 없다.
    InternalResourceViewResolver 가 호출된다.
  3. InternalResourceViewResolver
    이 뷰 리졸버는 InternalResourceView 를 반환한다.
  4. 뷰 - InternalResourceView
    InternalResourceView 는 JSP처럼 포워드 forward() 를 호출해서 처리할 수 있는 경우에 사용한다.
  5. view.render()
    view.render() 가 호출되고 InternalResourceView 는 forward() 를 사용해서 JSP를 실행한다.

(참고)
다른 뷰는 실제 뷰를 렌더링하지만, JSP의 경우 forward() 통해서 해당 JSP로 이동(실행)해야 렌더링이 된다. JSP를 제외한 나머지 뷰 템플릿들은 forward() 과정 없이 바로 렌더링 된다.

스프링 MVC

스프링은 @RequestMapping 애노테이션을 사용한 컨트롤러로 매우 유연하고 실용적인 컨트롤러를 만들 수 있다.

RequestMapping는 아래의 매핑정보와 어댑터를 사용한다.

  • RequestMappingHandlerMapping
  • RequestMappingHandlerAdapter

@RequestMapping을 이용하여 기존의 코드들을 변경해보자.

@Controller
public class SpringMemberFormControllerV1 {

    @RequestMapping("/springmvc/v1/members/new-form")
    public ModelAndView process(){
        return new ModelAndView("new-form");
    }
}
  • @Controller : 앞서 배웠듯이 Component를 내부에 가지고 있으므로 스프링이 자동으로 스프링 빈으로 등록한다. 스프링 MVC에서 애노테이션 기반 컨트롤러로 인식한다.

  • @RequestMapping : springmvc/v1/members/new-form URL이 호출되면 해당 메서드가 호출된다. 메서드 명은 임의로 지으면 된다

  • ModelAndView : 스프링 프레임 워크를 만들 때 사용했던 ModelView를 생각하면 된다. 모델과 뷰 정보를 담아서 반환한다. 따라서 위의 form에서는 담을 모델정보가 없어 담지 았았지만 viewName인 new-form을 담아서 반환했다.

@Controller는 @Component@RequestMapping과도 같다. RequestMapping이 빈으로 등록된 것을 매핑하기 때문에 사전에 빈으로 등록되어있어야한다. 따라서 @Component를 통한 컴포넌트 스캔 뿐만아니라 직접 빈 등록을 통해서도 사용가능하다.

@Controller
public class SpringMemberSaveControllerV1 {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @RequestMapping("/springmvc/v1/members/save")
    public ModelAndView process(HttpServletRequest request, HttpServletResponse response){

        String username = request.getParameter("username");
        int age = Integer.parseInt(request.getParameter("age"));

        Member member = new Member(username, age);
        memberRepository.save(member);

        ModelAndView mv = new ModelAndView("save-result");
        mv.addObject("member",member);

        return mv;
    }

회원 저장 코드이다. 요청 정보가 필요하므로 parameter로 request와 reponse가 온 것을 볼 수 있다.
mv.addObject("member",member);로 간편하게 ModelAndView에 Model을 추가할 수 있다.


컨트롤러 통합

@RequestMapping은 위에서 보듯이 메서드 단위에 적용되었다. 따라서 모든 메서드를 한 클래스에 통합할 수 있다.

@Controller
@RequestMapping("/springmvc/v2/members")
public class SpringMemberControllerV2 {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @RequestMapping("/new-form")
    public ModelAndView newForm(){
        return new ModelAndView("new-form");
    }

    @RequestMapping("/save")
    public ModelAndView save(HttpServletRequest request, HttpServletResponse response){

        String username = request.getParameter("username");
        int age = Integer.parseInt(request.getParameter("age"));

        Member member = new Member(username, age);
        memberRepository.save(member);

        ModelAndView mv = new ModelAndView("save-result");
        mv.addObject("member",member);

        return mv;
    }
}

위의 v1에선 @RequestMapping안에 /springmvc/v2/members부분이 중복되었다. 따라서 위와 같이 중복되는 부분을 생략하고 클래스 단위의 @RequestMapping에 중복되는 부분을 넣어줄 수 있다.


실용적인 방식

ModelAndView를 개발자가 생성해서 반환하는것은 불편하다. 따라서 Model 파라미터를 받아 Model에 넣고 viewName만 String형식으로 반환받는다.

@Controller
@RequestMapping("/springmvc/v3/members")
public class SpringMemberControllerV3 {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @GetMapping("/new-Form")
    public String newForm(){
        return "new-form";
    }

    @PostMapping("/save")
            public String save(
            @RequestParam("username") String username,
            @RequestParam("age")int age, //타입 캐스팅도 알아서 해줌
            Model model
    ){
        Member member = new Member(username, age);
        memberRepository.save(member);

        model.addAttribute("member", member);

        return "save-result";
    }

따라서 위와 같이 더 간편하게 바뀐 것을 볼 수 있다.
코드를 보면 바뀐 점이 더 있는데, 바로 @GetMapping@PostMapping이다.
그 전에는 GET이나 POST등 HTTP 메서드를 지정하지 않아 GET이나 POST로 요청이 와도 다 받아줬었다. 하지만 이건 좋은 코드가 아니다 따라서 위와 같이 GET이나 POST등 메서드를 지정할 수 있는데 위의 코드는 아래의 코드를 줄인 것이다.
@RequestMapping(value = "/new-form", method = RequestMethod.GET)

실제로 GetMapping의 안을 들여다 보면, @RequestMapping(method = RequestMethod.GET)가 들어있는 것을 확인할 수 있다.

또 바뀐 점이 있다면

@RequestParam("username") String username,
@RequestParam("age")int age, //타입 캐스팅도 알아서 해줌

부분일텐데, 스프링은 HTTP 요청 파라미터를 @RequestParam 으로 받을 수 있다.


출처 : 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술

profile
백엔드 개발자 지망생

0개의 댓글