5. 스프링 MVC - 구조 이해

김현준·2023년 7월 23일

Gaounuri_Spring_Study

목록 보기
6/10

Project Code File

📌 목차

  • 핸들러 매핑과 핸들러 어댑터 - OldController

  • 핸들러 매핑과 핸들러 어댑터 - HttpRequestHandler

  • 스프링 MVC - 시작하기

  • 스프링 MVC - 컨트롤러 통합

  • 스프링 MVC - 실용적인 방식

📌 개요

image

이전 글에서는 위와같은 MVC 프레임워크를 직접 만들었다.

image

실제 SpringMVC 모델은 위와 같다.
사실 구조적인 형태는 똑같다. 다만 이름이 몇가지 변경되었다.

  • FrontController -> DispatcherServlet
  • handlerMappingMap -> HandlerMapping
  • MyHandlerAdapter -> HandlerAdapter
  • ModelView -> ModelAndView
  • viewResolver -> ViewResolver
  • MyView -> 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 를 사용한다면 HandlerMappingBeanNameUrlHandlerMapping 을 사용하고 HandlerAdapterimpleControllerHandlerAdapter 을 사용하게 된다.

📌 핸들러 매핑과 핸들러 어댑터 - 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");
    }
}

간단하게 구현하면 위 코드와 같다.

HandlerMappingBeanNameUrlHandlerMapping 을 사용하고
HandlerAdapterHttpRequestHandlerAdapter 을 사용하게 된다.

📌 스프링 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 버전의 유용성을 그대로 가져올 수 있었고 심지어 파라미터에도 애노테이션을 사용하여 원하는 데이터를 쉽게 얻어올 수 있었다.

profile
울산대학교 IT융합학부

0개의 댓글