Spring

[Spring MVC] 스프링 MVC (3)

챛채 2023. 5. 15. 20:04
  • 직접 구현한 MVC 프레임 워크와 스프링 MVC 비교
  • 스프링 MVC에서 핸들러매핑, 뷰리졸버 사용 확인
  • 컨트롤러 사용시에 인터페이스 방식과 애노테이션 방식을 살펴볼 수 있음

5. 스프링 MVC 구조 이해

 

1) 스프링 MVC 전체 구조

직접 만든 MVC 프레임워크 구조

 

스프링 MVC 구조

 

직접 만든 프레임워크 -> 스프링 MVC 비교

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

스프링 MVC도 마찬가지로 프론트 컨트롤러 패턴을 사용하고 있으며 DispatcherServlet이 그 역할을 한다.

 

DispatcherServlet은 스프링부트가 톰캣을 가동할 때 자동으로 등록되며 모든 경로(urlPatterns="/")에 대해서 매핑한다.

즉 ,HttpServlet을 상속 받아서 사용하고 서블릿으로 동작한다.

서블릿이 호출되면 HttpServlet에서 service()가 호출된다. 스프링 MVC는 DispatcherServlet의 부모인 FrameworkServlet에서 service()를 오버라이드 해두었기 때문이다.

FrameworkServlet.service()를 시작으로 여러 메서드들이 호출되면서 내부적으로 GET, POST 등 판단 후 doGet(), doPost()를 실행한다.

 

Spring MVC 동작 순서

  1. 핸들러 조회 : 핸들러 매핑을 통하여 요청 URL에 매핑된 핸들러(컨트롤러)를 조회한다.
  2. 핸들러 어댑터 조회 : 핸들러를 실행할 수 있는 핸들러 어댑터를 조회한다.
  3. 핸들러 어댑터 실행 : DispatcherServlet이 handle(handler)를 통해 핸들러 어댑터를 실행한다.
  4. 핸들러 실행 : 핸들러 어댑터가 실제 핸들러를 실행한다.
  5. ModelAndView 반환 : 핸들러 어댑터는 핸들러가 반환하는 정보를 ModelAndView로 변환해서 반환한다.
  6. viewResolver 호출 : 뷰 리졸버를 찾고 실행한다.
    1. JSP의 경우 : InternalResourceViewResolver가 자동 등록되고, 사용된다.
  7. View 반환 : 뷰 리졸버는 뷰의 논리 이름을 물리 이름으로 바꾸고, 렌더링 역할을 담당하는 뷰 객체를 반환한다.
    1. JSP의 경우 : InternalResourceView(JstlView)를 반환하는데, 내부에 forawrd()로직이 있다.
  8. 뷰 렌더링 : 뷰를 통해서 뷰를 렌더링 한다.

즉, 핸들러 어댑터를 통하여 핸들러(컨트롤러)가 실행되고, 핸들러 어댑터가 반환한 내용을 통해 viewResolver가 실제 view를 반환한다. 그리고 view의 render를 통하여 HTML 응답을 클라이언트에게 전달한다.

 

 

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

과거에는 Controller 인터페이스가 사용되었다. Controller 인터페이스를 상속한 OldController을 만들어보자.

[OldController.java]

//@Controller 이전에 사용한 Controller 인터페이스
@Component("/springmvc/old-controller") //스프링 빈의 이름을 URL로 맞추어 사용 가능
public class OldController implements Controller {
    @Override
    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
        System.out.println("OldController.handleRequest");
      
    }
}

@Component : 이 컨트롤러는 /springmvc/old-controller라는 이름의 스프링 빈으로 등록되었다.

빈의 이름으로 URL 매핑할 것이다. 

 

실행

  • http://localhost:8080/springmvc/old-controller
  • 콘솔에 OldController.handleRequest 이 출력되면 성공이다.

이 컨트롤러가 호출되기 위해서는 2가지가 필요한데 우선 핸들러 매핑에서 이 컨트롤러를 찾을 수 있어야한다.  이 컨트롤러는 스프링 빈이므로 스프링 빈 이름으로 핸들러 매핑한다. 

 

그리고 핸들러 매핑을 통해 찾은 핸들러를 실행할 수 있는 핸들러 어댑터가 필요하다. Controller 인터페이스를 실행할 수 있는 핸들러 어댑터를 찾고 실행해야 한다.

 

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

 

HandlerMapping

  • 0순위 : RequestMappingHandlerMapping -> 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
  • 1순위 : BeanNameUrlHandlerMapping -> 스프링 빈 이름으로 핸들러를 찾음( OldController)

HandlerAdapter

  • 0순위 : RequestMappingHandlerAdapter -> 애노테이션 기반의 컨트롤러인 @RequestMapping 사용
  • 1순위 : HttpRequestHandlerAdapter -> HttpRequestHandler 처리
  • 2순위 : SimpleControllerHandlerAdapter -> Contorller 인터페이스 처리 (OldController)

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

 

제일 먼저 핸들러 매핑을 순서대로 실행해서, 핸들러를 찾는데 빈 이름으로 핸들러를 찾아야하기 때문에 BeanNameUrlHandlerMapping가 실행에 성공하고 핸들러인 OldController을 반환해준다. 

다음 핸들러 어댑터의 supports()를 순서대로 호출하는데 SimpleControllerHandlerAdapter가 Controller 인터페이스를 지원하므로 대상이 된다.

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

 

@RequestMapping

가장 우선순위가 높은 핸들러 매핑과 핸들러 어댑터는 RequestMappingHandlerMapping, RequestMappingHandlerAdapter이다. 애노테이션 기반의 컨트롤러를 지원하는 매핑과 어댑터로 실무에서는 99.9% 이 방식의 컨트롤러를 사용한다.

 

3) 뷰 리졸버

View를 사용할 수 있도록 위의 코드에 다음 코드를 추가한다.

  return new ModelAndView("new-form"); //논리이름 ->뷰리졸버에서 물리이름을 바꾸게해야함
        //컨트롤러는 호출이 됐는데 view를 못찾아서 오류페이지 뜸 ->뷰리졸버를 만들어줘야함

        /*
        * spring.mvc.view.prefix=/WEB-INF/views/
        * spring.mvc.view.suffix=.jsp
        * 어플리케이션프로퍼티스에 설정 정보 추가 후 잘 동작
        * */

http://localhost:8080/springmvc/old-controller로 실행을 하면 Whitelabel Error Page가 나오고 콘솔에 OldController.handleRequest가 출력될 것이다. -> 컨트롤러는 정상 호출되지만 에러 발생한다.

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

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

 

 

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

1순위 : BeanNameViewResolver -> 빈 이름으로 뷰를 찾아서 반환한다. (예: 엑셀 파일 생성 기능에 사용)

2순위 : InternalResourceViewResolver -> JSP를 처리할 수 있는 뷰를 반환한다.

 

OldController의 실행 순서는 

  1. 핸들러 어댑터 호출 : 핸들러 어댑터 통해 new-form이라는 논리 뷰 이름을 획득한다. 
  2.  ViewResolver 호출 : new-form이라는 뷰 이름으로 viewResolver를 순서대로 호출한다. (BeanNameViewResolver는 new-form이라는 스프링 빈으로 등록된 뷰를 찾아야 하는데 없으므로 InternalResourceViewResolver가 호출된다.)
  3. InternalResourceViewResolver가 InternalResourceView를 반환한다.
  4. view.render()가 호출되고 InternalResourceView는 forward()를 사용하여 JSP를 실행한다.

만약 JSTL 라이브러리가 있으면 InternalResourceViewResolver는 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가 대표적인데 앞서 보았듯이 가장 우선 순위가 높은 핸들러 매핑과 핸들러 어댑터를 실행한다.(RequestMappingHandlerMapping, RequestMappingHandlerAdapter)

 

이제 본격적으로 애노테이션 기반의 컨트롤러를 사용해보고자한다.

 

[SpringMemberFormControllerV1.java]-회원 등록 폼

@Controller
//1.controller만 있어도 컴포넌트 스캔 대상이 됨 -> 자동으로 스프링 빈 등록
//2.스프링 MVC에서 에노테이션 기반 컨트롤러로 인식함 -> RequestMappingHandlerMapping에서 씀
public class SpringMemberFormControllerV1 {

    @RequestMapping("/springmvc/v1/members/new-form") //요청 정보를 매핑(해당 URL이 호출되면 이 메서드가 호출된다.)
    public ModelAndView process() {
        return new ModelAndView("new-form"); //모델과 뷰 정보를 담아서 반환하면 됨
    }

}

RequestMappingHandlerMapping는 스프링 빈 중에서 @RequestMapping나 @Controller가 클래스 레벨에 붙어 있는 경우에는 매핑 정보로 인식을 한다.

 

[SpringMemberSaveControllerV1.java]-회원 저장

@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")); //문자로 뜨기 때문에 parseInt로 바꿔줘야함

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

        ModelAndView mv = new ModelAndView("save-result");
        mv.addObject("member", member);
        //mv.getModel().put("member", member); 보다 깔끔함
        return mv;

    }

}

 

[SpringMemberListControllerV1.java]-회원 목록

@Controller
public class SpringMemberListControllerV1 {
    private MemberRepository memberRepository = MemberRepository.getInstance();

    @RequestMapping("/springmvc/v1/members")
    public ModelAndView process() {

        List<Member> members = memberRepository.findAll();

        ModelAndView mv = new ModelAndView("members");
        mv.addObject("members", members);
        return mv;
    }
}

실행

  • 등록: http://localhost:8080/springmvc/v1/members/new-form
  • 목록: http://localhost:8080/springmvc/v1/members

 

5) 스프링 MVC -컨트롤러 통합

 

@RequestMapping을 잘 보면 클래스 단위가 아닌 메서드 단위에서도 적용이 돼서 컨트롤러를 하나로 통합이 가능하다.

 

[SpringMemberListControllerV2.java]

@Controller //@Controller는 @Component + @RequestMapping라서 @RequestMappingHandlerMapping 대상이된다.
@RequestMapping("/springmvc/v2/members")
public class SpringMemberControllerV2 {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    // /springmvc/v2/members/new-form
    @RequestMapping("/new-form")
    public ModelAndView newForm() {
        return new ModelAndView("new-form");
    }
    // //springmvc/v2/members/save
    @RequestMapping("/save")
    public ModelAndView save(HttpServletRequest request, HttpServletResponse response) {
        String username = request.getParameter("username");
        int age = Integer.parseInt(request.getParameter("age")); //문자로 뜨기 때문에 parseInt로 바꿔줘야함

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

        ModelAndView mv = new ModelAndView("save-result");
        mv.addObject("member", member);
        //mv.getModel().put("member", member); 보다 깔끔함
        return mv;
    }

    // /springmvc/v2/members
    @RequestMapping
    public ModelAndView members() {

        List<Member> members = memberRepository.findAll();
        ModelAndView mv = new ModelAndView("members");
        mv.addObject("members", members);
        return mv;
    }

}
/*
* RequestMappin은 메소드 단위이기 때문에 원하는 만큼 컨트롤러에 넣을 수 있음
*
* */

 

조합

컨트롤러 클래스를 통합하는 것을 넘어서 조합도 가능하다. /springmvc/v2/members라는 부분의 중복이 있다.

  • @RequestMapping("/springmvc/v2/members/new-form")
  • @RequestMapping("/springmvc/v2/members")
  • @RequestMapping("/springmvc/v2/members/save")

클래스 레벨에 @RequestMapping을 두면 메서드 레벨과 조합이 된다.

 

조합 결과

  • 클래스 레벨 @RequestMapping("/springmvc/v2/members")
    • 메서드 레벨 @RequestMapping("/new-form") /springmvc/v2/members/new-form
    • 메서드 레벨 @RequestMapping("/save") /springmvc/v2/members/save
    • 메서드 레벨 @RequestMapping /springmvc/v2/members

 

6) 스프링 MVC - 실용적인 방식

 

[SpringMemberControllerV3]

/*
	v3
    Model 도입
    ViewName 직접 반환
    @RequestParam 사용
    @RequestMapping ->@GetMapping, @PostMapping
*/

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


    // /springmvc/v2/members/new-form
   // @RequestMapping(value = "/new-form", method = RequestMethod.GET) //Post 아닌 Get의 경우에만 호출 (제약을 거는 게 더 좋은 설계)
    @GetMapping("/new-form")
    public String newForm() {
        return "new-form";
    }


    // //springmvc/v2/members/save
   // @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";

    }

    // /springmvc/v2/members
    //@RequestMapping(method = RequestMethod.GET) //members는 단순 조회이기 때문에 GET
    @GetMapping
    public String members(Model model) {

        List<Member> members = memberRepository.findAll();

        model.addAttribute("members", members);
        return "members";
    }
}

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

 

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

 

@RequestParam을 사용하여 HTTP 요청 파라미터를 받을 수 있다. 굳이 request.getParameter("username")을 사용할 필요는 없다. 물론 GET 쿼리 파라미터, POST Form 방식 모두 지원한다.

 

 

@RequestMapping은 URL만 매칭하는 것이 아닌 HTTP Method도 함께 구분할 수 있다.

예로 URL이 /new-form이고 HTTP Method가 GET인 경우를 모두 만족하는 매핑을 하려면 다음과 같이 처리를 한다.

@RequestMapping(value = "/new-form", method = RequestMethod.GET)

GET, POST 여부에 따라 @GetMapping, @PostMapping으로 더 편리하게 사용할 수도 있다.