김영한 님의 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 강의를 보고 작성한 내용입니다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1/dashboard
웹 서버 : 정적 리소스 제공
WAS : 프로그램 코드를 실행해서 어플리케이션 로직 수행
사용자마다 서로 다른 페이지를 보여줄 수 있음
동적 HTML, HTTP API( JSON ) 제공 가능
서블릿, JSP, 스프링 MVC 등이 WAS에서 동작
JSP : 서버로부터 데이터를 받아 동적으로 화면이 그려지는 템플릿 엔진 ( SSR 기술 )
HTTP API : HTML이 아닌 데이터를 전달 ( 주로 JSON 형식 )
WAS의 Servlet Container가 servlet 객체를 생성
클라이언트가 해당 servlet을 사용하는 http 요청을 하면, Servlet Container에서 request, response 객체 생성
이때, Thread가 Servlet 객체를 호출하고 request,response 객체를 Servlet 객체에게 넘겨준다
request 객체를 활용해 Servlet의 비즈니스 로직 실행
응답 결과를 response 객체에 담은 후, Servlet Container에 전달
Servlet Container가 http 응답 메시지 생성 후 클라이언트에게 전달
서블릿 : 동적 웹 페이지를 만들 때 사용되는 자바 기반의 웹 애플리케이션 프로그래밍 기술
WAS = Web Server + Servlet Container
WAS는 HTTP 요청 메세지를 기반으로 Request, Response 객체를 만들고 service() 를 호출하면서 객체들을 파라미터로 넘겨준다
request, response 객체는 요청이 올 때마다 새로 생성된다
Servlet Container
HTTP 요청 메세지를 파싱하고 HttpServltRequest
객체 생성 ( 헤더, message body에 관한 정보를 가짐 )
HttpServletResponse
객체로 HTTP 응답 메세지를 생성해 반환
서블릿 생성 & 싱글톤으로 관리
서블릿을 호출
init()
: 서블릿 클래스 로드 ➜ 인스턴스 생성 ➜ init() 메서드를 호출해서 초기화
service()
init() 메서드가 성공적으로 완료된 후에만 호출된다
서블릿 컨테이너는 클라이언트에서 오는 요청을 처리하기 위해 service() 메서드를 호출하고 HTTP 요청 유형( GET , POST , PUT , DELETE )을 해석하고 doGet , doPost , doPut , doDelete 등의 메서드를 적절하게 호출한다
destroy()
: 서블릿 컨테이너에 의해 호출
@ServletComponentScan
Servlet을 자동 등록하기 위해 main 메서드가 있는 클래스가 붙어야 하는 어노테이션
@WebFilter
, @WebListener
및 @WebServlet
에 대한 스캔을 활성화하기 위해 추가하는 어노테이션
@WebServlet()
: 클래스 레벨에 붙여서 url과 Servlet을 매핑한다
서블릿은 HTTP 요청 메세지를 파싱하고 그 결과를 HttpServletRequest 객체에 담아서 제공한다
request-line 조회 / header 조회 / 요청 데이터 조회 / 임시 저장소 기능 / 세션 기능이 존재
요청 데이터 조회는 쿼리 파라미터 조회 / HTML Form 데이터 조회 / message body 조회로 나눌 수 있다
쿼리 파라미터, HTML Form 데이터 조회
getParameter("name")
, getParameters()
를 사용한다
message body에 데이터가 넘어오는 경우 중 HTML Form 방식은 message body의 내용이 쿼리 파라미터 형식과 동일하므로 쿼리 파라미터 조회하는 메서드를 이용
message body 조회는 텍스트 형식인 경우, JSON 형식인 경우로 나뉜다
텍스트 형식의 경우 : getInputStream()
+ StreamUtils.copyToString(inputStream,StandardCharsets.UTF_8)
JSON 형식의 경우 : getInputStream()
+ StreamUtils.copyToString(inputStream,StandardCharsets.UTF_8)
+ objectMapper.readValue()
HTTP 응답 메세지를 작성할 때 사용
상태 코드 작성 / 헤더 작성 / message body에 내용 작성을 할 수 있다
헤더의 경우 쿠키 / 리다이렉트 등도 지정 가능하다
헤더 작성은 setHeader()
도 가능하지만 setXXX()
도 가능하다 ( XXX는 헤더 이름 )
message body에 내용 작성을 위해 PrintWriter writer = response.getWriter();
코드가 우선적으로 필요
message body에 텍스트 / html / JSON 데이터를 담을 수 있다
텍스트를 담을 때 writer.println("text");
html을 담을 때 writer.println("html태그");
를 이용하고 Content-type을 text/html로 지정하고 인코딩 방식도 지정해야함 ( utf-8 )
JSON 형식의 경우 objectMapper.writeValueAsString()
를 통해 객체를 String으로 변환시키고, response.getWriter().write()
를 통해 message body에 작성하는데 Content-Type을 application/json으로 지정하고 인코딩 방식은 따로 지정하지 않는다
@WebServlet(name = "mvcMemberSaveServlet", urlPatterns = "/servlet-mvc/members/save")
public class MvcMemberSaveServlet extends HttpServlet {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 요청 메세지에서 데이터 조회
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);
// Model에 데이터를 보관
request.setAttribute("member", member);
// 렌더링
String viewPath = "/WEB-INF/views/save-result.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
서블릿은 호출되면 service()
메서드가 실행되기 때문에 하나의 서블릿은 하나의 url과 매핑되고, 하나의 기능만을 수행한다
위의 코드를 보면 /servlet-mvc/members/save
로 요청이 들어오면 위의 서블릿 객체가 실행된다
service()
에 요청 메세지에서 데이터를 조회, 모델에 데이터를 저장, view 의 이름으로 랜더링을 실행하는 모든 코드가 있다
이러한 기능들을 분리하기 위해 MVC 패턴, 코드의 중복 제거를 위해 Front Controller 패턴, 여러 Contoller 지원을 위해 어댑터 패턴을 사용한다
나중에 나오는 내용이지만 스프링에서는 DispatcherServlet 을 통해 모든 요청을 받아들여 특정 경로와 매핑된 메서드를 실행하게 된다
FrontController
매핑 정보를 보고 요청 url에 맞는 Controller 추출
Controller 호출 ( HttpServletRequest, HttpServletResponse 도 함께 전달 )
Controller
HttpServletRequest 에서 데이터를 추출해 객체 생성
생성한 객체를 Model 에 저장
HttpServletRequest의 임시저장소를 Model로 활용
JSP 의 물리 이름( 파일 경로 + 이름 ) 을 가지고 JSP로 forward(request, response)
참고 : FrontController
서블릿 하나로 클라이언트의 요청을 받는다
요청에 맞는 컨트롤러를 찾아서 호출한다
FrontController
매핑 정보를 보고 요청 url에 맞는 Controller 추출
Controller 호출 ( HttpServletRequest, HttpServletResponse 도 함께 전달 )
Controller
HttpServletRequest 에서 데이터를 추출해 객체 생성 ( member )
생성한 객체( member )를 Model 에 저장
HttpServletRequest의 임시저장소를 Model로 활용
JSP 의 물리 이름( 파일 경로 + 이름 ) 을 가지고 MyView 객체를 생성해서 반환
FrontController
MyView
FrontController
매핑 정보를 보고 요청 url에 맞는 Controller 추출
HttpServletRequest 에서 데이터를 추출해 paramMap 객체 생성
Controller 호출 ( paramMap을 전달 )
Controller
전달받은 paramMap 에 있는 정보를 꺼내 객체를 생성 ( member )
JSP 파일의 논리 이름( 이름만 )을 가지고 ModelView 객체를 생성
생성한 ModelView 객체의 model에 생성한 객체 ( member )를 넣는다
ModelView 객체 반환
FrontController
반환된 ModelView 객체의 논리 이름을 viewResolver()
를 통해 경로 + 이름( 물리 이름 )으로 변환하고 물리이름으로 MyView 객체생성
즉, viewResolver()
는 view 의 논리 이름을 받아 물리 이름으로 변환하고 물리 이름으로 MyView 객체를 반환한다
MyView 객체의 render() 호출 ( ModelView의 model, HttpServletRequest, HttpServletResponse 도 함께 전달 )
MyView
render() 메서드에서 model의 정보를 HttpServlerRequest의 임시저장소에 저장
forward(request, response) 수행
FrontController
매핑 정보를 보고 요청 url에 맞는 Controller 추출
HttpServletRequest 에서 데이터를 추출해 paramMap 객체 생성
Controller가 model 정보를 담을 수 있도록 Map 객체를 생성 ( model )
Controller 호출 ( paramMap과 model을 전달 )
Controller
전달받은 paramMap을 활용해 객체 생성 ( member )
전달받은 model을 담기위한 Map ( model )에 생성한 객체( member ) 추가
view의 논리 이름을 반환
FrontController
반환된 view의 논리 이름을 viewResolver()
를 통해 경로 + 이름( 물리 이름 )으로 변환
물리이름으로 MyView 객체생성
MyView 객체의 render() 호출 ( model, HttpServletRequest, HttpServletResponse 객체도 함께 전달 )
MyView
render() 메서드에서 model의 정보를 HttpServlerRequest의 임시저장소에 저장0
forward(request, response) 수행
여러 버전의 Controller 호출할 수 있도록 변경 ➜ 어댑터 패턴
핸들러 매핑 : url에 따른 여러 버전의 컨트롤러 정보가 있음
핸들러 어댑터 매핑 : 핸들러 어댑터 정보가 있음
FrontController
핸들러 매핑 정보를 보고 요청 url에 맞는 핸들러 추출 ( getHandler()
)
핸들러 어댑터 매핑 정보를 보고 주어진 핸들러를 처리할 수 있는지 확인 ( 핸들러 어댑터의 supprot()
활용 )
처리 가능한 핸들러 어댑터 반환 ( getHandlerAdapter()
)
handle()
로 반환된 핸들러 어댑터 호출 ( HttpServletRequest, HttpServletResponse, 추출한 핸들러도 함께 전달 )
핸들러 어댑터
전달받은 핸들러를 본인이 처리 가능한 핸들러로 캐스팅
HttpServletRequest 에서 데이터를 추출해 paramMap 객체 생성
핸들러 호출 ( paramMap을 함께 전달 )
핸들러
전달받은 paramMap을 활용해 객체 생성 ( member )
view의 논리 이름 or ModelView 객체를 생성 및 model 에 데이터를 넣고 반환
핸들러 어댑터
FrontController
반환된 ModelView 객체의 논리 이름을 viewResolver()를 통해 물리 이름으로 변환
물리 이름으로 MyView 객체생성
MyView 객체의 render() 호출 ( ModelView의 model, HttpServletRequest, HttpServletResponse 도 함께 전달 )
MyView
render() 메서드에서 model의 정보를 HttpServlerRequest의 임시저장소에 저장
forward(request, response) 수행
FrontController
핸들러 매핑을 보고 핸들러 추출
핸들러 어댑터 목록을 보고 핸들러 어댑터 찾기
핸들러 어댑터 호출
핸들러 어댑터 : 핸들러 호출
핸들러
로직 수행
view 논리 이름 or ModelView 반환
ModelView : view의 논리 이름과 model 를 담을 Map 객체를 가짐 ( model )
핸들러 어댑터 : 핸들러가 반환한 것을 핸들러 어댑터의 인터페이스의 handle() 메서드에 맞는 반환형을 맞춰서 반환
FrontController
viewResolver() 호출
MyView 객체 생성
MyView의 render() 호출
viewResolver()
view의 논리 이름 ➜ 물리 이름
물리 이름으로 MyView 객체를 생성해서 반환
MyView
view 의 물리 이름을 가짐
render() 메서드로 렌더링 수행
FrontController ➔ DispatcherServlet
handlerMappingMap ➔ HandlerMapping ( 인터페이스 )
MyHandlerAdapter ➔ HandlerAdapter ( 인터페이스 )
ModelView ➔ ModelAndView
viewResolver ( 메서드 ) ➔ viewResolver ( 인터페이스 )
MyView ( 클래스 ) ➔ View ( 인터페이스 )
핸들러 조회 : 요청 URL에 매핑된 핸들러( Controller )를 조회
핸들러 어댑터 조회 : 핸들러를 실행할 수 있는 핸들러 어댑터 조회
핸들러 어댑터 실행
핸들러 실행 : 핸들러 어댑터가 실제 핸들러를 실행
ModelAndView 반환 : 핸들러 어댑터는 핸들러가 반환하는 정보를 ModelAndView로 변환해서 반환
viewResolver 호출
뷰 렌더링 : view 를 통해 뷰를 렌더링 ( render()
)
핸들러가 호출되기 위해 HandlerMapping, HandlerAdapter가 필요
HandlerMapping( 핸들러 매핑 )
핸들러 매핑에서 해당 컨트롤러를 찾을 수 있어야한다
ex> 스프링 빈의 이름으로 핸들러를 찾을 수 있는 핸들러 매핑이 필요하다
HandlerAdapter( 핸들러 어댑터)
핸들러 매핑을 통해 찾은 핸들러를 실행할 수 있는 핸들러 어댑터가 필요
ex> Controller 인터페이스를 실행할 수 있는 핸들러 어댑터를 찾고 실행해야한다
0 순위 : RequestMappingHandlerMapping
@RequestMapping
을 사용하는 어노테이션 기반의 컨트롤러에서 사용
가장 먼저 실행되는 객체
1순위 : BeanNameUrlHandlerMapping
스프링 빈의 이름으로 핸들러를 찾는 객체
URL 이름과 동일한 스프링 빈을 찾는다
0 순위 : RequestMappingHandlerAdapter
@RequestMapping
을 사용하는 어노테이션 기반의 컨트롤러에서 사용1순위 : HttpRequestHandlerAdapter
HttpRequestHandler
를 처리하는 어댑터 ( 객체 )2순위 : SimpleControllerHandlerAdapter
public class SimpleControllerHandlerAdapter implements HandlerAdapter {
@Override
public boolean supports(Object handler) {
return (handler instanceof Controller);
}
@Override
@Nullable
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
return ((Controller) handler).handleRequest(request, response);
}
}
위에서 핸들러 매핑을 통해 찾은 핸들러를 실행할 수 있는 핸들러 어댑터가 필요하다고 했다
이를 판단하는 기준이 위의 supports()
메서드인데 instanceof
로 인해 Controller 인터페이스를 호출할 수 있는지 여부를 확인할 수 있다
dispatcherServlet 이 핸들러 어댑터의 handle() 메서드를 실행한다
DispatcherServlet은 뷰 리졸버 목록을 가지고 있고, 핸들러 어댑터를 통해 반환된 논리적인 뷰 이름을 가지고, viewResolver 목록을 순회하며 view를 생성을 시도
1순위 : BeanNameViewResolver
2순위 : InternalResourceViewResolver
JSP를 처리할 수 있는 뷰를 반환
JSP 처럼 forward()
를 호출해야 하는 경우에 사용
스프링이 viewResolver를 등록할 때 application.properties에 등록한 정보를 사용해서 등록한다
다른 템플릿 엔진의 경우, dependencies를 입력하면 스프링부트가 자동으로 viewResolver를 등록해준다
viewResolver가 상황에 맞게 위의 객체 중 하나를 선택하고 선택된 것이 View를 만들어서 반환한다
JSP의 경우, forward() 실행 후 렌더링이 진행되고 다른 템플릿 엔진의 경우 바로 렌더링이 진행된다
어노테이션에 URL 을 매핑할 수 있으며, method 를 지정할 수 있다
method 를 지정하지 않고 @GetMapping
나 @PostMapping
을 사용할 수 있다
method 속성으로 HTTP 메서드를 지정하지 않으면 HTTP 메서드와 무관하게 무조건 호출된다
URL 자체에 값이 들어가 있는 경우, 파라미터에서 @PathVariable
을 사용해 꺼낼 수 있음
@GetMapping("/mapping/{userId}")
이면 @PathVariable("userId") String userId
경로 변수의 이름 ( URL을 통해 들어오는 값의 이름 )과 메서드에서 사용되는 파라미터 이름이 같으면 생략할 수 있다
@PathVariable("userId") String userId
➜ @PathVariable String userId
쿼리 파라미터
message body 없이, URL의 쿼리 파라미터에 데이터를 포함해서 전달
쿼리 파라미터는 ?
로 시작하고 &
로 구분
HTML Form
HTTP Message Body에 데이터를 직접 담아서 요청
HttpServletRequest : getParameters()
, getParameter()
, getParameterValues()
@RequestParam("파라미터이름") 반환형 변수명
파라미터 이름과 변수명이 동일하다면 파라미터 이름을 생략 가능
@RequestParam 반환형 변수명
@ModelAttribute 클래스이름 객체이름
: 요청 파라미터로 받은 값을 이용해 객체를 만드는 경우에 사용
@RequestParam
으로 받아서 직접 객체를 생성할 수 있지만 @ModelAttribute
를 사용하면 이 과정을 스프링이 자동으로 처리
단> 객체 클래스에 @Data
어노테이션이 붙어 있어야 가능
HttpServletRequest : getInputStream()
+ StreamUtils.copyToString()
InputStream : StreamUtils.copyToString()
HttpEntity : getBody()
@RequestBody
HttpServletRequest : getInputStream()
+ StreamUtils.copyToString()
+ objectMapper.readValue()
HttpEntity : getBody()
@RequestBody
@RequestBody
로 데이터를 문자로 읽어온다 ➜ 객체로 변환
@RequestBody
에 객체를 지정하면 HTTP 메세지 컨버터가 message body의 내용을 객체로 변환시켜준다
@ModelAttribute
vs @RequestBody
@ModelAttribute
는 요청 파라미터 정보로 객체를 생성
@RequestBody
는 message body 의 정보로 객체를 생성
정적 리소스
view template
HttpServletResponse : response.getWriter().println("html태그")
ModelAndView 반환 : view 논리 이름으로 객체 생성 + addObject()
로 모델 정보 삽입
파라미터로 Model 사용, view의 논리 이름( String ) 반환
HTTP Message Body에 데이터를 직접 담아서 보내기
텍스트 형식 전달
HttpServletResponse : response.getWriter().println("text")
ResponseEntity<String>
@ResponseBody
: 응답 결과를 message body에 직접 담아서 전달
JSON 형식 전달
HttpServletResponse : objectMapper.writeValueAsString()
이용
ResponseEntity<클래스이름>
@ResponseBody
, @ResponseStatus
@RequestBody
, @ResponseBody
를 사용하는 경우에 viewResolver 대신 HttpMessageConverter
가 동작한다
기본 문자 처리 : StringHttpMessageConverter
기본 객체 처리 : MappingJackson2HttpMessageConverter
HttpMessageConverter 의 종류
0순위 : ByteArrayHttpMessageConverter
1순위 : StringHttpMessageConverter
2순위 : MappingJackson2HttpMessageConverter
아래 메서드를 통해 해당 메세지 컨버터를 사용 가능한지 확인할 수 있다
canRead()
, canWrite()
: 메시지 컨버터가 대상 클래스 타입과, 미디어타입을 지원하는지 체크@Controller
public class RequestBodyStringController {
@PostMapping("/request-body-string-v1")
public void requestBodyStringV1(HttpServletRequest request, HttpServletResponse response) throws IOException { ... }
@PostMapping("/request-body-string-v2")
public void requestBodyStringV2(InputStream inputStream, Writer responseWriter) throws IOException { ... }
@PostMapping("/request-body-string-v3")
public HttpEntity<String> requestBodyStringV3(HttpEntity<String> httpEntity) throws IOException { ... }
@ResponseBody
@PostMapping("/request-body-string-v4")
public String requestBodyStringV4(@RequestBody String messageBody) throws IOException { ... }
}
어노테이션 기반 컨트롤러에서 파라미터로 HttpServletRequest, Model, @RequestParam, @RequestBody, HttpEntity 등을 사용했는데 이것을 사용할 수 있으려면 누군가가 데이터를 해당 파라미터에 맞게 전달해주어야 한다
이런 것을 처리해주는 것이 바로 ArgumentResolver
이다
어노테이션 기반 컨트롤러를 처리하는 RequestMappingHandlerAdapter
는 바로 이 ArgumentResolver
를 호출해서 컨트롤러가 필요로 하는 다양한 파라미터의 값(객체)을 생성하고 모든 파라미터의 값이 준비되면 컨트롤러를 호출하면서 값을 넘겨준다
public interface HandlerMethodArgumentResolver {
boolean supportsParameter(MethodParameter parameter);
@Nullable
Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
}
ArgumentResolver는 핸들러가 필요로 하는 객체를 생성하는 역할을 수행
RequestMappingHandlerAdapter가 호출함으로써 실행되고, 생성한 객체를 RequestMappingHandlerAdapter에게 넘겨준다
supportsParameter()
: 핸들러( Controller )가 받아야 하는 파라미터 정보를 지원하는지 판단
지원하는 경우, resolveArgument()
를 통해 객체를 만들어서 반환한다
public interface HandlerMethodReturnValueHandler {
boolean supportsReturnType(MethodParameter returnType);
void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception;
}
ReturnValueHandler : 핸들러가 값을 반환할 때 응답( 반환 ) 값을 변환하고 처리해준다
핸들러에서 String으로 view 이름을 반환해도 동작하는 이유가 ReturnValueHandler 덕분이다
@RequestBody
와 @ResponseBody
를 컨트롤러에서 사용하는데 이들은 모두 HttpMessageConverter 를 사용한다
즉, @RequestBody
, @ResponseBody
, HttpEntity
를 사용하는 경우 ArgumentResolver와 ReturnValueHandler 가 메세지 컨버터를 사용한다
요청 시
@RequestBody
, HttpEntity
등을 처리하는 서로 다른 ArgumentResolver 가 있다 ( 여러 개 존재 )
ArgumentResolver 들이 HTTP 메시지 컨버터를 사용해서 필요한 객체를 생성한다
응답 시
@ResponseBody
, HttpEntity
등을 처리하는 서로 다른 ReturnValueHandler 가 있다 ( 여러 개 존재 )
ReturnValueHandler 들이 HTTP 메시지 컨버터를 호출해서 응답 결과를 만든다