본문 바로가기
Develop/Spring

[Spring/MVC] 스프링 MVC 구조

by J-rain 2024. 3. 21.

 

SpringMVC 구조

  •  DispatcherServlet.doDispatch()  서블릿 등록 → DispatcherServlet 도 부모 클래스에서 HttpServlet을 상속 받아서 사용하고, 서블릿으로 동작한다.
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, ModelAndViewmv, 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);
}

 

컨트롤러가 호출되려면 다음 2가지가 필요하다.

  • HandlerMapping (핸들러 매핑)
    • 핸들러 매핑에서 이 컨트롤러를 찾을 수 있어야 한다.
    • ex) 스프링 빈의 이름으로 핸들러를 찾을 수 있는 핸들러 매핑이 필요하다.
  • HandlerAapter (핸들러 어댑터)
    • 핸들러 매핑을 통해서 찾은 핸들러를 실행할 수 있는 핸들러 어댑터가 필요하다.
    • ex) Controller 인터페이스를 실행할 수 있는 핸들러 어댑터를 찾고 실행해야 한다.

스프링은 이미 대부분 구현해서 개발자가 직접 만드는 일이 없다.

HandlerMapping

0 = RequestMappingHandlerMapping : 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용한다.

1 = BeanNameUrlHandlerMapping : 스프링 빈의 이름으로 핸들러를 찾는다.

HandlerAdapter

0 = RequestMappingHandlerAdapter : 애노테이션 기반의 컨트롤러인 @RequestMapping 에서 사용한다.

1 = HttpRequestHandlerAdapter : HttpRequestHandler 처리

2 = SimpleControllerHandlerAdapter : Controller 인터페이스 처리 (애노테이션X, 과거에 사용)

 

 

HttpRequestHandler

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

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");

	}
}
  1. 핸들러 매핑으로 핸들러 조회
    1. HandlerMapping을 순서대로 실행해서 핸들러를 찾는다.
    2. 이 경우 빈 이름으로 핸들러를 찾아야 하기 때문에 이름 그대로 빈 이름으로 핸들러를 찾아주는   BeanNameUrlHandlerMapping  이 실행에 성공하고 핸들러인  MyhttpRequestHandler  를 반환한다.
  2. 핸들러 어댑터 조회
    1.  HandlerAdapter  의  supports()  를 순서대로 호출한다.
    2.  HttpRequestHandlerAdapter  가  HttpRequestHandler  인터페이스를 지원하므로 대상이 된다.
  3. 핸들러 어댑터 실행
    1. 디스패치 서블릿이 조회한  HttpRequestHandlerAdapter  를 실행하면서 핸들러 정보도 함께 넘겨준다.
    2.  HttpRequestHandlerAdapter  는 핸들러인  MyHttpRequestHandler  를 내부에서 실행하고, 그 결과를 반환한다.

정리하자면

가장 우선순위가 높은 핸들러 매핑과 핸들러 어댑터는 애노테이션 기반의 컨트롤러를 지원하는

 @RequestMapping  을 사용함

 RequestMappingHandlerMapping   RequestMappingHandlerAdapter  이다.  @RequestMapping  의 앞글자를 따서 만든 이름들인데 실무에서 99.9% 이 방식의 컨트롤러를 사용한다.

 

뷰 리졸버

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

1 = BeanNameViewResolver : 빈 이름으로 뷰를 찾아서 반환 (ex: 엑셀 파일 생성 기능에 사용) 2 = InternalResourceViewResolver : JSP를 처리할 수 있는 뷰를 반환

 

 

스프링 MVC - 시작하기

스프링이 제공하는 컨트롤러는 애노테이션 기반으로 동작해서, 매우 유연하고 실용적이다. 과거에는 애노테이션이 없었다, 스프링도 초반엔 xml 방식으로 컨트롤러를 제공했었는데 당시엔 스트럿츠 외에도 여러 프레임워크가 있었다.하지만 애노테이션 기반의 컨트롤러가 등장하면서 스프링 완승으로 끝났다.

@RequestMapping

  • RequestMappingHandlerMapping
  • RequestMappingHandlerAdapter

위의 RequestMappingHandlerMapping와 RequestMappingHandlerMapping는 @RequestMapping의 앞글자를 따서 만든 이름이다. 대부분 실무의 99.9%는 이 방식의 컨트롤러를 사용한다.

 

SpringMemberFormControllerV1 - 회원 등록 폼

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

}

 

  •  @Controller 
    • 스프링이 자동으로 스프링 빈으로 등록한다. (내부에 @Component 애노테이션이 있어서 컴포넌트 스캔의 대상이 된다.)
    • 스프링 MVC에서 애노테이션 기반 컨트롤러로 인식한다.
  •  @RequestMapping 
    • 요청 정보를 매핑한다. 해당 URL이 호출되면 이 메서드가 호출되며, 애노테이션 기반으로 동작해서 메서드의 이름(위에서는 process() 사용) 은 임의로 지으면 된다.
  •  ModelAndView 
    • 모델과 뷰 정보를 담아서 반환하면 된다.

 

SpringMemberSaveControllerV1 - 회원 저장

@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;
    }
}

 

  •  mv.addObjec("member", member) 
    • 스프링이 제공하는  ModelAndView  를 통해 Model 데이터를 추가할 때는  addObject()  를 사용하면 된다. 이 데이터는 이후 뷰를 렌더링 할 때 사용된다

 

SpringMemberListControllerV1 - 회원 목록

@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 - 컨트롤러 통합

@RequestMapping 위치를 보면 메소드 레벨인걸 알 수 있다. 컨트롤러를 여러개 만들 필요 없이 하나로 통합할 수 있다.

 

@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("/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;
    }
}
  • 매핑 경로를 보면 메서드 레벨에서 /spring/v2/members 중복되는 걸 볼 수 있다. 이 중복을 해결하는 방법은 간단하다. 클레스 레벨에 @RequestMapping(중복경로)를 추가하면 된다.

 

스프링 MVC - 실용적인 방식

이전까지 보면서 매번 반복되는 게 더 있었다.

new ModelAndView <-- 매번 반환

이걸 개선한 방식이 존재한다.

 

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

    private final MemberRepository memberRepository = MemberRepository.getInstance();

    @RequestMapping
    public String members(Model model) {
        model.addAttribute("members", memberRepository.findAll());
        return "members";
    }

    @RequestMapping("/save")
    public String save(
                @RequestParam("usename") String username,
                @RequestParam("age") int age,
                Model model
                ) {

        Member member = new Member(username, age);
        memberRepository.save(member);

        model.addAttribute("member", member);
        return "save-result";
    }

    @RequestMapping("/new-form")
    public String newForm() {
        return "new-form";
    }
}

 

다양한 변화가 존재한다.

1. ModelAndView 제거.

  • 뷰네임을 반환하는 방식으로 변경
  • 모델은 Model 파라미터로 받음

2. @RequestParam 사용 (자세한 원리는 나중에)

  • HttpServletRequest.getParameter로 받아온 데이터를 직접 관리했지만, @RequestParam(key)를 이용해서 편하게 파라미터를 관리

추가적으로 GET, POST 호출 방식을 제한 걸 수 있다.

@RequestMapping(value = "/save", method = RequestMethod.POST)
public String save(...) {
    ...
}

이전까지는 GET, POST 상관없이 호출이 되었지만 이제 save는 POST 방식으로만 호출이 된다.

 

여기서 더 나아가

 @RequestMapping(value = "/save", method = RequestMethod.POST)  를 한번 더 개선된 기능이 나온다.

 @PostMapping  @GetMapping  으로 위의 기능을 하나로 합친 애노테이션이다.

 

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

    private final MemberRepository memberRepository = MemberRepository.getInstance();

    @GetMapping
    public String members(Model model) {
        ...
    }

    @PostMapping("/save")
    public String save(
        ...
    }

    @GetMapping("/new-form")
    public String newForm() {
        ...
    }
}

댓글