개발일기
스프링 MVC 구조 정리 및 회고 본문
springmvc의 구조는 우리가 이전에 만든 frontController의 구조와 매우 흡사하다.
스프링 MVC에도 프론트 컨트롤러 패턴이 구현이 되어 있는데 그것이 DispatcherServlet이다.
DispatcherServlet은 FrameWorkServlet을 상속하고 FrameWorkServlet은 HttpServlet을 상속하는 것을 볼 수 있다.
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
HttpServletRequest request;
HttpServletResponse response;
if (!(req instanceof HttpServletRequest && res instanceof HttpServletResponse)) {
throw new ServletException("non-HTTP request or response");
}
request = (HttpServletRequest) req;
response = (HttpServletResponse) res;
service(request, response);
}
위에 코드와 같이 service가 구현되어있다.
우리가 구현했던 FrontController Code에서 Service를 구현했던 부분과 유사하다. 다만 mvc의 service는 조금 더 복잡하고 다양한 기능을 지원한다.
이러한 Service들이 타고타고 흘러가서 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. @Component를 통한 Bean으로 MVC 패턴 사용방법 (과거)
package hello.servlet.web.springmvc.old;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.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 new ModelAndView("new-form");
}
}
해당 코드는 @Component를 통해서 /springmvc/old-controller라는 빈이 올라가게되고 HandlerAdapter는 1순위는 Mapping 2순위는 /springmvc/old-controller로 순위를 정해서 매칭되는 것을 통해 URI를 설정하여 Controller에 접근할 수 있다.
- HandlerMapping(핸들러매핑)
- 핸들러 매핑에서 이 컨트롤러를 찾을 수 있어야 한다.
- 예) 스프링 빈의 이름으로 핸들러를 찾을 수 있는 핸들러 매핑이 필요하다.
- HandlerAdapter(핸들러 어댑터)
- 핸들러 매핑을 통해서 찾은 핸들러를 실행할 수 있는 핸들러 어댑터가 필요하다.
- 예) Controller 인터페이스를 실행할 수 있는 핸들러 어댑터를 찾고 실행해야 한다.
# HandlerMapping
0 = RequestMappingHandlerMapping : 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
1 = BeanNameUrlHandlerMapping : 스프링 빈의 이름으로 핸드러를 찾는다.
HandlerAdapter
0 = RequestMappingHandlerAdapter : 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
1 = HttpRequestHandlerAdapter : HttpRequestHandler 처리
2 = SimpleControllerHandlerAdapter : Controller 인터페이스(애노테이션 X, 과거에 사용)처리
뷰 리졸버
application.properties에
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp로 등록하면된다.
이렇게하면 InternalResourceViewResolver에 해당 정보의 prefix와 suffix가 인식함.
이렇게 안하려면 Application에서 Bean으로 설정해주면 된다.
1. 핸들러 어댑터 호출
핸들러 어댑터를 통해 new-form이라는 논리 뷰 이름을 획득한다.
2. ViewResolver호출
- new-form이라는 뷰 이름으로 viewResolver를 순서대로 호출한다.
- BeanNameViewResolver는 new-form이라는 이름의 스프링 빈으로 등록된 뷰를 찾아야 하는데 없다.
- InternalResourceViewResolver가 호출된다.
3. InternalResouceViewResolver
이 뷰 리졸버는 InternalResourceView를 반환한다.
4. 뷰 - InternalResouceView
InternalResourceView는 JSP처럼 포워드 forward를 호출해서 처리할 수 있는 경우 사용된다.
5. view.render()
view.render()가 호출되고 InternalResouceView는 forward() 사용해서 jsp render 한다.
2. RequestMapping을 통한 ModelAndView로 MVC패턴 사용방법
package hello.servlet.web.springmvc.v1;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
@Controller
public class SpringMemberFormControllerV1 {
@RequestMapping("/springmvc/v1/members/new-form")
public ModelAndView process(){
return new ModelAndView("new-form");
}
}
@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;
}
@RequestMapping 어노테이션을 통하여 매핑 정보를 인식한다.
스프링 부트 3.0 부터는 클래스 레벨에 @RequestMapping이 있어도 스프링 컨트롤러로 인식되지 않는다. 오직 @Controller가 있어야 스프링 컨트롤러로 인식된다.
참고로 @RestController는 내부에 @Controller 애노테이션이 포함되어 있기 때문에 인식된다.
스프링에서 제공하는 MdoelAndView에서 mv.addObject를 통해서 modle데이터를 추가해주었다.
package hello.servlet.web.springmvc.v2;
import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import java.util.List;
@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
public ModelAndView members() {
List<Member> members = memberRepository.findAll();
ModelAndView mv = new ModelAndView("members");
mv.addObject("members",members);
return mv;
}
@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;
}
}
해당 방식은 Controller를 통합하여 구현한 방식이다. 맨위에 공통 URI해당하는 부분을 적어주어 생략하게 만들어주었다.
지금까지 Return에 ModelAndView 객체를 리턴해주었다면 이제부터는 String으로 넘겨줄 것이다.
package hello.servlet.web.springmvc.v3;
import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@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(method= RequestMethod.GET)
@GetMapping
public String members(Model model) {
List<Member> members = memberRepository.findAll();
model.addAttribute("members",members);
return "members";
}
// @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";
}
}
request.param을 통해서 HttpServlerRequest를 통해서 paramter 값을 뽑아왔지만 조금 더 편하게 @RequestParam을 통해서 데이터를 가져올 수 있다.
데이터를 변경할 때는 PostMapping , 데이터를 변경하지 않는 다면 GetMapping으로 가져온다. 사실 둘다 되지만 이는 HttpSpec을 지켜주기 위해 이런식으로 해주어야 한다. 이외에도 Patch, Delete, Put 등이 있다.