📌 목차
핸들러 매핑과 핸들러 어댑터 - OldController
핸들러 매핑과 핸들러 어댑터 - HttpRequestHandler
스프링 MVC - 시작하기
스프링 MVC - 컨트롤러 통합
스프링 MVC - 실용적인 방식
📌 개요
이전 글에서는 위와같은 MVC 프레임워크를 직접 만들었다.
실제 Spring 의 MVC 모델은 위와 같다.
사실 구조적인 형태는 똑같다. 다만 이름이 몇가지 변경되었다.
FrontController -> DispatcherServlethandlerMappingMap -> HandlerMappingMyHandlerAdapter -> HandlerAdapterModelView -> ModelAndViewviewResolver -> ViewResolverMyView -> View이미 이전에 만든 MVC 와 구조적으로 동일하기 때문에 기존의 코드를 거의 그대로 사용하되 , Spring 의 기능을 사용해보자.
📌 핸들러 매핑과 핸들러 어댑터 - OldController
public interface Controller {
ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception;
}
과거 버전의 스프링 컨트롤러는 위와 같이 되어있었다.
먼저 이 인터페이스를 사용해보자.
package hello.servlet.web.springmvc.old;
@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 를 통해 경로의 이름으로 스프링 빈을 등록하였다. 이제 빈의 이름으로 URL 을 매핑할 것이다.
또한 논리 주소를 반환하였다.
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp
물리적 주소를 추가하기 위해서는 application.properties 경로에 위 코드를 추가하면 된다.
즉 ViewResolver 역할을 한다.
컨트롤러가 호출되기 위해서는 두가지 정보가 필요하다.
HandlerMapping (핸들러 매핑)
핸들러 매핑에서 이 컨트롤러를 찾을 수 있어야 한다.
예) 스프링 빈의 이름으로 핸들러를 찾을 수 있는 핸들러 매핑이 필요하다.
HandlerAdapter (핸들러 어댑터)
핸들러 매핑을 통해서 찾은 핸들러를 실행할 수 있는 핸들러 어댑터가 필요하다.
예) Controller 인터페이스를 실행할 수 있는 핸들러 어댑터를 찾고 실행해야 한다.
간단한 예시를 들자면 내가 고객 Client 일때 레스토랑 매니저 DispatcherServlet 에게 주문을 요청한다. 만약 고기와 관련된 음식을 주문하면 매니저는 고기를 담당하는 주방에 가서 Handler Mapping 을 시도한다.
그리고 해당 요리를 조리할줄 아는 요리사가 직접 요리하는 것을 Handler Adapter 라고 한다.
스프링이 자동으로 등록하는 핸들러 매핑과 핸들러 어뎁터는 여러 종류가 있다.
HandlerMapping
0 = RequestMappingHandlerMapping : 애노테이션 기반의 컨트롤러인 @RequestMapping 에서
사용
1 = BeanNameUrlHandlerMapping : 스프링 빈의 이름으로 핸들러를 찾는다.
HandlerAdapter
0 = RequestMappingHandlerAdapter : 애노테이션 기반의 컨트롤러인 @RequestMapping 에서
사용
1 = HttpRequestHandlerAdapter : HttpRequestHandler 처리
2 = impleControllerHandlerAdapter : Controller 인터페이스(애노테이션X, 과거에 사용)
처리
숫자가 낮을수록 우선순위가 높다.
아까 구현한 OldController 를 사용한다면 HandlerMapping 은 BeanNameUrlHandlerMapping 을 사용하고 HandlerAdapter 은 impleControllerHandlerAdapter 을 사용하게 된다.
📌 핸들러 매핑과 핸들러 어댑터 - HttpRequestHandler
이번에는 서블릿과 유사한 형태인 HttpRequestHandler 를 사용해보자.
public interface HttpRequestHandler {
void handleRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException;
}
실제 코드는 위와 같다. 특이한 점은 void 를 반환한다.
package hello.servlet.web.springmvc.old;
@Component("/springmvc/request-handler")
public class MyHttpRequestHandler implements HttpRequestHandler {
@Override
public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("MyHttpRequestHandler.handleRequest");
}
}
간단하게 구현하면 위 코드와 같다.
HandlerMapping 은 BeanNameUrlHandlerMapping 을 사용하고
HandlerAdapter 은 HttpRequestHandlerAdapter 을 사용하게 된다.
📌 스프링 MVC - 시작하기
스프링이 제공하는 컨트롤러는 애노테이션 기반으로 동작해서 아주 실용적이고 유연하다.
주로 @RequestMapping 을 사용하고 핸들러 매핑과 핸들러 어뎁터는 각각 RequestMappingHandlerMapping , RequestMappingHandlerAdapter 이다.
이전에 구현한 회원저장 , 회원등록 , 목록조회 로직을 스프링 애노테이션을 통해 구현해보자.
package hello.servlet.web.springmvc.v1;
@Controller
public class SpringMemberFromControllerV1 {
@RequestMapping("/springmvc/v1/members/new-form")
public ModelAndView process() {
return new ModelAndView("new-form");
}
}
회원등록 폼 코드는 위와같이 작성할 수 있다. @Controller 를 통해 자동으로 스프링 빈으로 등록한다. 그리고 ReqeustMapping 을 통해서 핸들러 매핑과 핸들러 어뎁터를 사용할 수 있다.
package hello.servlet.web.springmvc.v1;
@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);
memberRepository.save(member);
ModelAndView mv = new ModelAndView("save-result");
mv.addObject("member", member);
return mv;
}
}
회원 저장 로직은 크게 변한점이 없다. 다만 반환 타입이 ModelAndView 이고 addObject 를 통해 데이터를 저장할 수 있다.
package hello.servlet.web.springmvc.v1;
@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;
}
}
회원목록 조회 코드도 이전에 만든 MVC 모델에 비해 크게 달라진점은없다.
📌 스프링 MVC - 컨트롤러 통합
RequestMapping 을 통해 컨트롤러 클래스를 하나의 파일로 합칠 수 있다.
package hello.servlet.web.springmvc.v2;
@Controller
@RequestMapping("springmvc/v2/members")
public class SpringMemberControllerV2 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@RequestMapping("/new-form")
public ModelAndView newFrom() {
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;
}
}
이렇게 하면 중복된 경로를 제거할 수 있다.
클래스 레벨에서 @RequestMapping 에 경로를 추가하면 메서드에서 사용하는 경로가 추가되서 중복된 경로를 사용할 필요가없다.
지금까지 보면 이전에 구현한 MVC 모델의 v3 버전 형태와 유사하다. 따라서 v3 버전의 문제점도 그대로 생긴다.
즉 매번 ModelAndView 객체를 생성하고 반환한다.
Spring 에서는 이러한 문제점을 해결할 수 있는 방법이 존재한다. 알아보자.
📌 스프링 MVC - 실용적인 방식
package hello.servlet.web.springmvc.v3;
@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 newFrom() {
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";
}
}
v4 버전에서 컨트롤러는 Model 객체를 반환하지 않고 String 형식의 논리 주소만 반환하였다. Spring 에서도 똑같이 적용이 가능하다.
@Override
public String process(Map<String, String> paramMap, Map<String, Object> model) {
String username = paramMap.get("username");
int age = Integer.parseInt(paramMap.get("age"));
Member member = new Member(username , age);
memberRepository.save(member);
model.put("member", member);
return "save-result";
}
이전에 구현한 v4 버전의 컨트롤러에서는 Map<String, String> paramMap, Map<String, Object> model 파라미터를 받았다.
하지만 Spring 에서는 @RequestParam("username") String username, @RequestParam("age") int age, Model model 처럼 애노테이션을 통해 파라미터를 받아올 수 있고 Model 객체또한 받을 수 있다.
model 객체에 데이터를 저장하긴 위해선 addAttribute 메서드를 사용하면된다.
또한 @RequestMapping 를 통해 경로를 지정해주었는데 , 만약 GET , POST 같은 상태코드를 지정하고 싶다면 @RequestMapping(value = "/new-form" , method = RequestMethod.GET) 처럼 사용할 수 있다.
@GetMapping("/new-form") , @GetMapping("/new-form") 같은 애노테이션을 통해 더 간단하게 적을 수 있다.
실제로 @GetMapping 애노테이션에 직접 구현된 코드를 보면 @RequestMapping(method = RequestMethod.GET) 애노테이션이 등록되어 있다.
📌 정리
지금까지 이전에 만든 MVC 모델을 코드 로직은 거의 변경하지 않고 Spring MVC 형태로 바꾸었다.
처음에는 구버전의 인터페이스 형식의 핸들러 매핑 , 핸들러 어뎁터를 사용해보았고
요즘 가장 많이 쓰는 애노테이션 형식을 사용해보았다.
역시 유연성이 좋은 Spring 답게 애노테이션을 통해 v4 버전의 유용성을 그대로 가져올 수 있었고 심지어 파라미터에도 애노테이션을 사용하여 원하는 데이터를 쉽게 얻어올 수 있었다.