개발놀이터

스프링 MVC 구조 파악하기 본문

Spring/Spring

스프링 MVC 구조 파악하기

마늘냄새폴폴 2022. 1. 7. 19:35

이 포스팅은 인프런 김영한 님의 스프링 핵심 원리 기본 편을 보고 각색한 포스팅입니다. 자세한 내용은 강의를 확인해주세요

 

우리는 앞서 직접 MVC 프레임워크를 만들었다. 안정적인 아키텍처와 사용자의 편의를 위한 기능도 추가했었다. 

https://coding-review.tistory.com/87

 

MVC 프레임워크 만들기

이 포스팅은 인프런 김영한 님의 스프링 핵심 원리 기본 편을 보고 각색한 포스팅입니다. 자세한 내용은 강의를 확인해주세요 우리는 앞으로 점진적인 버전업을 통해 스프링 MVC를 구현해 볼 것

coding-review.tistory.com

 

이번엔 우리가 직접 만든 MVC 프레임워크와 스프링 MVC 프레임워크를 비교해보고 스프링 MVC에 대해 좀 더 자세히 알아보는 시간을 가지려 한다.

 

먼저 직접 만든 MVC 프레임워크의 구조이다.

다음은 스프링 MVC의 구조이다.

비교해보면 둘이 완벽하게 일치하는 것을 알 수 있다. 그렇다. 우리가 직접 만든 MVC 프레임워크는 사실 스프링을 기반으로 한 MVC 구조와 완전히 똑같다. 다른것이라고 한다면 스프링이 사용하고 있는 이름과 우리가 직접 만든 프레임워크의 이름이 다르다는 점과 스프링은 viewResolver나 View를 인터페이스로 만들어 확장성에 더 용이하게 만들었다는 것이다.

 

 

동작 순서

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

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

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

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

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

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

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

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

 

스프링 MVC의 큰 강점은 DispatcherServlet 코드의 변경 없이, 원하는 기능을 변경하거나 확장할 수 있다는 점이다. 지금까지 설명한 대부분을 확장 가능할 수 있게 인터페이스로 제공한다.

 

 

과거버전 스프링 컨트롤러는 다음과 같았다. 

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

스프링도 처음에는 이런 딱딱한 형식의 컨트롤러를 제공했다. 우리가 만든 V3와 비슷한 모양이다. 인터페이스 Controller는 어노테이션 @Controller와 완전히 다른 객체이다.

 

과거버전 스프링 컨트롤러를 간단하게 구현해보자

@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는 컴포넌트 스캔의 대상으로서 스프링빈으로 등록하는 것인데 스프링 빈 이름이 url이네? 이게 동작하려면 어떤게 필요할까?

 

핸들러 매핑부분

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

-예) 스프링 빈의 이름으로 핸들러를 찾을 수 있는 핸들러 매핑이 필요하다.

 

핸들러 어댑터 부분

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

-예)Controller 인터페이스를 실행할 수 있는 핸들러 어댑터를 찾고 실행해야 한다.

 

스프링은 이미 필요한 핸들러 매핑과 핸들러 어댑터를 대부분 구현해두었다.

 

 

이제 스프링의 핸들러 매핑과 핸들러 어댑터에 대해 더 자세히 알아보도록 하자 실제로는 더 많지만 중요한 부분 위주로 설명하기 위해 일부 생략

 

HandlerMapping 

1. RequestMappingHandlerMapping : 어노테이션 기반의 컨트롤러인 @RequestMapping에서 사용

2. BeanNameUrlHandlerMapping : 스프링 빈의 이름으로 핸들러를 찾는다. 

 

HandlerAdapter

1. RequestMappingHandlerAdapter : 어노테이션 기반의 컨트롤러인 @Requestmapping에서 사용

2. HttpRequestHandlerAdapter : HttpRequestHandler 처리

3. SimpleControllerHandlerAdapter : Controller 인터페이스 처리

 

각각의 번호는 호출 순서를 의미한다.

 

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

1. HandlerMapping을 순서대로 실행해서, 핸들러를 찾는다.

2. 이 경우 빈 이름으로 핸들러를 찾아야 하기 때문에 이름 그대로 빈 이름으로 핸들러를 찾아주는 BeanNameHandlermapping이 실행에 성공하고 핸들러인 OldController를 반환한다.

2. 핸들러 어댑터 조회

1. HandlerAdapter의 supports()를 순서대로 호출한다.

2. SimpleControllerHandlerAdapter가 Controller 인터페이스를 지원하므로 대상이 된다.

3. 핸들러 어댑터 실행

1. 디스패처 서블릿이 조회한 SimpleControllerHandlerAdapter를 실행하면서 핸들러 정보도 함께 넘겨준다.

2. SimpleControllerHandlerAdapter는 핸들러인 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 new ModelAndView("new-form");
	}
}

View를 사용할 수 있도록 다음 코드를 추가했다.

return nuew ModelAndView("new-form");

실행해보면 404 오류페이지가 뜬다. 

 

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

 

뷰 리졸버 동작 방식은 다음과 같다.

 

1. 핸들러 어댑터 호출

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

2. ViewResolver 호출

new-form이라는 뷰 이름으로 viewResolver를 순서대로 호출하고 BeanNameViewResolver는 new-form이라는 이름의 스프링 빈으로 등록된 뷰를 찾아야 하는데 없다. 그럼 InternalResourceViewResolver가 호출된다.

3. InternalResourceViewResolver

이 뷰 리졸버는 InternalResourceView를 반환한다.

4. 뷰 - InternalResourceView

InternalResourceView는 JSP처럼 포워드를 호출해서 처리할 수 있는 경우에 사용한다.

5. view.render()

view.render()가 호출되고 InternalResourceView는 forward()를 사용해서 JSP를 실행한다.

 

 

 

 

이제 본격적으로 스프링 MVC를 시작해보자, 스프링이 제공하는 컨트롤러는 어노테이션 기반으로 동작해서, 매우 유연하고 실용적이다. 과거에는 자바 언어에 어노테이션이 없기도 했고, 스프링도 처음부터 이런 유연한 컨트롤러를 제공한 것은 아니다.

 

@RequestMapping

스프링은 어노테이션을 활용한 매우 유연하고, 실용적인 컨트롤러를 만들었는데 이것이 바로 @RequestMapping어노테이션을 사용하는 컨트롤러이다. 다들 한번쯤 사용해봤을 것이다. 

 

@Controller

1. 스프링이 자동으로 스프링빈으로 등록한다 (내부에 @Component 어노테이션이 있어서 컴포넌트 스캔의 대상이 됨)

2. 스프링 MVC에서 어노테이션 기반 컨트롤러로 인식한다.

 

@GetMapping

@RequestMapping의 확장버전 Get방식의 url을 처리하는 컨트롤러에 붙여준다.

 

@PostMapping

@RequestMapping의 확장버전 Post방식의 url을 처리하는 컨트롤러에 붙여준다.

 

 

바로 현업에서는 어떻게 사용하는지 보자

@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";
	}
}

@RequestParam은 request.getParameter와 역할이 같다. 스프링에선 타입까지 지정해서 받아올 수 있다. 때문에 Integer.parseInt같은 문법을 사용하지 않고 깔끔하게 처리할 수 있다. 넘어오는 파라미터가 한두개면 @RequestParam을 사용하지만 넘어오는 파라미터가 여러개라면 따로 DTO를 만들어서 받아올 수 있다. 또한 인자값으로 Model을 받을 수 있는데 이는 우리가 Model을 직접 넘긴 것과 같은 효과를 본다.