Web/Spring

SpringMVC (5) - 스프링 MVC 구조 이해

now0204 2023. 10. 2. 16:43

출처: 스프링mvc (인프런) - 김영한

 

- 직접 만든 프레임 워크와 스프링 MVC는 큰 차이가 없다 

- 이름만 조금 다를 뿐 구조는 매우 비슷함.

 

> DispatcherServlet 구조 살펴보기

 

-org,springframwork.web.servlet.DispatcherServlet 

프론트 컨트롤러 패턴으로 구현됨

스프링 MVC의 핵심

 

- dispatcherservlet도 httpservlet을 상속받아서 사용하고, 서블릿으로 동작한다.

- 스프링 부트는 dispatcherservlet을 서블릿으로 자동등록하며, 모든 경로에 대해서 맵핑한다.

 

* 더 자세한 경로가 우선순위 높음 따라서 기존에 등록한 서블릿도 함께 동작함

 

> 요청 흐름

 

- 서블릿이 호출되면, service()가 호출됨

- frameworkServlet.service()를 시작으로 여러 메서드 호출 -> dispacherServlet에 doDispatch()호출됨

- 이 메서드가 dispatcher의 핵심코드이다.

 

protected void doDispatch(HttpServletRequest request, HttpServletResponse 
response) throws Exception {

// 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);
}

핸들러 조회 -> 어댑터 조회 -> 어댑터 실행 -> 핸들러 실행 -> ModelAndView반환 -> viewResolver호출 -> view 객페 반환 -> 뷰 랜더링 

 

- 인터페이스 : 스프링 MVC의 강점은 DispatcherServlet의 코드 변경없이 기능 확장 가능 아래 인터페이스만 구현해서 Dispatcher에 등록하면 컨트롤러 만들 수 있다.

 

주요 인터페이스 목록

핸들러 매핑:org.springframework.web.servlet.HandlerMapping

핸들러 어댑터: org.springframework.web.servlet.HandlerAdapter

뷰 리졸버: org.springframework.web.servlet.ViewResolver

뷰: org.springframework.web.servlet.View

 

 

2. 핸들러 매핑과 핸들러 어댑터

 

- 핸들러 매핑과 어댑터가 어떤 것들이 사용되는지 알아봅시다.

- 과거에 주로 사용했던, 간단한 컨트롤러로 핸들러 매핑과 어댑터 알아보자

 

2.1 Controller 인터페이스 

 

org.springframwork.web.servlet.mvc.Controller

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

 

- 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 null;

}
}

- @Conponent : 이 컨트롤러는 /springmvc/old-controller라는 이름으로 스프링 빈 등록

- 빈의 이름으로 url 매핑함

 

이 컨트롤러가 호출되려면 2가지가 필요함

- HandlerMapping, HandlerAdapter

스프링은 oldController형식의 컨트롤러를 처리할 수 있는 핸들러맵핑방식과 어댑터를 구현해 둠

 

출처: 스프링MVC(인프런) - 김영한

  - 직접 맵핑정보를 등록하지 않아도 스프링 빈으로 등록시, 자동으로 등록됨

  - 어댑터 또한 직접 구현하지 않아도, Controller인터페이스 구현한 핸들러를 처리할 수 있는 어댑터가 이미 있음 이걸로 처리

 

* 핸들러 맵핑은 어노테이션 @RequestMapping에서 한번 찾고 없으면 -> 빈 이름으로 핸들러 찾음

   핸들러 어댑터는 -> handlerAdapter의 supports()호출하는데, 2번에서 걸림 

   핸들러 어댑터 조회에 성공했으면, 핸들러 어댑터 실행하면서 핸들러(OldController) 넘김

 

결론적으로 Controller 사용하면 

BeanNameUrlHandlerMapping과 SimpleControllerHandlerAdapter가 작동해서 처리해준다.

따라서 Contoller 구현 + @Conpnent만 잘 지키자

 

2.2 HtpRequestHandler

 

- 서블릿과 가장 유사한 형태의 핸들러이다.

public interface HttpRequestHandler {
void handleRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException;
}
@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");
 }
}

HttpRequestHandler를 구현한 컨트롤러 (핸들러)를 등록하면,

BeanNameUrlHandlerMapping과 HttpRequestHandlerAdapter가 작동하여 핸들러를 처리함

 

2.3 @RequestMapping

 

- 가장 우선순위 높은 RequestMappingHandlerMapping과 Adapter을 통해 실행됨

- 거의 위 어노테이션 기반으로 컨트롤러와 메서드들을 등록함

 

3. 뷰 리졸버 

 

- OldController를 통해 뷰를 조회하도록  바꿔보자 

@Override
 public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
 System.out.println("OldController.handleRequest");
 return new ModelAndView("new-form");
 }

- 컨트롤러는 정성 호출 -> 랜더는 실패 -> 추가 설정 필요

 

application.properties에 다음 코드 추가 

spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp

 

> 뷰 리졸버 - InternalResourceViewResolver

 스프링 부트가 자동 등록하는 뷰 리졸버임

//ServletApplcation 내에서 아래와 같은 뷰리졸버 등록한다 -> 뷰리졸버가 따로 등록된게 없으면 이거 씀
@Bean
ViewResolver internalResourceViewResolver(){
   return new InternalResourceViewResolver("prefix","suffix")
}

스프링 부트가 얘를 호출해줌 

 

3.1 스프링 부트가 자동으로 등록하는 뷰 리졸버 

출처: 스프링MVC (인프런) -김영한

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

2. new-form이름을 빈으로 등록된 뷰객체 가 있는지 찾기 -> 없으면 다음 뷰 리졸버 호출(이미 빈에 자동 등록된)

3. InternalResourceViewResolver가 InternalResourceView객체를 반환

4. InternalResourceView는 JSP처럼 포워드 forwad() 호출해서 처리할  수 있는 경우 사용 (MyView랑 거의 같은 역할) 

4. view.render() -> 포워딩해서 jsp 실행 (이때 모델에 담긴 값 request로 옮김)

 

* 만약 jstl라이브러리가 있으면, JstlView가 반환됨 (InternalResourceViewResolver를 통해)

* View인터페이스를 구현한 View 객체를 빈으로 등록해두고, 컨트롤러가 해당 view객체 빈 이름을 return하면, 뷰 리졸버가 해당 View객체 리턴함 

 

 

4. 스프링 MVC 시작하기 

 

- 스프링이 제공하는 컨트롤러는 어노테이션 기반으로 동작함, 매우 유연하고 실용적

 

- @RequestMapping : 컨트롤러는 이 어노테이션을 사용함 

- @Controller : 어노테이션 기반 컨트롤러로 인식 -> @RequestMapping을 처리할 핸들러 맵핑 맵과, 어댑터 동작하도록 알림 

> 클래스 레벌에 @RequestMapping, @Controller를 매핑 정보로 인식한다.

 

* @Contorller를 통해 어노테이션 기반 매핑맵과, 어댑터 사용해야함 알림 

   > 해당 컨트롤러 처리할 어댑터를 통해 내부적으로 다시 request에서 url 뽑고 

   > 컨트롤러내에 url매핑된 메서드 실행

 

*이전까지 구조를 보면서 프론트 컨트롤러 어댑터 부분들 알아봤다면,

이제 스프링MVC에서 제공하는 기능을 사용하는 방법임 (컨트롤러 구성방법)

어노테이션 기반 컨트롤러 인식하고 처리하는 맵핑맵과 어댑터 있기 때문에

@Controller와 @RequestMapping을 통해 보다 자유롭게 

컨트롤러 만들 수 있음 (Controller 구현x, 메서드 명 및 매개변수 자유)

어댑터가 내부적으로 매핑정보에 따라 메서드 호출 + 매개변수 주입 해준다.

 

4.1 회원 등록 폼

 

@Controller
public class SpringMemberFormControllerV1 {
 @RequestMapping("/springmvc/v1/members/new-form")
 public ModelAndView process() {
 return new ModelAndView("new-form");
 }
}

@Controller : 스프링 빈으로 등록

                      어노테이션 기반 컨트롤러(핸들러)임을 인식

@RequestMapping: 요청 정보 매핑

                                 해당 url이 호출되면, 이 메서드가 호출됨

ModelAndView : 모델과 뷰 정보를 담아서 변환.

 

RequestMappingHandlerMapping은 스프링 빈 중에서 @RequestMapping 또는 @Controller가 클래스 레벨에 붙어있는 경우 매핑 정보로 인식한다.

 

따라서 아래 코드도 정확하게 동일하게 작동

@Component //컴포넌트 스캔을 통해 스프링 빈으로 등록
@RequestMapping
public class SpringMemberFormControllerV1 {
 @RequestMapping("/springmvc/v1/members/new-form")
 public ModelAndView process() {
 return new ModelAndView("new-form");
 }
}

 

// ServletApplication
// componentscan없이 직접 등록

@Bean
SpringMemberFormControllerV1 springMemberFormControllerV1() {
return new SpringMemberFormControllerV1();
}


@RequestMapping
public class SpringMemberFormControllerV1 {
 @RequestMapping("/springmvc/v1/members/new-form")
 public ModelAndView process() {
 return new ModelAndView("new-form");
 }
}

* 스프링 3.0부터는 이거 안됨 @Controller 필요

 

4.2 회원 저장 및 목록 

 

@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);
 System.out.println("member = " + member);
 memberRepository.save(member);
 ModelAndView mv = new ModelAndView("save-result");
 mv.addObject("member", member);
 return mv;
 }
 
 
 / 회원 목록 
 @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;
 }
}

 
}

mv.addObject()를 사용하면, ModelAndView 내부 Model에 손쉽게 데이터 추가 가능 

 

5. 스프링 MVC - 컨트롤러 통합

 

- @RequestMapping은 클래스 단위가 아니라 메서드 단위로 적용 따라서 컨트롤러 클래스를 

하나로 통합할 수 있다.

 

@Controller
@RequestMapping("/springmvc/v2/members")
public class SpringMemberControllerV2{

	private MemberRepositroy memberRepository = MemberRepository.getInstance();
    
    @RequestMapping("/new-form")
    public ModelAndView newForm(){
    	return new ModelAndView("new-form");
    }
    
    @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 mav = new ModelAndView("save-result");
        mav.addObject("member",member);
        return mav;
    }
    
    @RequestMapping
    public ModelAndView members(){
    List<Member> members = memberRepository.findAll();
	 ModelAndView mav = new ModelAndView("members");
	 mav.addObject("members", members);
 	return mav;
    
    }
}

 

6. 스프링 MVC 실용적인 방식 

 

-보통 이 방식 사용

 

@controller
@RequestMapping("springmvc/v3/members")
public class SpringMemberControllerV3{

		private MemberRepositroy 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 = memberRepositroy.findAll();
            model.addAttribute("members",members);
            return "members";
          }
}

 

- Model

  : 어댑터가 만들어서 넣어줌 (예전 ControllerV4처럼) 

 

- ViewName 직접 반환

 : V4처럼 컨트롤러가 ViewName만 반환하면 알아서 이를 토대로 

   ModelAndView만들어줌 (어댑터)

 

- RequestParam

 :  Http요청 파라미터를 위와 같이 받을 수 있음 (request.getParameter랑 같음)

 

- @RequestMapping, @GetMapping,@PostMapping

 : HTTP method 구분할 수 있음 

 

 

*Controller 인터페이스 구현해서 컨트롤러 만들면 메서드 오버라이드 해야해서 유연성 떨어지고,

  Mapping별로 컨트롤러 구현체 만들어야함 

 

  어노테이션 기반: pojo도 하나만 두고 어댑터가 여러 형식을 처리를 지원해줘서 

                               String,ModelAndView 리턴해도되고, 메서드명 자유임! 

                               매개변수도 request,response,Model 등등 약간의 자유가 있다.

 

  > 어댑터는 컨트롤러(핸들러) 실행 결과로 ModelAndView만 만들면 그만임 

     -- 디스팻쳐 역할---

     리졸버 호출 -> 리졸버가 ModelAndView에서 viewName꺼낸 후 View객체 만듦 

     랜더 호출 -> View에 render는  ModelAndView에서 Model꺼내가서 랜더링함 

 

> 어댑터는 요청 아까도 봤듯이 컨트롤러(핸들러)에 맞춤형으로 파라미터들 생성해서 넘겨주고 

    결과 받아서 ModelAndView 리턴하는 역할 

 

 

 

참고자료:https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1/dashboard

 

스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 - 인프런 | 강의

웹 애플리케이션을 개발할 때 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 MVC의 핵심 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., 원

www.inflearn.com