Spring MVC 다시 정리하기

YEON·2023년 11월 23일
1

My Spring

목록 보기
2/4
스프링 내부 동작을 잘 알고 사용하고 있는가 시리즈 (3)

# before 1 - 서블릿의 등장

과거의 웹 프로그램들은 주로 클라이언트의 요청에 대한 응답으로 정적인 페이지를 넘겨주었다.
하지만, 정적인 HTML 문서라면 화면이 계속 달라지는 수량 변화 등의 동적인 페이지를 만드는 것은 불가능하다.

그래서 이제는 응답을 가공하여 웹 서버가 동적인 페이지를 넘겨주도록 발전하였다.
이때, 웹서버가 동적인 콘텐츠를 생성할 수 있도록 도와주는 어플리케이션이 '서블릿' 인 것이다.

(* 동적인 페이지: 이미 정해진 페이지가 아닌 사용자가 요청한 시점에 페이지를 생성하여 전달되는 페이지)


# before 2 - Controller 의 발전

아래 코드는 이전의 스프링이 제공하던 Controller 인터페이스이다.

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

@Component("/mycontroller")
public class MyController implements Controller {
    @Override
    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
        // ...
        return null;
    }
}

이 Controller 가 지금 우리가 작성하는 @Controller 까지 어떻게 구성이 이뤄지게 되왔는지 살펴보자.



Servlet (서블릿)

서블릿(Servlet)은 클라이언트의 HTTP 요청과 응답을 처리하는 자바 클래스로서,
웹 서버의 동적인 웹 페이지를 만들거나 데이터를 처리하는데 사용된다.

클라이언트를 통해 들어온 Http 요청(Request)은 다음과 같다.

GET /api/products HTTP/1.1
Content-Type: application/json
User-Agent: PostmanRuntime/7.28.0
Accept: */*
Postman-Tocken: abcedf-abcdfg...
Host: localhost:8080
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

이때 만약 서블릿이 없었다면 개발자는 직접 HTTP 요청 메시지를 파싱하여 사용해야한다.
(ex. Header에서는 GET, POST, Content-Type, Body에서는 JSON.. 등)

하지만 서블릿은 개발자가 HTTP 요청 메시지를 편리하게 사용할 수 있도록 개발자 대신에 HTTP 요청 메시지를 파싱하여 HttpServletRequest 객체에 담아준다.

하지만, 컨트롤러 입장에서 HttpServletRequest, HttpServletResponse 객체를 매번 꼭 작성해줘야할까?
request 객체를 Model 로 사용하는 대신에 별도의 Model 객체를 만들어서 반환하며 컨트롤러가 서블릿 기술을 전혀 사용하지 않도록 만든 것이 Front Controller 패턴의 Dispatcher Servlet 이다.

특징

  • 클라이언트의 HTTP 요청, 응답을 도와주는 역할의 자바 프로그램
  • Java Thread를 이용하여 동작
  • MVC 패턴에서 Controller로 이용

서블릿 동작방식

서블릿은 각 URL 에 맞게 매핑을 해가지고 각기 다른 서블릿이 서로 다른 URL로 들어오는 요청을 맡아서 처리하는 방식으로 동작한다.
이에 대한 플로우는 다음과 같다.

  1. 사용자가 URL (ex. localhost:8080/hello) 을 입력한다.
  2. HTTP Request가 WAS의 Servlet Container로 전송한다. (ex. 톰캣)
  3. 요청을 전송받은 Servlet Container 는 HttpRequest, HttpResponse 객체를 생성한다.
  4. 사용자가 요청한 URL이 어느 서블릿에 대한 요청인지 찾는다.
  5. service 메서드를 호출한 후 클라이언트의 GET, POST 여부에 따라 doGet(), doPost()를 호출한다.
  6. 동적페이지를 생성한 후 HttpServletResponse 객체에 응답을 보낸다.
  7. 응답 후 HttpServletRequest, HttpServletResponse 객체를 소멸한다.

HttpServletRequest, HttpServletResponse 객체

HTTP 요청 메시지, HTTP 응답 메시지를 편리하게 사용하도록 도와주는 객체이다.

  • HttpServletRequest : 클라이언트의 HTTP 요청에 대한 정보를 제공하고 해당 요청을 처리하는 데 사용. (ex. HTTP 메서드 및 URI 정보 확인)
  • HttpServletResponse : 서버에서 클라이언트로의 HTTP 응답을 생성하고 전송하는 데 사용 (ex. 응답 헤더 및 쿠키 설정, 응답 상태 코드 설정)




Servlet Container

서블릿 컨테이너는 말 그대로 서블릿을 관리해주는 컨테이너이다. (ex. 톰캣)
자바는 웹 구현 기술로 Servlet을 사용하는데, 이 서블릿(생명주기 & 초기화 등)을 대신 관리하고 실행할 수 있도록(Ioc) 해주는 것이 Servlet Container이다.

Web Server + Web Container (Servlet Container) 로 이루워진 WAS의 일부이다.
때문에 WAS가 정적인 컨텐츠(Web Server)를 제공하면서도 웹 컨테이너를 이용해 내부 로직을 거쳐 동적 페이지(Web Container)를 보여줄 수 있게 되는 것이다.

동작 원리

사용자가 웹 애플리케이션에 요청을 보내면, 서블릿 컨테이너는 요청된 URL을 분석하여 해당 요청을 처리할 서블릿을 찾는다.
이를 위해 서블릿 컨테이너는 웹 애플리케이션의 설정 파일(ex. web.xml, 어노테이션 기반의 설정)을 참고한다.

서블릿 컨테이너는 이 매핑 정보를 기반으로 사용자의 요청이 어떤 서블릿으로 가야 하는지를 결정하고 없다면 해당 서블릿을 인스턴스화하고 초기화한다.

이후, 사용자 요청에 대한 서비스 메소드가 호출되어 실제로 해당 서블릿이 요청을 처리하고 응답을 반환한다.

특징

  • 톰캣처럼 서블릿을 지원하는 WAS를 서블릿 컨테이너라 한다
  • 서블릿 객체를 생성, 초기화, 호출, 종료하는 생명주기 관리
  • 클라이언트의 요청을 받고 응답 할 수 있도록 웹 서버와 소켓으로 통신
  • 서블릿 객체를 싱글톤으로 관리
  • 요청마다 자바 쓰레드를 새롭게 생성하며 동시 요청을 위한 멀티 쓰레드 처리 지원

Servlet Web MVC

Spring Web MVC가 Servlet을 사용하게 된다.

스프링이 서블릿을 구현하고 있다.
서블릿이 인터페이스를 상속받아 구현하고 있는데 그것을 서블릿 컨테이너가 가지고 있고 그것이 서버와 통신한다.

CGI

http 통신규약을 사용하는 웹 서버가 웹 애플리케이션과 데이터를 주고 받는 처리 규약
CGI 일때는 1클라이언트 1프로세스여서 비효율적이었는데 서블렛을 통해서 멀티 쓰레드 등이 가능해졌다? 그래서 우리가 서블릿을 사용하게 되었다.

서블릿도 CGI규칙에 따라 데이터를 주고 받지만 이를 서블릿을 가지고 있는 컨테이너에게 위임하고 대신 서블릿 컨테이너와 서블릿 사이의 규칙이 존재.

Servlet이 컨테이너 내부에서 쓰레드 단위로 요청을 처리하고 그것이 어떤 생명 주기를 지닌다.




DispatcherServlet (디스패처 서블릿)

DispatcherServlet도 HttpServlet을 상속받은 Servlet으로,
표현 계층(Presentation layer) 전면에서 HTTP 프로토콜을 통해 들어오는 모든 요청을 중앙집중식으로 처리하는 프론트 컨트롤러(Front Controller) 이다.

한마디로, 클라이언트로부터 어떤 요청이 들어오면 서블릿 컨테이너(ex. 톰캣)이 요청을 받는데 이때 공통 작업은 DispatcherServlet 에서 처리 하고, 이외 작업은 적절한 세부 컨트롤러로 위임하는 역할을 한다.

서블릿을 직접 사용하게 되면 매번 모든 서블릿을 URL 매핑을 위해 web.xml에 모두 등록해주어야하는데,
컨트롤러 호출 전에 먼저 공통 기능을 처리하는 수문장 같은 역할로서 프론트 컨트롤러 패턴인 DispatcherServlet을 도입함으로써
우리는 이제 컨트롤러를 구현해두기만 하면 스프링 혼자 알아서 컨트롤러를 주입받고 사용할 수 있는 것이다.

이렇게 프론트 컨트롤러 역할을 분리하면서 컨트롤러가 서블릿에 종속적이지 않도록 변경하여 보다 객체지향적인 개발이 가능하도록 설계된 것을 확인할 수 있다. (개발자는 이제 DispatcherServlet 웹 요청 처리 관련 구현체들을 사용하면서 실제로 작성해야 하는 핸들러 즉 비즈니스 로직에만 집중할 수 있기 때문이다.)

그리고 이러한 일련의 과정들을 바탕으로해서 만들어진 프레임워크가 바로 Spring Web MVC 이다.

FrontController 패턴

: "여러 요청을 처리하는데 반복적으로 등장하게 되는 공통 작업을 하나의 오브젝트에서 일괄적으로 처리하게 만드는 방식"
주로 서블릿 컨테이너의 제일 앞에서 서버로 들어오는 클라이언트의 모든 요청을 받아서 처리해주는 컨트롤러로써, MVC 구조에서 함께 사용되는 디자인 패턴이다.
디스패처 서블릿은 이러한 프론트 컨트롤러로서, HTTP 프로토콜로 들어오는 모든 요청을 가장 먼저(Front) 받아 적합한 컨트롤러에 위임(Dispatch)해준다.


HandlerMapping (핸들러 매핑)

컨트롤러가 호출되기 위해서는 해당 컨트롤러(핸들러)를 찾는 과정이 필요한데, 이를 핸들러 매핑이라고 한다.
클라이언트의 요청 URL을 특정 핸들러(컨트롤러)에 매핑시키는 것이다.

스프링 부트는 다음과 같은 우선 순위로 핸들러(컨트롤러)를 찾는다.

  1. RequestMappingHandlerMapping : 스프링 빈 중에서 @RequestMapping 혹은 @Controller 가 클래스 레벨에 붙어 있는 경우에 매핑 정보로 인식한다.
  2. BeanNameUrlHandlerMapping : 스프링 빈 이름으로 핸들러를 찾는다.

HandlerAdapter (핸들러 어댑터)

일치하는 컨트롤러(핸들러)를 찾았다면, 해당 컨트롤러가 실제로 실행되도록 도와주는 역할을 핸들러 어댑터라고 한다.
어댑터라는 말은 어댑터 패턴과 동일하게 다양한 핸들러 타입(메서드, 컨트롤러 등)을 동일한 방식으로 실행할 수 있도록 지원하는 것이다.
(ex. Controller 인터페이스나 @Controller 어노테이션이 붙은 클래스의 메서드 등을 특정한 방식으로 실행할 수 있도록 돕는다.)

스프링 부트는 다음과 같은 우선 순위로 핸들러 어댑터를 처리한다.
해당 코드를 살펴보면 DispatcherServlet에서 이 어댑터의 handle을 호출한다.

  1. RequestMappingHandlerAdapter : 어노테이션 기반 컨트롤러 @RequestMapping 인걸 처리한다.
  2. HttpRequestHandlerAdapter : HttpRequestHandler 를 처리한다.
  3. SimpleControllerHandlerAdapter : Controller 인터페이스를 처리한다. (어노테이션 Controller 아님)

HandlerMethod (핸들러 메서드)

핸들러 메서드는 핸들러(컨트롤러)의 일부로, 클라이언트의 요청에 대한 응답을 생성하는 등 클라이언트의 요청을 처리하는 실제 로직이 담겨 있는 메서드이다.

Spring 에서는 @RequestMapping 어노테이션을 사용하여 특정 URL에 매핑되는 핸들러 메서드를 정의할 수 있다.
핸들러 메서드는 컨트롤러에서 특정 URI 패턴에 맞춰 호출되는 메서드로서, 요청을 처리하고 모델을 생성하여 반환하거나, 뷰 이름을 반환하여 뷰를 결정하는 등의 역할을 수행한다.




@Controller 와 어노테이션

@RequestMapping

이제 우리가 작성하는 컨트롤러를 살펴보자.

@Controller
@RequestMapping("/v1/user")
public class UserController {
	
    @GetMapping("")
    public ModelAndView getUser(
    	@RequestParam("id") Long id
    ) {
    	// 로직
    	return new ModelAndView("");
    }
    
    @PostMapping("")
    @ResponseBody
    public User saveUser(
    	@RequestBody UserRequest request
    ) {
    	// 로직
    	return new User();
    }
}
  • @Controller : 스프링이 자동으로 빈으로 등록하고, 애노테이션 기반 컨트롤러(핸들러)임으로 인식한다.
  • @RequestMapping : 요청 정보를 매핑한다. 현재 가장 우선순위가 높은 핸들러 매핑과 핸들러 어댑터는 ReqeustMappingHandlerMapping, RequestMappingHandlerAdapter 이다.
  • @RestController : @Controller에 + @ResponseBody가 추가된 것이다.

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

@ResponseBody

그리고 이때 @RequestParam, @RequestBody 또한 어노테이션을 통해서 정리가 되어있는데

  • @RequestParam : 이전의 request.getParameter("id") 와 거의 동일한 코드로 HTTP 요청 파라미터를 대신 받는다.
  • @RequestBody : HTTP 메시지 컨버터가 HTTP 메시지 바디의 내용을 우리가 원하는 문자,객체로 변환해준다.
  • @ResponseBody : HTTP Body에 문자 내용을 직접 반환하며, viewResolver 대신 HttpMessageConvertor 가 동작한다.

JSON 요청 -> HTTP 메시지 컨버터 -> 객체 -> HTTP 메시지 컨버터 -> JSON 응답 의 플로우로 동작하게 된다.

HttpMessageConvertor

ArgumentResolver







정리


디스패처 서블릿 플로우 요약

클라이언트로부터 HTTP 요청이 들어오면 톰캣과 같은 서블릿 컨테이너가 요청을 받게 된다.
그리고 이 모든 요청을 프론트 컨트롤러인 디스패처 서블릿이 가장 먼저 받게 된다.
그럼 디스패처 서블릿은 공통적인 작업를 먼저 처리한 후에 해당 요청을 처리해야 하는 컨트롤러를 찾아서 작업을 위임한다.

이렇게 프론트 컨트롤러 역할을 하는 디스패처 서블릿 덕분에, 컨트롤러는 서블릿에 종속적이지 않고 개발자는 실제 핸들러/비즈니스 로직에만 집중하여 개발하여 객체지향적 개발이 가능하게 되었다.

Spring 컨트롤러(@Controller)의 플로우

  1. 스프링 실행시 서블릿 컨테이너(ex.톰캣)가 초기화된다.
  2. DispatcherServlet이 설정 파일에서 정의된 핸들러 매핑 Bean을 확인하고 초기화한다.
  3. 클라이언트의 HTTP 요청이 들어오면 DispatcherServlet이 핸들러 매핑에게 전달한다.
  4. 핸들러 매핑은 @Controller 어노테이션이 붙은 클래스들 중에서 적절한 컨트롤러(핸들러)를 찾아 반환한다.
  5. 핸들러 어댑터는 찾은 핸들러(컨트롤러)의 메서드를 실행하여 요청을 처리한다.
  6. 컨트롤러(핸들러)가 처리한 결과를 기반으로 디스패처 서블릿은 HTTP 응답을 생성한다.
  7. 생성된 HTTP 응답이 클라이언트로 전송되어 화면에 표시됩니다.

단, 스프링 부트(Spring Boot) 의 경우

  • 내장된 서블릿 컨테이너를 포함하고 있기 때문에 별도의 서블릿 컨테이너 설정이 필요하지 않고, 빌드시 내장된 서블릿 컨테이너가 초기화된다.
  • DispatcherServlet도 스프링 부트가 자동으로 구성하는 기본 설정을 사용하고 (빈으로 등록된) @Controller에 대해 따로 핸들러 매핑을 설정하지 않아도 된다.







[참고]

https://tecoble.techcourse.co.kr/post/2021-05-23-servlet-servletcontainer/ / 서블릿
https://www.inflearn.com/questions/505167/%EC%84%9C%EB%B8%94%EB%A6%BF-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-was%EA%B0%80-%ED%97%B7%EA%B0%88%EB%A6%BD%EB%8B%88%EB%8B%A4 / 서블릿 질문1
https://www.inflearn.com/questions/405451/%EC%84%9C%EB%B8%94%EB%A6%BF-%EC%83%9D%EC%84%B1-%EC%8B%9C%EC%A0%90%EA%B3%BC-%EC%97%AD%ED%95%A0-%EA%B4%80%EB%A0%A8-%EC%A7%88%EB%AC%B8%EC%9E%85%EB%8B%88%EB%8B%A4 / 서블릿 질문2
https://mangkyu.tistory.com/18
https://mangkyu.tistory.com/180
https://non-stop.tistory.com/534
https://tecoble.techcourse.co.kr/post/2021-06-25-dispatcherservlet-part-1/ / 디스패처서블릿

profile
- 👩🏻‍💻

2개의 댓글

comment-user-thumbnail
2023년 12월 30일

면접준비에 많은 도움이 되었어요! 감사합니다 👏

1개의 답글