스프링 MVC 핵심 기술

지윤·2021년 7월 20일
0

Spring

목록 보기
4/7

스프링 MVC 1편- 백엔드 웹 개발 핵심 기술을 듣고 정리

서블릿

웹 어플리케이션 서버를 직접 구현하려면 웹 브라우저가 생성한 HTTP 요청 메시지를 모두 직접 파싱해서 읽고 요청 메시지도 직접 생성해야 한다. 이 과정은 매우 어렵고 비효율적이다.

개발자가 비지니스 로직에만 집중해서 개발할 수 있도록 HTTP 내용을 대신 파싱해서 읽어주고 응답 메시지도 생성해주는 바로 서블릿이다.

  • HttpServlet을 상속받아서 구현한다.
  • service라는 메서드를 재정의한다. 이때 파라미터로 HttpServletRequest, HttpServletResponse을 받는다.
  • HTTP 요청을 통해 매핑된 URL이 호출되면 서블릿 컨테이너가 service 메서드를 자동으로 실행한다.
  • HTTP 요청 메시지 로그 확인하려면 properties에logging.level.org.apache.coyote.http11=debug를 추가

@WebServlet(name = "helloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {

	@Override
	protected void service(HttpServletRequest request,
				HttpServletResponse response)
				throws ServletException, IOException {
		System.out.println("HelloServlet.service");
	}
}

서블릿 컨테이너 동작 방식

스프링 부트가 내장 톰켓 서버를 통해 서블릿 컨테이너에 서블릿 객체를 생성해서 담는다.


HttpServletRequest

서블릿은 HTTP 요청 메시지를 파싱해서 HttpServletRequest 객체에 담아서 제공한다.
HTTP 요청 메시지를 조회하려면 해당 객체를 통해서 getXXX()를 호출하면 된다.

HTTP 요청 메시지 전달 방법(중요)

  1. GET - 쿼리 파라미터
  2. POST - HTML Form
    • content-type: application/x-www-form-urlencoded
    • 메시지 바디에 쿼리 파리미터 형식으로 전달 username=hello&age=20
  3. HTTP Message Body
    • 주로 JSON 사용, Jackson 라이브러리 사용

템플릿 엔진

서블릿에서 동적인 HTML을 만들 수 있지만 매우 복잡하고 비효율적이다. HTML 문서에 동적으로 변경해야 하는 부분만 Java 코드를 넣는 편이 편리하다. 그래서 등장한 것이 템플릿 엔진이며 JSP, Thymeleaf 등이 있다.



MVC 패턴

서블릿과 JSP를 사용하면서 JSP가 뷰 기능보다 비즈니스 기능을 더 많이 담당하게 되면서 유지보수의 지옥에 빠지 되었다. HTTP 요청을 서블릿이 대신 담당하듯이 비즈니스 로직을 JSP가 아닌 다른 곳에서 처리할 수 있도록 분리하기 위해 MVC 패턴이 등장했다. Model, View, Controller를 의미한다.

  • Model: 뷰에 츌력할 데이터를 담는다.

  • View: 모델에 담겨있는 데이터를 사용해서 화면을 나타낸다.

  • Controller: HTTP 요청을 받아서 파라미터를 검증하고 비즈니스 로직을 실행한다. 그리고 뷰에 전달할 결과 데이터를 조회해서 모델에 담는다.

  • service
    컨트롤러가 너무 많은 역할을 담당하기 때문에 비즈니스 로직만 따로 분리한 것이 서비스 계층이다. 컨트롤러에서는 서비스를 호출하기만 한다.

MVC 패턴의 한계

컨트롤러와 뷰를 통해 역할을 구분했지만 컨트롤러들 간에 중복(비슷한 코드)이 너무 많다.

  • 포워드 중복
  • view path 중복

한마디로 공통처리가 어렵다. 이를 해결하기 위해서 컨트롤러를 호출하기 전에 공통기능을 처리할 수 있는 프론트 컨트롤러 패턴을 도입하게 된다.



프론트 컨트롤러 패턴

FrontController 패턴 특징

  • 프론트 컨트롤러 서블릿 하나로 클라이언트의 요청을 받음
  • 프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출
  • 공통 처리 가능
  • 프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용하지 않아도 됨

스프링 MVC의 DispatcherServlet이 FrontController 패턴으로 구현되어 있다.
서블릿 컨테이너에서 HTTP 프로토콜을 통해 들어오는 모든 요청을 프레젠테이션 계층의 맨 앞에 DispatcherServlet을 둬서 중앙집중식으로 처리하도록 한다.
DispatcherServletdms 공통 작업을 처리하고 적절한 세부 컨트롤러로 작업을 위임한다.

단계별 프론트 컨트롤러 패턴 적용(강의 자료 참고)

  1. 프론트 컨트롤러 도입
  2. View 분리
    • 컨트롤러에서 뷰로 이동하는 로직 중복을 제거하기 위해 MyView 객체 생성
  3. ModelView 객체 추가
    • HttpServletRequest를 사용하지 않고 서블릿 종속성을 제거하기 위해 Model 객체를 별도로 생성
  4. ModelView가 아닌 ViewName(뷰의 논리이름)을 반환하도록 변경
  5. 어댑터 패턴 적용하여 유연성, 확장성 향상
    • 핸들러 어댑터
    • 핸들러



스프링 MVC 구조

DispatcherServlet

  • 부모 클래스에서 HttpServlet을 상속 받아서 사용하며, 서블릿으로 동작한다.
    (DispatcherServlet -> FrameworkServlet -> HttpServletBean -> HttpServlet)
  • 스프링 부트는 DispacherServlet 을 서블릿으로 자동으로 등록하면서 모든 요청 경로(urlPatterns="/")에 대해서 매핑한다.

요청 흐름

  • 서블릿이 호출되면 HttpServlet 이 제공하는 serivce()가 호출된다.
    (스프링 MVC는 DispatcherServlet 의 부모인 FrameworkServlet 에서 service() 를 오버라이드해두었다.)
  • FrameworkServlet.service() 를 시작으로 여러 메서드가 호출되면서
  • DispacherServlet.doDispatch() 가 호출된다. doDispatch()에서 위에서 봤던 핸들러 어댑터, 핸들러를 조회, 실행부터 뷰 렌더링까지 수행한다.

스프링 MVC 동작 과정

  1. 핸들러 조회: 핸들러 매핑을 통해 요청 URL에 매핑된 핸들러(컨트롤러)를 조회
  2. 핸들러 어댑터 조회: 핸들러를 실행할 수 있는 핸들러 어댑터를 조회
  3. 핸들러 어댑터 실행: 핸들러 어댑터를 실행한다.
  4. 핸들러 실행: 핸들러 어댑터가 실제 핸들러를 실행한다.
  5. ModelAndView 반환: 핸들러 어댑터는 핸들러가 반환하는 정보를 ModelAndView로 변환해서 DispatcherServlet에 반환
  6. viewResolver 호출: 뷰 리졸버를 찾고 실행
    JSP의 경우: InternalResourceViewResolver 가 자동 등록되고, 사용된다.
  7. View 반환: 뷰 리졸버는 뷰의 논리 이름을 물리 이름으로 바꾸고, 렌더링을 수행하는 뷰 객체를 반환
    JSP의 경우 InternalResourceView(JstlView) 를 반환하는데, 내부에 forward() 로직이 있다.
  8. 뷰 렌더링: 뷰를 통해서 뷰를 렌더링(서버로부터 HTML 파일을 받아 브라우저에 뿌려주는 과정)한다.

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

스프링 부트가 자동 등록하는 핸들러 매핑과 핸들러 어댑터
우선순위 = 종류

HandlerMapping
0 = RequestMappingHandlerMapping : 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
1 = BeanNameUrlHandlerMapping : 스프링 빈의 이름으로 핸들러를 찾는다.

HandlerAdapter
0 = RequestMappingHandlerAdapter : 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
1 = HttpRequestHandlerAdapter : HttpRequestHandler 처리
2 = SimpleControllerHandlerAdapter : Controller 인터페이스(애노테이션X, 과거에 사용) 처리

작동 순서

  1. 핸들러 매핑으로 핸들러 조회
    • HandlerMapping 을 순서대로 실행해서, 핸들러를 찾는다
  2. 핸들러 어댑터 조회
    • HandlerAdapter 의 supports() 를 순서대로 호출한다.
  3. 핸들러 어댑터 실행

뷰 리졸버

InternalResourceViewResolver
스프링 부트가 이 리졸버를 자동으로 등록하는데, 이때 application.properties에 등록한 spring.mvc.view.prefix , spring.mvc.view.suffix 설정정보를 사용해서 등록한다.

스프링 부트가 자동 등록하는 뷰 리졸버
1 = BeanNameViewResolver : 빈 이름으로 뷰를 찾아서 반환한다.
2 = InternalResourceViewResolver : JSP를 처리할 수 있는 뷰를 반환한다.

작동 순서
1. 핸들러 어댑터를 통해 논리 뷰 이름을 얻는다.
2. 논리이름을 토대로 뷰 리졸버를 호출한다.
3. 뷰 리졸버는 뷰 객체를 반환한다.
4. 뷰 객체는 reder()를 호출한다.

스프링 MVC 실용적인 기능

  1. Model 파라미터
  2. ViewName 직접 반환
  3. @RequestParam 사용
    • HTTP 요청 파라미터를 받을 수 있다. reqeust.getParameter("..")와 같다.
  4. @RequestMapping @GetMapping, @PostMapping
    • URL만 매칭하는 것이 아닌 HTTP Method도 함께 구분할 수 있다.

스프링 MVC 기능

HTTP 요청을 어떻게 매핑하고 어떻게 응답하는지 알아보자!

요청(Request)

클라이언트가 서버로 요청하는 방법은 크게 요청파라미터를 사용하는 법과 메시지 바디를 사용하는 법으로 나뉜다.

요청 기본, 헤더 조회 방법

  • HttpServletRequest, HttpServletResponse
  • HttpMethod
  • Locale
  • @RequestHeader MultiValueMap<String, String> headerMap: 모든 HTTP 헤더를 MultiValueMap 형식으로 조회한다.
  • @CookieValue(value = "myCookie", required = false) String cookie: 특정 쿠키를 조회한다.
@Slf4j
@RestController
public class RequestHeaderController {

    @RequestMapping("/headers")
    public String headers(HttpServletRequest request,
                          HttpServletResponse response,
                          HttpMethod httpMethod,
                          Locale locale,
                          @RequestHeader MultiValueMap<String, String> headerMap,
                          @RequestHeader("host") String host,
                          @CookieValue(value = "myCookie", required = false) String cookie) {

        log.info("request={}", request);
        log.info("response={}", response);
        log.info("httpMethod={}", httpMethod);
        log.info("locale={}", locale);
        log.info("headerMap={}", headerMap);
        log.info("header host={}", host);
        log.info("myCookie={}", cookie);
        return "ok ok";
    }

}

참고로 Contoller에 사용 가능한 파라미터 목록은 공식 메뉴얼 참고

1. 요청 파라미터(쿼리파라미터, HTML Form, @RequestParam, @ModelAttribute)

  • GET(쿼리 파라미터), POST(HTML Form) 둘다 형식이 같으므로 HttpServletRequest의 request.getParameter()로 조회할 수 있다.
  • POST는 메시지 바디 내에 쿼리 파라미터 형식으로 전달

@RequestParam

@RequestParam("변수명") String 파라미터명
  • 파라미터 이름으로 바인딩된다.
  • request.getParameter("변수명")과 같다.
  • 이때, 파라미터명과 변수명이 같으면 변수명 생략 가능
  • String, int 등 단순 타입이면 @RequestParam 어노테이션도 생략 가능
  • 파라미터 필수 여부는 required 속성으로 설정할 수 있다.
  • /request-param?username= 와 같은 경우 ->
    파라미터 이름만 있고 값이 없으면 null이 아닌 빈 문자로 인식된다.
  • defualtValue 속성을 사용하면 기본형에 null이 들어왔을 때 나타나는 500 에러를 막을 수 있다.

@ModelAttribute

파라미터를 받아서 필요한 객체를 만들고 그 객체에 값을 넣어주어야 하는데, 스프링은 이 과정을 자동으로 처리해주는 @ModelAttribute를 제공한다. @RequestParam처럼 생략 가능하다.

  1. 객체 생성
  2. 요청 파라미터 이름으로 해당 객체의 프로퍼티를 찾는다. 프로퍼티의 setter를 호출해서 파라미터 값을 바인딩한다.

@RequestParam vs @ModelAttribute

  • String , int , Integer 같은 단순 타입 - @RequestParam
  • 나머지(argument resolver로 지정해둔 타입 외) - @ModelAttribute

2. 메시지 바디

HTTP message body에 데이터를 담아서 요청하는 경우 요청 파라미터가 아니기 때문에 @RequestParam, @ModelAttribute를 사용할 수 없다.

요청 메시지(단순 텍스트)

HTTP 메시지 바디의 데이터를 읽는 방법

  1. InputStream 사용
  2. HttpEntity 사용(HttpEntity는 응답에도 사용 가능)
  3. @RequestBody, @RequestHeader 사용

참고로 스프링은 HTTP 메시지 바디를 읽어서 문자나 객체 등으로 값을 변환하여 전달해주는데, 이때 HTTP 메시지 컨버터 기능을 사용한다.

요청 메시지(JSON)

HTTP 메시지 바디의 데이터를 읽는 방법

  1. HttpServletRequest를 사용해서 직접 HTTP 메시지 바디를 읽어와서 문자로 변환한다. 문자로 변환된 JSON 데이터를 Jackson 라이브러리인 objectMapper를 사용해서 자바 객체로 변환한다.

  2. @RequestBody로 메시지 바디 데이터를 꺼내서 String 변수로 받는다. 이때, StringHttpMessageConverter가 작동한다. 이후 objectMapper를 사용해서 변환한다.

    -> 문자로 변환 후 자바 객체로 변환하는 과정이 불편하며, 한 번에 자바 객체로 변환할 수 없을까?

  3. @RequestBody + 객체 파라미터
    이때, MappingJackson2HttpMessageConverter가 사용되며 메시지 바디 내용을 읽어 자바 객체로 매핑한다.
    또한, @RequestBody는 생략할 수 없다. 생략하면 @ModelAttribute로 적용되기 때문이다.

  4. HttpEntity.getBody() 사용

정리

  • 요청 파라미터를 조회하는 기능: @RequestParam , @ModelAttribute
  • HTTP 메시지 바디를 직접 조회하는 기능: @RequestBody

응답(Response)

서버에서 응답 데이터를 만드는 방법은 크게 3가지이다.

1. 정적 리소스
- src/main/resources
2. 뷰 템플릿
- src/main/resources/templates
3. HTTP 메시지 바디 직접 입력(HTTP API 제공하는 경우)
- HttpServletResponse의 response.getWriter() 사용
- ResponseEntity(HttpEntity 상속) 사용 - HTTP 응답 코드 설정 가능
- @ResponseBody 사용

@ResponseBody는 응답 코드 설정 불가능하여, @ResponseStatus(HttpStatus.OK) 사용하면 된다. 그래도 애노테이션이기 때문에 동적으로 변경할 수는 없으므로, 조건에 따라 동적으로 변경하려면 ResponseEntity를 사용하면 된다.


HTTP 메시지 컨버터

지금까지 HTTP 메시지 바디를 읽는 경우를 봤을 때, 다양한 상황(문자, 객체 등)에 따라 알맞게 데이터를 변환해주는 역할을 누가 수행하는 걸까?
바로 메시지 컨버터이다.

@ResponseBody를 사용하면, 뷰 리졸버 대신 HttpMessageConverter가 동작하는데 종류는 아래와 같다.

  • 기본 문자처리: StringHttpMessageConverter
  • 기본 객체처리: MappingJackson2HttpMessageConverter
  • 기타 등등 여러개 존재

스프링 부트 기본 메시지 컨버터
0 = ByteArrayHttpMessageConverter
1 = StringHttpMessageConverter
2 = MappingJackson2HttpMessageConverter

동작 조건
스프링 MVC는 다음의 경우에 HTTP 메시지 컨버터를 적용한다.

  • HTTP 요청: @RequestBody , HttpEntity(RequestEntity) 사용된 경우
  • HTTP 응답: @ResponseBody , HttpEntity(ResponseEntity) 사용된 경우

동작 과정
1. 스프링 부트는 대상 클래스 타입미디어 타입 두가지를 모두 체크해서 메시지 컨버터를 결정한다. 클래스 타입이란 컨트롤러의 반환 타입 정보이고, 미디어 타입은 HTTP Accept 해더 정보이다.
2. HttpMessageConverter클래스의 canRead(), read(), canWriter(), write()를 이용하여 요청 데이터를 읽고 응답 데이터를 생성한다.
3. 클래스 타입과 미디어 타입 둘 중 하나라도 맞지 않으면 컨버터로 결정되지 않는다.

요청 매핑 핸들러 어댑터

위에서 알아본 HTTP 메시지 컨버터는 스프링 MVC 구조상 정확히 어디에서 사용되는 것일까?

애노테이션 기반의 컨트롤러에서 사용되는 @RequestMapping을 처리하는 핸들러 어댑터인 RequestMappingHandlerAdapter를 보자.

ArgumentResolver란?

애노테이션 기반 컨트롤러는 매우 다양한 타입의 파라미터를 사용할 수 있는데, 파라미터에 값을 셋팅해서 넣어주는 역할을 ArgumentResolver가 담당한다.

즉, RequestMappingHandlerAdapter는 ArgumentResolver를 호출해서 컨트롤러가 필요로 하는 다양한 파라미터 값을 생성하여 컨트롤러를 호출하면서 값을 넘겨준다.

  • @RequestBody, @ResponseBody가 있으면
    RequestResponseBodyMethodProcessor (ArgumentResolver)를 사용
  • HttpEntity가 있으면 HttpEntityMethodProcessor (ArgumentResolver)를 사용

ReturnValueHandler란?

ArgumentResolver와 비슷하며, 응답 값을 변환한다.

이를 통해 알 수 있는 HTTP 메시지 컨버터의 위치

HTTP 메시지 바디를 처리해야 하는 요청 또는 응답의 경우 ArgumentResolver가 HTTP 메시지 컨버터를 사용한다.

확장

스프링은 다음을 모두 인터페이스로 제공한다.

  • HandlerMethodArgumentResolver
  • HandlerMethodReturnValueHandler
  • HttpMessageConverter

만약 해당 기능을 확장하고 싶다면 WebMvcConfigurer를 상속받아서 스프링 빈으로 등록하고, 확장하고 싶은 기능의 메서드를 재정의하면 된다.

profile
헬로🙋‍♀️

0개의 댓글