스프링 웹 MVC 1편 - 5. 스프링 MVC 구조 이해

링딩·2022년 7월 31일
0

스프링 MVC

목록 보기
5/18

이 글은 김영한 강사님의 강의를 참고하여 작성하였습니다.


Chap 5. 스프링 MVC 구조 이해

이제 그동안 배웠던 MVC 프레임워크를 기반으로 더 나은 스프링에서 제공해주는 스프링 MVC에 대한 구조를 배워보자.



1. 스프링 MVC 전체 구조

이전까지 우리가 만들었던 스프링 MVC는 몇몇의 이름만 조금 달라졌다고 보면 된다!

[직접 만든 프레임워크 -> 스프링 MVC]

  • FrontController ⇨ DispatcherServlet
  • handlerMappingMap ⇨ HandlerMapping
  • MyHandlerAdapter ⇨ HandlerAdapter
  • ModelView ⇨ ModelAndView
  • viewResolver ⇨ ViewResolver
  • MyView ⇨ View

[스프링 MVC]

생각보다 명칭 외에는 달라지는 것이 없다.
그렇다면 크게 차이가 있을지 세세히 들어가보자



✨ DisaptcherServlet 구조

근데 원래 이 자리 FrontController 자리 아니였나요? 🤷‍♂️🤷‍♂️

A. 맞다 매우 맞다. 그 이름이 '디스패치 서블릿'으로 바꼈다고 생각해도 무관하다!


DisaptcherServlet 서블릿 등록

  • DispacherServlet부모 클래스에서 HttpServlet 을 상속 받아서 사용하고, 서블릿으로 동작한다.
    DispatcherServlet -> FrameworkServlet -> HttpServletBean -> HttpServlet
  • 스프링 부트 DispacherServlet¹서블릿으로 자동으로 등록하면서 ²모든 경로( urlPatterns="/" )에 대해서 매핑한다.
    참고: 더 자세한 경로가 우선순위가 높다
    => 기존에 등록한 서블릿도 함께 동작

요청 흐름 (코드와 함께)

서블릿이 호출되면 HttpServlet 이 제공하는 serivce() 가 호출된다.

  • '스프링 MVC는' DispacherServlet부모인 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. 뷰 렌더링
    : 뷰를 통해서 뷰를 렌더링 한다.

스프링 MVC 강점

  • 인터페이스
    위의 DispatcherServlet 의 코드 변경 없이, 원하는 기능을 변경 및 확장이 가능하다.

이 모든 것을 다 알아야 하나요? 🤦‍♂️😢

  • 스프링 MVC는 분량도 코드도 많고 복잡하다. 그러나 실제 이 기능들을 직접 확장하고 나만의 컨트롤러를 만들 일은 없다 걱정 말자! 👍
    => 이미 필요로 하는 대부분의 기능이 다 구현되어 있다.

  • 향후 이런 핵심 동작들을 알아두어야 문제의 원인 파악을 할 수 있고 , 확장이 필요할 때 어떤 부분을 확장하면 좋을지 감을 잡을 수 있다!

그저 너무 어려워 하지 않고 전체적인 구조를 파악하자





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

'핸들러 매핑'과 '핸들러 어댑터'가 어떤 것들이 어떻게 사용되는지 알아보자.
지금은 전혀 사용하지 않지만, 과거에 주로 사용했던 스프링이 제공하는 '간단한 컨트롤러'로 핸들러 매핑과 어댑터를 이해해보자.


[Controller 인터페이스]

과거 버전 스프링 컨트롤러

public interface Controller {
	ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse 
response) throws Exception;
}

Controller 인터페이스는 @Controller 애노테이션과는 전혀 다르다.


[OldController]

@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 null;
 }
}
  • @Component
    이 컨트롤러는 springmvc/old-controller 라는 이름의 스프링 빈으로 등록되었다.
    => '빈의 이름'으로 URL을 매핑할 것 이다

근데 생각해보니 이 'oldController'가 어떻게 호출된걸까??? 🤷‍♂️

컨트롤러가 호출되려면 다음의 2가지 모두 필요하다.

  1. HandlerMapping(핸들러 매핑)
    핸들러 매핑에서 이 '컨트롤러'가 있는지 찾아서 존재해야 함.
    예) 스프링 빈의 이름으로 핸들러를 찾을 수 있는 핸들러 매핑이 필요

  2. HandlerAdapter(핸들러 어댑터)
    핸들러 매핑을 통해서 찾은 '이 핸들러(컨트롤러)'를 실행할 수 있는 핸들러 어댑터가 필요하니 찾아준다.
    예) Controller 인터페이스를 실행할 수 있는 핸들러 어댑터를 찾고 실행해야 한다.

그런데 걱정말자 우리 스프링은 똑똑해서 이미 필요한 대부분의 '핸들러 매핑' & '핸들러 어댑터'를 구현해두었다~! 👍😎
-> 개발자가 손수 만드는 일은 거의 없다.


Q. 스프링 부트가 '자동 등록' 한 "핸들러 매핑"&"핸들러 어댑터"는?

0부터 우선순위가 제일 높고, '핸들러 매핑'을 먼저 가서 순서대로 찾고 선택되면, '핸들러 어댑터'로 옮겨 순서대로 찾아 택한다.


그렇다면 방금까지 진행하던 OldController는 어떤 흐름으로 진행되었을까? 🤷‍♂️

정리 - OldController 핸들러매핑, 어댑터

OldController 를 실행하면서 사용된 객체는 다음과 같다.

  • HandlerMapping = BeanNameUrlHandlerMapping
  • HandlerAdapter = SimpleControllerHandlerAdapter

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

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

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


@RequestMapping

  • 가장 우선순위가 높다.
    핸들러 매핑 : RequestMappingHandlerMapping
    핸들러 어댑터 : RequestMappingHandlerAdapter 이다.
  • 이것이 바로 지금 스프링에서 주로 사용하는
    애노테이션 기반의 컨트롤러를 지원하는 매핑과 어댑터이다.
    => 실무에서는 99.9% 이 방식의 컨트롤러를 사용


✨ viewResolver ?

근데 원래 이 자리 FrontController 자리 아니였나요? 🤷‍♂️🤷‍♂️

A. 맞다 매우 맞다. 그 이름이 '디스패치 서블릿'으로 바꼈다고 생각해도 무관하다!


@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"); //null 이 아니네??
 }
}

  • 이제 View를 사용할 수 있도록 null이 아닌 '논리적 이름'을 넣었다.

그러나 이렇게 실행하면 Whitelabel Error Page 오류가 뜰테지.... 💦🤦‍♀️

그러니 aplication.properties에 이 코드를 추가해줘야 한다~
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp


근데 왜 저걸 넣는다고 되는데? 🤔

  • 스프링 부트는 InternalResourceViewResolver 라는 '뷰 리졸버'를 자동 등록 해준다 대신...
    -> 저기 저 application.properties저 설정 정보를 통해 '등록' 된다!



✨어떤 과정으로 '뷰 리졸버'가 작동되나요

이 사진은 스프링 부트가 '자동 등록'해주는 '뷰 리졸버' 일부이다.

[동작 흐름]

  1. 핸들러 어댑터 호출
    • 핸들러 어댑터를 통해 new-form 이라는 논리 뷰 이름을 획득한다.

  2. ViewResolver 호출
    • new-form 이라는 뷰 이름으로 viewResolver를 순서대로 호출한다.
    • BeanNameViewResolvernew-form 이라는 이름의 스프링 빈으로 등록된 뷰를 찾아야 하는데 없다?
    • InternalResourceViewResolver 가 호출된다.

  3. InternalResourceViewResolver -> 뷰 리졸버가 뷰를 반환!
    • 이 뷰 리졸버는 InternalResourceView반환

  4. 뷰 - InternalResourceView
    • InternalResourceView 는 JSP처럼 포워드 forward() 를 호출해서 처리할 수 있는 경우에 사용한다.
  5. view.render()
    • view.render() 가 호출되고 , InternalResourceView(뷰) 는 forward() 를 사용해서 JSP를 실행한다.

참고

  • JSP의 경우 forward() 로 해당 JSP로 이동(실행) 해야 렌더링 된다.
    -> JSP 외의 '뷰 템플릿'은 forward() 없이 바로 렌더링 된다.

  • Thyeleaf 뷰 템플릿은 최근에는 '라이브러리'만 추가해도 이런 작업들을 모두 스프링 부트가 자동화를 해준다.
    -> 원래는 ThymeleafViewResolver를 등록해야 되지만





스프링 MVC

🤔 애노테이션 기반으로 동작하는 '컨트롤러'다.

@RequestMapping

  • 앞에서도 한 번 나왔지만 지금 가장 많이 쓰이고 또 사용하는 어노테이션
  • 매우 유연하고, 실용적인 컨트롤러다.
  • RequestMaapingHandlerMapping
    RequestMappingHandlerAdapter 을 지원한다.

[회원 등록 폼]

@Controller
public class SpringMemberFormControllerV1 {
 
 	@RequestMapping("/springmvc/v1/members/new-form")
 	public ModelAndView process() {
 		return new ModelAndView("new-form");
 }
}

원래라면 비즈니스 로직을 담는 '서비스'라는 계층이 따로 있음.

  • @Controller
    - 내부에 @Component 가 있어서 스프링이 자동으로 '스프링 빈'으로 등록
    => 이걸 통해 스프링MVC에서는 '애노테이션 기반 컨트롤러'를 인식한다.
  • @RequestMapping
    - 요청 정보를 매핑
    -> 해당 url이 호출된다? => 이 애노테이션이 있는 메서드가 호출.
  • ModelAndView
    - 모델과 뷰 정보를 담아서 반환한다.

+) 추가

@RequestMapping + @Component 클래스 레벨에 붙어 있을 경우, '매핑 정보'로 인식함.

@Component //컴포넌트 스캔을 통해 스프링 빈으로 등록
@RequestMapping
public class SpringMemberFormControllerV1 {
 
 @RequestMapping("/springmvc/v1/members/new-form")
 public ModelAndView process() {
 return new ModelAndView("new-form");
 }
}

@Component로 스프링 빈 등록 , @RequestMapping 을 같이 '클래스'단에 있으면 아 넌 내가 처리할 수 있는 핸들러구만! 하고 인식


스프링 빈을 직접 등록해주고 @RequestMapping만 넣기

@RequestMapping
public class SpringMemberFormControllerV1 {
 
 @RequestMapping("/springmvc/v1/members/new-form")
 public ModelAndView process() {
 return new ModelAndView("new-form");
 }
}


//스프링 빈 직접 등록
@Bean
SpringMemberFormControllerV1 springMemberFormControllerV1() {
return new SpringMemberFormControllerV1();
}

이렇게 다양하게 있다. 그러나 제일 편한건 @Controller를 클래스 단에 넣어주면 제일 편할 것이다~


[회원저장]

@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);
 
		System.out.println("member = " + member);
 
 		memberRepository.save(member);
 		
        ModelAndView mv = new ModelAndView("save-result");
 		mv.addObject("member", member);
 		
        return mv;
 }
}
  • mv.addObject("member", member)
    • 스프링이 제공하는 ModelAndView 를 통해 Model 데이터를 추가할 때는 addObject() 를 사용하면 된다.
    • 이 데이터는 이후 뷰를 렌더링 할 때 사용


조합

아까 앞에서 봤던 것처럼 @RequestMapping은 클래스 단위에서도 쓰고 메서드 단위에서도 쓴다.
=> 아 유연하게 사용이 가능하구나 😎😮👍

/**
 * 클래스 단위 -> 메서드 단위
 * @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 mav = new ModelAndView("save-result");
  		mav.addObject("member", member);
  	
    	return mav;
 	}
 
  	@RequestMapping
  	public ModelAndView members() {
  	 	List<Member> members = memberRepository.findAll();
  	 	ModelAndView mav = new ModelAndView("members");
 	 	mav.addObject("members", members);
  	 	
        return mav;
 }
}

클래스 단위에 쓴 url "/springmvc/v2/members"이 중복적이게 되기 때문에 조합을 써봤다. 저 부분이 반복되기 때문에

  • 클래스 레벨 @RequestMapping("/springmvc/v2/members")

  • 메서드 레벨 @RequestMapping("/new-form") -> /springmvc/v2/members/new-form 으로 간소화

  • 메서드 레벨 @RequestMapping("/save") -> /springmvc/v2/members/save

  • 메서드 레벨이 클래스 레벨가 같은 경우..
    @RequestMapping만 url은 더 안 쓴다. -> /springmvc/v2/members 이런 식으로 인식 됨!




그리고 최근 실용적인 방식

/**
 * v3
 * Model 도입
 * ViewName 직접 반환
 * @RequestParam 사용
 * @RequestMapping -> @GetMapping, @PostMapping
 */
@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
 	public String members(Model model) {
 		List<Member> members = memberRepository.findAll();
 		model.addAttribute("members", members);
 
 		return "members";
 }
}

1. Model 파라미터
save() , members() 를 보면 Model을 파라미터로 받는다.

2. ViewName 직접 반환
뷰의 논리 이름을 반환할 수 있다.
-> 원래 ModelView 타입으로 반환해주어도 되지만, 편하게 String 타입으로 ViewName을 직접 반환해줘도 된다.
=> 스프링은 String으로 반환된 것을 보고 "아하 뷰 이름이구나" 하고 인식해준다.

3. @RequestParam 사용

• 스프링은 HTTP 요청 파라미터를 @RequestParam 으로 받을 수 있다.
@RequestParam("username")request.getParameter("username") 와 거의 같은 코드라 생각하자
• GET 쿼리 파라미터, POST Form 방식을 모두 지원

4. 가장 좋은 것은 HTTP Method도 구분이 가능😮

Get, Post, Put, Delete, Patch 모두 애노테이션이 존재.

ex) 조건 : URL이 /new-form 이고, HTTP Method가 GET인 경우를 모두 만족하는 매핑을 하려면

이다. 그러나 이것을 보다 더욱 더 편하게 만들 수 있는 방법이 있다.

@GetMapping("/new-form")으로 더 간추릴 수 있다.
ex) @GetMapping, @PostMapping, @PutMapping....


V5 에서 더

그렇다면 ControllerV3 외에 다른 ControllerV4도 호환이 되게끔 적용해보자!

또한 해당 핸들러를 처리할 수 있는 '핸들러 어댑터'도 잊지 말아야 한다!!!! 😮👍


[FrontController]

  • 이를 위해 기존의 FrontController에서 해당되는 핸들러들과 그에 맞는 '핸들러 어댑터'를 추가해준다.
private void initHandlerMappingMap() {
        handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new
                MemberFormControllerV3());
        handlerMappingMap.put("/front-controller/v5/v3/members/save", new
                MemberSaveControllerV3());
        handlerMappingMap.put("/front-controller/v5/v3/members", new
                MemberListControllerV3());

        //V4 추가
        handlerMappingMap.put("/front-controller/v5/v4/members/new-form", new MemberFormControllerV4());
        handlerMappingMap.put("/front-controller/v5/v4/members/save", new MemberSaveControllerV4());
        handlerMappingMap.put("/front-controller/v5/v4/members", new MemberListControllerV4());
    }

    private void initHandlerAdapters() {

        handlerAdapters.add(new ControllerV3HandlerAdapter());
        handlerAdapters.add(new ControllerV4HandlerAdapter()); //V4 추가
    }

[ControllerV4HandlerAdapter] _ MyHandlerAdapter 구현체

public class ControllerV4HandlerAdapter implements MyHandlerAdapter {
    @Override
    public boolean supports(Object handler) {
        return (handler instanceof ControllerV4);
    }

    @Override
    public ModelView handle(HttpServletRequest request, HttpServletResponse
            response, Object handler) {
        ControllerV4 controller = (ControllerV4) handler;
        
        Map<String, String> paramMap = createParamMap(request);
        Map<String, Object> model = new HashMap<>();
        
        String viewName = controller.process(paramMap, model);

        //어댑터의 기능: ControllerV4의 String을 -> ModelView반환으로 만들어줌
        ModelView mv = new ModelView(viewName);
        mv.setModel(model);

        return mv;
    }

    private Map<String, String> createParamMap(HttpServletRequest request) {
        Map<String, String> paramMap = new HashMap<>();
        request.getParameterNames().asIterator()
                .forEachRemaining(paramName -> paramMap.put(paramName,
                        request.getParameter(paramName)));
        return paramMap;
    }
}
  • ControllerV4를 사용하는 핸들러를 처리하기 위한 '핸들러 어댑터'를 만들어줘야 한다.

좀 더 구체적으로 code 살피기

  1. supprts(핸들러)
public boolean supports(Object handler) {
 return (handler instanceof ControllerV4);
}
  • ControllerV4의 구현체인 handler만 처리해주는 어댑터를 조회.
    -> 한 마디로 핸들러를 처리할 수 있는 어댑터를 조회하는 곳이다.


  1. 어댑터의 기능 _변환
ControllerV4 controller = (ControllerV4) handler;

Map<String, String> paramMap = createParamMap(request);

Map<String, Object> model = new HashMap<>();
String viewName = controller.process(paramMap, model);

ModelView mv = new ModelView(viewName);
mv.setModel(model);

return mv;

[제약] but 이곳에서는 ModelView로 반환해야 함

  • 원래라면 ControllerV4는 String 타입의 '뷰 이름'을 반환.
    => (어댑터의 기능) : ModelView로 형식을 맞추어 반환해준다.
profile
초짜 백엔드 개린이

0개의 댓글