개발일기

스프링 MVC 구조 정리 및 회고 본문

카테고리 없음

스프링 MVC 구조 정리 및 회고

한둥둥 2024. 3. 8. 01:01

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 등이 있다.