본문 바로가기
개발자 일지/Spring

[인프런 김영한 로드맵4]스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술(5)

by 네빌링 2022. 6. 14.
반응형

-이전 시간에 단계별로 만든 'MVC 프레임워크'와 '스프링 MVC'를 비교해본다.

-스프링MVC에서 핸들러매핑, 뷰리졸버가 어떻게 사용되는지 확인한다.

-컨트롤러 사용시 컨트롤러 인터페이스 방식과 애노테이션 방식을 학습하며 실무에서 사용되는 법을 학습한다.

-모든 소스는 깃허브에서 관리한다.(https://github.com/coderahn/Spring-Lecture4)


5.스프링 MVC 구조 이해

 

1.스프링 MVC 전체 구조

 

직접만든 MVC프레임워크와 스프링 MVC를 비교해보자.

 

  • FrontController -> DispatcherServlet
  • handlerMappingMap -> HandlerMapping
  • MyHandlerAdapter -> HandlerAdapter
  • ModelView -> ModelAndView
  • viewResolver -> ViewResolver(인터페이스)
  • MyView -> View(인터페이스)

 

 

MVC 프레임워크와 마찬가지로 스프링 MVC도 프론트 컨트롤러 패턴을 사용하며 DispatcherServlet이 그 역할을 한다.

 

DispatcherServlet은 스프링부트가 톰캣 가동시 자동으로 등록되며 모든 경로를 받도록 되어 있다.(urlPattern="/")

즉, 내부적으로 HttpServlet을 상속 받아 사용한다.

 

DispatcherServlet의 상속 구조를 따라가다 보면 HttpServlet이 있음을 확인 가능

 

서블릿이 호출되면 HttpServlet의 service()가 실행된다. service()는 내부적으로 GET, POST 등을 판단 후 doGet(), doPost()등을 실행한다.

 

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

 

과거에는 Controller 인터페이스가 사용되었다. 이 인터페이스로 핸들러매핑, 핸들러어댑터를 알아본다.

 

아래와 같이 Controller 인터페이스를 상속한 OldController를 만들어보자.

 

[OldController.java]

//@Controller 이전에 사용한 Controller 인터페이스
//@Component : 스프링 빈 이름을 URL로 맞추어 사용 가능
@Component("/springmvc/old-controller")
public class OldController implements Controller {
    @Override
    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
        System.out.println("OldController.handleRequest");
        
        //뷰리졸버를 만들어주자 -> application.properties
        return new ModelAndView("new-form");
    }
}

 

 

@Component에 이름을 주어 /springmvc/old-controller라는 이름의 스프링 빈을 만들어 등록했다.

 

http://localhost:8080/springmvc/old-controller로 접속 가능하다. 논리명 "new-form"으로 리턴했는데 호출이 정상적으로 된다.

 

이 컨트롤러가 호출되는 방법은 다음과 같다.

 

우선 핸들러매핑을 통해 컨트롤러를 찾을 수 있어야 한다. 이 컨트롤러는 스프링 빈이므로 스프링 빈 이름으로 핸들러매핑한다.

 

그리고 핸들러 어댑터를 통해 찾은 핸들러를 실행할 수 있는 어댑터가 필요하다. 여기서는 Controller 인터페이스를 실행할 수 있는 핸들러어댑터가 필요하다.

 

스프링은 기본적으로 대부분의 핸들러매핑, 핸들러어댑터를 구현해 두었다. 스프링부트는 이것들을 자동으로 등록한다.

 

  • HandlerMapping
    • 0순위 : RequestMappingHandlerMapping / 애노테이션 기반 컨트롤러인 @RequestMapping에서 사용
    • 1순위 : BeanNameUrlHandlerMapping / 스프링 빈 이름으로 핸들러를 찾음 -> OldController
  • HandlerAdapter
    • 0순위 : RequestMappingHandlerAdapter / 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
    • 1순위 : HttpRequestHandlerAdapter / HttpRequestHandler 처리
    • 2순위 : SimpleControllerHandlerAdapter / Controller 인터페이스 처리 -> OldController

 

매핑은 순위별로 진행한다. 0순위에서 처리 불가하면 1순위로 넘어가는 식이다.

 

디스패처 서블릿이 처음에 핸들러매핑에서 핸들러를 찾고, 핸들러어댑터를 찾아 handle()을 실행한다.

 

핸들러 조회 -> 핸들러어댑터조회 -> 핸들러어댑터 실행 순이다.

 

가장 우선순위가 높은 핸들러매핑과 핸들러어댑터는 RequestMappingHandlerMapping이다. @RequestMapping 애노테이션 컨트롤러를 지원한다.

 

3.뷰리졸버

 

위의 코드에서 View를 사용할 수 있도록 다음과 같은 코드가 있다.

 

return new ModelAndView("new-form");

 

실행하면 Whitelabel Error Page가 나오는데 뷰리졸버 설정을 해줘야 한다. 스프링부트는 InternalResourceViewResolver라는 뷰리졸버를 자동등록한다. 이 때 application.properties에 prefix, suffix을 설정해주어 사용 가능하다.

 

#스프링부트 뷰리졸버(InternalResourceViewResolver 자동 등록 / 아래 prefix, suffix 사용해서 등록)
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp

 

그러면 실제로 "/WEB-INF/views/new-form.jsp"로 실행이 된다.

 

스프링부트는 자동 등록하는 뷰리졸버들이 있다.

 

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

 

위의 OldController 실행 순서는 간단하게 다음과 같다.

 

  1. 핸들러어댑터 호출 : 핸들러어댑터를 통해 new-form이라는 논리뷰이름을 획득
  2. ViewResolver 호출 : new-form이라는 뷰 이름으로 viewResolver를 순서대로 호출(new-form 빈이름 매칭 없으므로 InternalResourceViewResolver) 호출
  3. InternalResourceViewResolver가 InternalResourceView 반환
  4. view.rendor()가 호출되어 InternalResourceView의 forward() 실행하여 JSP 실행

 

참고로 InternalResourceViewResolver는 JSTL 라이브러리가 있으면 InternalResourceView를 상속받은 JstlView를 반환한다.

 

라이브러리를 까보니 아래와 같이 구현되어 있다.

 

[InternalResourceViewResolver.java]

public class InternalResourceViewResolver extends UrlBasedViewResolver {

	//jstl클래스가 존재하는지, 로드할 수 있는지 체크
	private static final boolean jstlPresent = ClassUtils.isPresent(
			"javax.servlet.jsp.jstl.core.Config", InternalResourceViewResolver.class.getClassLoader());

	@Nullable
	private Boolean alwaysInclude;


	/**
	 * Sets the default {@link #setViewClass view class} to {@link #requiredViewClass}:
	 * by default {@link InternalResourceView}, or {@link JstlView} if the JSTL API
	 * is present.
	 */
     
    //스프링부트 실행시 Bean등록되면서 실행
	public InternalResourceViewResolver() {
		Class<?> viewClass = requiredViewClass();
		if (InternalResourceView.class == viewClass && jstlPresent) {
			viewClass = JstlView.class;
		}
		setViewClass(viewClass);
	}
    
    //...
    
    @Override
	protected Class<?> requiredViewClass() {
		return InternalResourceView.class;
	}
 }

 

4.스프링 MVC - 시작하기

 

스프링이 제공하는 컨트롤러는 애노테이션 기반으로 유연하며 실용적이다. @RequestMapping이 대표적이다.

 

@RequestMapping은 앞서 보았듯이 가장 우선순위가 높은 핸들러매핑, 핸들러어댑터를 실행한다.

 

  • RequestMappingHandlerMapping
  • RequestMappingHandlerAdapter

 

이제 애노테이션 기반의 컨트롤러를 만들어볼텐데, v1,v2,v3 순서대로 만든다. 그러나 v1은 v2를 각각만든 것과 거의 동일하므로 바로 v2로 넘어간다.

 

[SpringMemberControllerV2.java]

@Controller
//@Controller는 @Component + @ReuqestMapping이다. @RequestMappingHandlerMapping 대상이됨.
@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.getModel().put("member", member);
        mv.addObject("member", member);

        return mv;
    }

    @RequestMapping
    public ModelAndView members() {
        List<Member> members = memberRepository.findAll();
        ModelAndView mv = new ModelAndView("members");
        //mv.getModel().put("members", members);
        mv.addObject("members", members);

        return mv;
    }
}

 

위에 주석에서 언급했듯이 @Controller는 내부에 @Component가 있어 컴포넌트 스캔 대상이 되며 @RequestMapping도 존재하여 요청 정보를 매핑한다. 

 

즉, RequestMappingHandlerMapping은 스프링 빈 중에 @RequestMapping이나 @Controller 컨트롤러를 매핑 정보로 인식한다.

 

다음 코드도 동일하게 동작한다.

 

@Component
@RequestMapping
public class SpringMemberControllerV2 {
	//...
}

 

그리고 Model에 데이터 추가시 getModel().put()보다 addObject()로 편하게 사용할 수 있다.

 

다음으로 V3은 더욱 편리하게 쓸 수 있도록 개선하였다. 다음과 같다.

 

[SpringMemberControllerV3.java]

@Controller
@RequestMapping("/springmvc/v3/members")
public class SpringMemberControllerV3 {
    private MemberRepository memberRepository = MemberRepository.getInstance();

    //@RequestMapping(value = "/new-form", method = RequestMethod.GET)
    @GetMapping("/new-form")
    public String newForm() {
        return "new-form";
    }

    //@RequestMapping(value = "/save", method = RequestMethod.POST)
    @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";
    }

    //@RequestMapping(method = RequestMethod.GET)
    @PostMapping
    public String members(Model model) {
        List<Member> members = memberRepository.findAll();
        model.addAttribute("members", members);

        return "members";
    }
}

 

우선 논리명 반환시 viewName을 직접 반환할 수 있도록 변경하였다.

 

그리고 Model파라미터를 추가해서 사용할 수 있는데 save(), members()를 보면 Model을 파라미터로 받는 것을 확인할 수 있다. 이 model을 사용해 쉽게 JSP에 보낼 데이터를 추가할 수 있다.

 

그리고 @RequestParam을 사용하여 http 요청 파라미터를 받을 수 있다. 굳이 HttpServletRequest의 getParameter("username") 등을 사용할 필요가 없다.

 

마지막으로 @RequestMapping 대신에 GET, POST 여부에 따라 @GetMapping, @PostMapping을 사용할 수 있다. 

반응형