@WebServlet(name = "helloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest request,
HttpServletResponse response){
//애플리케이션 로직
}
}
특징
사용
장점
주로 JSON 형태로 데이터 통신
UI 클라이언트 접점
서버 to 서버
@ServletComponentScan
을 지원 한다. -> @SpringBootApplication
과 같이 등록@WebServlet
서블릿 애노테이션protected void service(HttpServletRequest request, HttpServletResponse response)
메서드를 실행application.properties
에 다음 설정을 추가하면 서버가 받은 HTTP 요청 메시지를 출력한다.logging.level.org.apache.coyote.http11=debug
webapp
경로에 index.html
을 두면 http://localhost:8080 호출시 index.html
페이지가 열린다.HttpServletRequest
객체에 담아서 제공request.setAttribute(name, value)
request.getAttribute(name)
request.getSession(create: true)
//http://localhost:8080/request-header?username=hello
private void printStartLine(HttpServletRequest request) {
System.out.println("--- REQUEST-LINE - start ---");
System.out.println("request.getMethod() = " +
request.getMethod()); //GET
System.out.println("request.getProtocol() = " +
request.getProtocol()); //HTTP/1.1
System.out.println("request.getScheme() = " +
request.getScheme()); //http
// http://localhost:8080/request-header
System.out.println("request.getRequestURL() = " +
request.getRequestURL());
// /request-header
System.out.println("request.getRequestURI() = " +
request.getRequestURI());
// username=hello
System.out.println("request.getQueryString() = " +
request.getQueryString());
// false
System.out.println("request.isSecure() = " +
request.isSecure()); //https 사용 유무
System.out.println("--- REQUEST-LINE - end ---");
System.out.println();
}
//Header 모든 정보
private void printHeaders(HttpServletRequest request) {
System.out.println("--- Headers - start ---");
/*
Enumeration<String> headerNames = request.getHeaderNames();
while(headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
System.out.println(headerName + ": " + headerName);
}
*/
request.getHeaderNames().asIterator()
.forEachRemaining(headerName ->
System.out.println(headerName + ": "
+ request.getHeader(headerName)));
System.out.println("--- Headers - end ---");
System.out.println();
}
//Header 편리한 조회
private void printHeaderUtils(HttpServletRequest request) {
System.out.println("--- Header 편의 조회 start ---");
System.out.println("[Host 편의 조회]");
System.out.println("request.getServerName() = " +
request.getServerName()); //Host 헤더
System.out.println("request.getServerPort() = " +
request.getServerPort()); //Host 헤더
System.out.println();
System.out.println("[Accept-Language 편의 조회]");
request.getLocales().asIterator()
.forEachRemaining(locale -> System.out.println("locale = " +
locale));
System.out.println("request.getLocale() = " + request.getLocale());
System.out.println();
System.out.println("[cookie 편의 조회]");
if (request.getCookies() != null) {
for (Cookie cookie : request.getCookies()) {
System.out.println(cookie.getName() + ": " + cookie.getValue());
}
}
System.out.println();
System.out.println("[Content 편의 조회]");
System.out.println("request.getContentType() = " +
request.getContentType());
System.out.println("request.getContentLength() = " +
request.getContentLength());
System.out.println("request.getCharacterEncoding() = " +
request.getCharacterEncoding());
System.out.println("--- Header 편의 조회 end ---");
System.out.println();
}
private void printEtc(HttpServletRequest request) {
System.out.println("--- 기타 조회 start ---");
System.out.println("[Remote 정보]");
System.out.println("request.getRemoteHost() = " +
request.getRemoteHost()); //
System.out.println("request.getRemoteAddr() = " +
request.getRemoteAddr()); //
System.out.println("request.getRemotePort() = " +
request.getRemotePort()); //
System.out.println();
System.out.println("[Local 정보]");
System.out.println("request.getLocalName() = " +
request.getLocalName()); //
System.out.println("request.getLocalAddr() = " +
request.getLocalAddr()); //
System.out.println("request.getLocalPort() = " +
request.getLocalPort()); //
System.out.println("--- 기타 조회 end ---");
System.out.println();
}
-Djava.net.preferIPv4Stack=true
//단일 파라미터 조회
String username = request.getParameter("username");
//파라미터 이름들 모두 조회
Enumeration<String> parameterNames = request.getParameterNames();
//파라미터를 Map 으로 조회
Map<String, String[]> parameterMap = request.getParameterMap();
//복수 파라미터 조회
String[] usernames = request.getParameterValues("username");
/**
* 1. 파라미터 전송 가능
* http://localhost:8082/request-param?username=hello&age=20
*
* 2. 동일한 파라미터 전송 가능
* http://localhost:8080/request-param?username=hello&username=kim&age=20
*/
@WebServlet(name = "requestParamServlet", urlPatterns = "/request-param")
public class RequestParamServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
System.out.println("[전체 파라미터 조회] - start");
request.getParameterNames().asIterator()
.forEachRemaining(paramName ->
System.out.println(paramName + "="
+ request.getParameter(paramName)));
System.out.println("[전체 파라미터 조회] - end");
System.out.println();
System.out.println("[단일 파라미터 조회]");
String username = request.getParameter("username");
String age = request.getParameter("age");
System.out.println("username = " + username);
System.out.println("age = " + age);
System.out.println();
System.out.println("[이름이 같은 복수 파라미터 조회]");
String[] usernames = request.getParameterValues("username");
for (String name : usernames) {
System.out.println("username = " + name);
}
response.getWriter().write("ok");
}
}
application/x-www-form-urlencoded
username=hello&age=20
@WebServlet(name = "requestBodyStringServlet", urlPatterns = "/request-body-string")
public class RequestBodyStringServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
ServletInputStream inputStream = request.getInputStream();
String messageBody = StreamUtils.copyToString(inputStream,
StandardCharsets.UTF_8); //byte code -> String
System.out.println("messageBody = " + messageBody);
response.getWriter().write("ok");
}
}
@WebServlet(name = "requestBodyJsonServlet", urlPatterns = "/request-body-json")
public class RequestBodyJsonServlet extends HttpServlet {
private ObjectMapper objectMapper = new ObjectMapper();
@Override
protected void service(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
ServletInputStream inputStream = request.getInputStream();
String messageBody = StreamUtils.copyToString(inputStream,
StandardCharsets.UTF_8);
System.out.println("messageBody = " + messageBody);
HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
System.out.println("helloData.username = " + helloData.getUsername());
System.out.println("helloData.age = " + helloData.getAge());
response.getWriter().write("ok");
}
}
ObjectMapper
)를 함께 제공한다.@WebServlet(name = "responseHeaderServlet", urlPatterns = "/response-header")
public class ResponseHeaderServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
//[status-line]
response.setStatus(HttpServletResponse.SC_OK);
//[response-headers]
response.setHeader("Content-type", "text/plain;charset=utf-8");
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
response.setHeader("Pragma", "no-cache");
response.setHeader("my-header", "hello");
//[Header 편의 메서드]
// content(response);
// cookie(response);
// redirect(response);
//[message body]
PrintWriter writer = response.getWriter();
writer.println("ok");
}
private void content(HttpServletResponse response) {
//Content-Type: text/plain;charset=utf-8
//Content-Length: 2
//response.setHeader("Content-Type", "text/plain;charset=utf-8");
response.setContentType("text/plain");
response.setCharacterEncoding("utf-8");
//response.setContentLength(2); //(생략시 자동 생성)
}
private void cookie(HttpServletResponse response) {
//Set-Cookie: myCookie=good; Max-Age=600;
//response.setHeader("Set-Cookie", "myCookie=good; Max-Age=600");
Cookie cookie = new Cookie("myCookie", "good");
cookie.setMaxAge(600); //600초
response.addCookie(cookie);
}
private void redirect(HttpServletResponse response) throws IOException {
//Status Code 302
//Location: /basic/hello-form.html
//response.setStatus(HttpServletResponse.SC_FOUND); //302
//response.setHeader("Location", "/basic/hello-form.html");
response.sendRedirect("/basic/hello-form.html");
}
}
@WebServlet(name = "responseHtmlServlet", urlPatterns = "/response-html")
public class ResponseHtmlServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
//Content-Type: text/html;charset-utf-8
response.setContentType("text/html");
response.setCharacterEncoding("utf-8");
PrintWriter writer = response.getWriter();
writer.println("<html>");
writer.println("<body>");
writer.println(" <div>안녕?</div>");
writer.println("</body>");
writer.println("</html>");
}
}
@WebServlet(name = "responseJsonServlet", urlPatterns = "/response-json")
public class ResponseJsonServlet extends HttpServlet {
private ObjectMapper objectMapper = new ObjectMapper();
@Override
protected void service(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
//Content-Type: application/json
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
HelloData helloData = new HelloData();
helloData.setUsername("kim");
helloData.setAge(20);
//{"username":"kim", "age":20}
String result = objectMapper.writeValueAsString(helloData);
response.getWriter().write(result);
}
}
application/json
은 스펙상 utf-8 형식을 사용하도록 정의되어 있다. 그래서 스펙에서 charset=utf-8과 같은 추가 파라미터를 지원하지 않는다. 따라서 application/json
이라고만 사용해야지 application/json;charset-utf-8
이라고 전달하는 것은 의미 없는 파라미터를 추가한 것이 된다. response.getWriter()를 사용하면 추가 파라미터를 자동으로 추가해버린다. 이때는 response.getOutputStream()으로 출력하면 그런 문제가 없다.//JSP 추가 시작
implementation 'org.apache.tomcat.embed:tomcat-embed-jasper'
implementation 'javax.servlet:jstl'
//JSP 추가 끝
//JSP 추가 시작
implementation 'org.apache.tomcat.embed:tomcat-embed-jasper'
implementation 'jakarta.servlet:jakarta.servlet-api' //스프링부트 3.0 이상
implementation 'jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api' //스프링부트 3.0 이상
implementation 'org.glassfish.web:jakarta.servlet.jsp.jstl' //스프링부트 3.0 이상
//JSP 추가 끝
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="hello.servlet.domain.member.MemberRepository" %>
<% ~~ %>
자바 코드 입력<%= ~~ %>
자바 코드 출력request.setAttribute()
, request.getAttribute()
//회원 등록 폼 - 컨트롤러
@WebServlet(name = "mvcMemberFormServlet", urlPatterns = "/servlet-mvc/members/new-form")
public class MvcMemberFormServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
dispatcher.forward()
: 다른 서블릿이나 JSP로 이동할 수 있는 기능. 서버 내부에서 다시 호출이 발생한다./WEB-INF
: 이 경로안에 JSP가 있으면 외부에서 직접 JSP를 호출할 수 없다. 항상 컨트롤러를 통해서 JSP를 호출.//회원 등록 폼 - 뷰
// main/webapp/WEB-INF/views/new-form.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<!-- 상대경로 사용, [현재 URL이 속한 계층 경로 + /save] -->
<form action="save" method="post">
username: <input type="text" name="username" />
age: <input type="text" name="age" />
<button type="submit">전송</button>
</form>
</body>
</html>
/
로 시작)가 아니라 상대경로(/
로 시작X) -> 폼 전송시 현재 URL이 속한 계층 경로 +save가 호출/servlet-mvc/members/
/servlet-mvc/members/save
//회원 저장 - 컨트롤러
@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);
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);
}
}
//회원 저장 - 뷰
// main/webapp/WEB-INF/views/save-result.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
성공
<ul>
<li>id=${member.id}</li>
<li>username=${member.username}</li>
<li>age=${member.age}</li>
</ul>
<a href="/index.html">메인</a>
</body>
</html>
<%= request.getAttribute("member")%>
로 모델에 저장한 member 객체를 꺼낼 수 있지만, 너무 복잡해진다.${}
문법 제공, 이 문법을 사용하면 request의 attribute에 담긴 데이터를 편리하게 조회할 수 있다.//회원 목록 조회 - 컨트롤러
@WebServlet(name = "mvcMemberListServlet", urlPatterns = "/servlet-mvc/members")
public class MvcMemberListServlet extends HttpServlet {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
protected void service(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
List<Member> members = memberRepository.findAll();
request.setAttribute("members", members);
String viewPath = "/WEB-INF/views/members.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
//회원 목록 조회 - 뷰
// main/webapp/WEB-INF/views/members.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<html>
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<a href="/index.html">메인</a>
<table>
<thead>
<th>id</th>
<th>username</th>
<th>age</th>
</thead>
<tbody>
<c:forEach var="item" items="${members}">
<tr>
<td>${item.id}</td>
<td>${item.username}</td>
<td>${item.age}</td>
</tr>
</c:forEach>
</tbody>
</table>
</body>
</html>
<c:forEach>
기능 사용하려면 다음과 같이 선언<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
스프링 MVC의 프론트 컨트롤러가 바로 디스패처서블릿(DispatcherServlet)이다
DispatcherServlet
도 부모 클래스에서 HttpServlet
을 상속 받아서 사용하고, 서블릿으도 동작한다.DispatcherServlet
을 서블릿으로 자동으로 등록하면서 모든 경로(urlPatterns="/"
)에 대해서 매핑한다.HttpServlet
이 제공하는 service()
가 호출된다.DispatcherServlet
의 부모인 FrameworkServlet
에서 service()
를 오버라이드 해두었다.FrameworkServlet.service()
를 시작으로 여러 메서드가 호출되면서 DispatcherServlet.doDispatch()
가 호출된다.// DispatcherServlet.doDispatch() 코드
// 예외처리, 인터셉터 기능은 제외
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, 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);
}
InternalResourceViewResolver
가 자동 등록되고, 사용된다.InternalResourceView(JstlView)
를 반환하는데, 내부에 forword()
로직이 있다.
- 핸들러 매핑:
org.springframework.web.servlet.HandlerMapping
- 핸들러 어댑터:
org.springframework.web.servlet.HandlerAdapter
- 뷰 리졸버:
org.springframework.web.servlet.ViewResolver
- 뷰:
org.springframework.web.servlet.View
DispatcherServlet
에 등록하면 나만의 컨트롤러를 만들 수도 있다.0 = RequestMappingHandlerMapping : 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
1 = BeanNameUrlHandlerMapping : 스프링 빈의 이름으로 핸들러를 찾는다.
0 = RequestMappingHandlerAdapter : 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
1 = HttpRequestHandlerAdapter : HttpRequestHandler 처리
2 = SimpleControllerHandlerAdapter : Controller 인터페이스(애노테이션X, 과거에 사용) 처리
// org.springframework.web.servlet.mvc.Controller
// @Controller 애노테이션과는 전혀 다르다.
public interface Controller {
ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception;
}
// 간단하게 구현
@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;
}
}
@Component
: 이 컨트롤러는 /springmvc/old-controller
라는 이름의 스프링 빈으로 등록되었다.
- 핸들러 매핑으로 핸들러 조회
HandlerMapping
을 순서대로 실행해서, 핸들러를 찾는다.- 이 경우 빈 이름으로 핸들러를 찾아야 하기 때문에 이름 그대로 빈 이름으로 핸들러를 찾아주는
BeanNameUrlHandlerMapping
이 실행에 성공하고 핸들러인OldController
를 반환한다.
- 핸들러 어댑터 조회
HandlerAdapter
의supports()
를 순서대로 호출한다.SimpleControllerHandlerAdapter
가Controller
인터페이스를 지원하므로 대상이 된다.
- 핸들러 어댑터 실행
- 디스패처 서블릿이 조회한
SimpleControllerHandlerAdapter
를 실행하면서 핸들러 정보도 함께 넘겨준다.SimpleControllerAdapter
는 핸들러인OldController
를 내부에서 실행하고, 그 결과를 반환한다.
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");
}
}
MyHttpRequestHandler
를 실행하며 BeanNameUrlHandlerMapping
, HttpRequestHandlerAdapter
객체가 사용된다.// View를 사용할 수 있도록 return new ModelAndView("new-form"); 추가
@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 new ModelAndView("new-form");
}
}
# application.proterties에 추가
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp
InternalResourceViewResolver
라는 뷰 리졸버를 자동으로 등록하는데, 이때 application.properties
에 등록한 spring.mvc.view.prefix
, spring.mvc.view.suffix
설정 정보를 사용해서 등록한다.return new ModelAndView("/WEB-INF/views/new-form.jsp");
설정 없이 이렇게 해도 동작하기는 한다.// 실제로는 더 많다
1 = BeanNameViewResolver : 빈 이름으로 뷰를 찾아서 반환한다. (예: 엑셀 파일 생성
기능에 사용)
2 = InternalResourceViewResolver : JSP를 처리할 수 있는 뷰를 반환한다.
- 핸들러 어댑터 호출
- 핸들러 어댑터를 통해
new-form
이라는 논리 뷰 이름 획득
- ViewResolver 호출
new-form
이라는 뷰 이름으로 viewResolver를 순서대로 호출BeanNameViewResolver
는new-form
이라는 이름의 스프링 빈으로 등록된 뷰를 찾아야 하는데 없다.InternalResourceViewResolver
가 호출된다.
- InternalResourceViewResolver
- 이 뷰 리졸버는
InternalResourceView
를 반환
- 뷰 - InternalResourceView
InternalResourceView
는 JSP처럼forward()
를 호출해서 처리할 수 있는 경우에 사용한다.
- view.render()
view.render()
가 호출되고InternalResourceView
는forward()
를 사용해서 JSP를 실행
InternalResourceViewResolver
는 만약 JSTL 라이브러리가 있으면 InternalResourceView
를 상속받은 JstlView
를 반환한다. JstlView
는 JSTL 태그 사용시 약간의 부가 기능이 추가된다.forward()
를 통해서 해당 JSP로 이동(실행)해야 렌더링이 된다. JSP를 제외한 나머지 뷰 템플릿들은 forward()
과정 없이 바로 렌더링 된다.ThymeleafViewResolver
를 등록해야 한다. 최근에는 라이브러리만 추가하면 스프링 부트가 이런 작업도 모두 자동화해준다.@RequestMapping
애노테이션을 사용하는 컨트롤러이다.RequestMappingHandlerMapping
, RequestMappingHandlerAdapter
@controller
:@Component
)@RequestMapping
: 요청 정보를 매핑한다. 해당 URL이 호출되면 이 메서드가 호출된다. 애노테이션을 기반으로 동작하기 때문에, 메서드의 이름은 임의로 지으면 된다.ModelAndView
: 모델과 뷰 정보를 담아서 반환하면 된다.스프링 부트 3.0 이전
RequestMappingHandlerMapping
은 스프링 빈 중에서@RequestMapping
또는@Controller
가 클래스 레벨에 붙어 있는 경우에 매핑 정보로 인식- 따라서 클래스에
@Compenent
+@RequestMapping
또는 수동 빈 등록 후@RequestMapping
만 있어도 동작한다.
스프링 부트 3.0 이상
- 스프링 부트 3.0(스프링 프레임워크 6.0)부터는 클래스 레벨에
@RequestMapping
이 있어도 스프링 컨트롤러로 인식하지 않는다. 오직@Controller
가 있어야 스프링 컨트롤러로 인식한다. (RequestMappingHandlerMapping
에서@RequestMapping
은 이제 인식하지 않고,Controller
만 인식한다.)
@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 mv = new ModelAndView("save-result");
mv.addObject("member", member);
return mv;
}
@RequestMapping
public ModelAndView members() {
List<Member> members = memberRepository.findAll();
ModelAndView mv = new ModelAndView("members");
mv.addObject("members", members);
return mv;
}
}
//실용적인 방식
@Controller
@RequestMapping("/springmvc/v3/members")
public class SpringMemberControllerV3 {
private MemberRepository memberRepository = MemberRepository.getInstance();
// @RequestMapping(value = "/new-form", method = RequestMethod.GET)
@GetMapping("/new-form")
public String newForm() {
return "new-form";
}
// @RequestMapping(value = "/save", method = RequestMethod.POST)
@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";
}
// @RequestMapping(method = RequestMethod.GET)
@GetMapping
public String members(Model model) {
List<Member> members = memberRepository.findAll();
model.addAttribute("members", members);
return "members";
}
}
Jar
을 사용하면 /resources/static/
위치에 index.html
파일을 두면 Welcome 페이지로 처리해준다. (스프링 부트가 지원하는 정적 컨텐츠 위치에 /index.html
이 있으면 된다.)spring-boot-starter-logging
)가 함께 포함된다.private Logger log = LoggerFactory.getLogger(getClass());
private static final Logger log = LoggerFactory.getLogger(Xxx.class)
@Slf4j
: 롬복 사용 가능TRACE > DEBUG > INFO > WARN > ERROR
#전체 로그 레벨 설정(기본 info)
logging.level.root=info
#hello.springmvc 패키지와 그 하위 로그 레벨 설정
logging.level.hello.springmvc=debug
log.debug("data="+data)
log.debug("data={}", data)
@Controller
는 반환 값이 String
이면 뷰 이름으로 인식. 뷰를 찾고 뷰가 렌더링.@RestController
는 반환 값으로 뷰를 찾는 것이 아니라, HTTP 메시지 바디에 바로 입력.배열[]
로 제공하므로 다중 설정이 가능{"/hello-basic", "/hello-go"}
스프링 부트 3.0 이전
- URL 요청:
/hello-basic
,/hello-basic/
-> 매핑:/hello-basic
스프링 부트 3.0 이후
- 기존에는 마지막에 있는
/
(slash)를 제거했지만, 스프링 부트 3.0부터는 마지막의/
(slash)를 유지한다.- URL 요청:
/hello-basic
-> 매핑:/hello-basic
- URL 요청:
/hello-basic/
-> 매핑:/hello-basic/
@RequestMapping
에 method
속성으로 HTTP 메서드를 지정하지 않으면 HTTP 메서드와 무관하게 호출된다.//최근 HTTP API는 리소스 경로에 식별자를 넣는 스타일을 선호
@GetMapping("/mapping/{userId}")
public String mappingPath(@PathVariable("userId") String data) {
log.info("mappingPath userId={}", data);
return "ok";
}
@PathVariable
의 이름과 파라미터 이름이 같으면 생략할 수 있다.@GetMapping("/mapping/users/{userId}/orders/{orderId}")
public String mappingPath(@PathVariable String userId,
@PathVariable Long orderId) {
log.info("mappingPath userId={}, orderId={}", userId, orderId);
return "ok";
}
/**
* 파라미터로 추가 매핑
* params="mode",
* params="!mode"
* params="mode=debug"
* params="mode!=debug" (! = )
* params = {"mode=debug","data=good"}
*/
@GetMapping(value = "/mapping-param", params = "mode=debug")
public String mappingParam() {
log.info("mappingParam");
return "ok";
}
/**
* 특정 헤더로 추가 매핑
* headers="mode",
* headers="!mode"
* headers="mode=debug"
* headers="mode!=debug" (! = )
*/
@GetMapping(value = "/mapping-header", headers = "mode=debug")
public String mappingHeader() {
log.info("mappingHeader");
return "ok";
}
/**
* Content-Type 헤더 기반 추가 매핑 Media Type
* consumes="application/json"
* consumes="!application/json"
* consumes="application/*"
* consumes="*\/*"
* MediaType.APPLICATION_JSON_VALUE
*/
@PostMapping(value = "/mapping-consume", consumes = "application/json")
public String mappingConsumes() {
log.info("mappingConsumes");
return "ok";
}
/**
* Accept 헤더 기반 Media Type
* produces = "text/html"
* produces = "!text/html"
* produces = "text/*"
* produces = "*\/*"
*/
@PostMapping(value = "/mapping-produce", produces = "text/html")
public String mappingProduces() {
log.info("mappingProduces");
return "ok";
}
/users
/users
/users/{userId}
/users/{userId}
/users/{userId}
HttpServletRequest
HttpServletResponse
HttpMethod
: HTTP 메서드 조회. org.springframework.http.HttpMethod
Locale
: Locale 정보 조회@RequestHeader MultiValueMap<String, String> headerMap
@RequestHeader("host") String host
required
defaultValue
@CookieValue(value = "myCookie", required = false) String cookie
required
defaultValue
@RequestParam(name="xx")
생략 가능String
, int
, Integer
등의 단순 타입이면 @RequestParam
도 생략 가능@RequestParam
을 생략하면 스프링 MVC는 내부에서 required=false
를 적용 (기본값이 true
)required=true
일 때 요청 파라미터가 없으면 400 예외 발생/request-param?username=
)@RequestParam(required = false) int age
null
을 기본형(primitive)인 int
에 입력하는 것은 불가능(500 예외 발생)null
을 받을 수 있는 Integer
로 변경하거나, defaultValue
사용@RequestParam(required = true, defaultValue = "guest") String username
required
는 의미가 없다./request-param-default?username=
)@ResponseBody
@RequestMapping("/request-param-map")
public String requestParamMap(@RequestParam Map<String, Object> paramMap) {
log.info("username={}, age={}",
paramMap.get("username"), paramMap.get("age"));
return "ok";
}
@RequestParam Map
@RequestParam MultiValueMap
@ResponseBody
@RequestMapping("/model-attribute-v1")
public String modelAttributeV1(@ModelAttribute HelloData helloData) {
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
log.info("helloData={}", helloData); //toString
return "ok";
}
HelloData
객체를 생성HelloData
객체의 프로퍼티를 찾는다. 그리고 해당 프로퍼티의 setter를 호출해서 파라미터의 값을 입력(바인딩) 한다.username
이면 setUsername()
메서드를 찾아서 호출하면서 값을 입력한다.)@ModelAttribute
는 생략 가능생략시
String
,int
,Integer
같은 단순 타입 =@RequestParam
- 나머지 =
@ModelAttribute
(argument resolver 로 지정해둔 타입 외)
/**
* HttpEntity: HTTP header, body 정보를 편리하게 조회
* - 메시지 바디 정보를 직접 조회(@RequestParam X, @ModelAttribute X)
* - HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
*
* 응답에서도 HttpEntity 사용 가능
* - 헤더 정보 포함 가능
* - 메시지 바디 정보 직접 반환(view 조회X)
* - HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
*/
@PostMapping("/request-body-string-v3")
public HttpEntity<String> requestBodyStringV3(HttpEntity<String> httpEntity) {
String messageBody = httpEntity.getBody();
log.info("messageBody={}", messageBody);
return new HttpEntity<>("ok");
}
HttpEntity
를 상속받은 다음 객체들도 같은 기능 제공RequestEntity
: HttpMethod, url 정보가 추가, 요청 에서 사용ResponseEntity
: HTTP 상태 코드 설정 가능, 응답에서 사용/**
* @RequestBody
* - 메시지 바디 정보를 직접 조회(@RequestParam X, @ModelAttribute X)
* - HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
*
* @ResponseBody
* - 메시지 바디 정보 직접 반환(view 조회X)
* - HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
*/
@ResponseBody
@PostMapping("/request-body-string-v4")
public String requestBodyStringV4(@RequestBody String messageBody) {
log.info("messageBody={}", messageBody);
return "ok";
}
HttpEntity
를 사용하거나 @RequestHeader
를 사용HTTP 요청시에 content-type이 application/json인지 꼭 확인! 그래야 JSON을 처리할 수 있는 HTTP 메시지 컨버터가 실행
@RequestBody
를 사용해서 HTTP 메시지에서 데이터를 꺼내고 messageBody에 저장messageBody
를 objectMapper
를 통해서 자바 객체로 변환@RequestBody HelloData data
(@RequestBody
에 직접 만든 객체를 지정할 수 있다.) <- HTTP 메시지 컨버터@RequestBody
는 생략 불가능 (생략시 @ModelAttribute
)@ResponseBody
@PostMapping("/request-body-json-v4")
public String requestBodyJsonV4(HttpEntity<HelloData> httpEntity) {
HelloData data = httpEntity.getBody();
log.info("username={}, age={}", data.getUsername(), data.getAge());
return "ok";
}
/**
* @RequestBody 생략 불가능(@ModelAttribute 가 적용되어 버림)
* HttpMessageConverter 사용 -> MappingJackson2HttpMessageConverter (content-type: application/json)
*
* @ResponseBody 적용
* - 메시지 바디 정보 직접 반환(view 조회X)
* - HttpMessageConverter 사용 -> MappingJackson2HttpMessageConverter 적용 (Accept: application/json)
*/
@ResponseBody
@PostMapping("/request-body-json-v5")
public HelloData requestBodyJsonV5(@RequestBody HelloData data) {
log.info("username={}, age={}", data.getUsername(), data.getAge());
return data;
}
HttpEntity
를 사용해도 된다./static
, /public
, /resources
, /META-INF/resources
src/main/resources
는 리소스를 보관하는 곳이고, 또 클래스패스의 시작 경로src/main/resources/static/basic/hello-form.html
http://localhost:8080/basic/hello-form.html
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
src/main/resources/templates
<!-- src/main/resources/templates/response/hello.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<p th:text="${data}">empty</p>
</body>
</html>
//뷰 템플릿 렌더링
@Controller
public class ResponseViewController {
@RequestMapping("/response-view-v1")
public ModelAndView responseViewV1() {
ModelAndView mav = new ModelAndView("response/hello")
.addObject("data", "hello!");
return mav;
}
@RequestMapping("/response-view-v2")
public String responseViewV2(Model model) {
model.addAttribute("data", "hello!");
return "response/hello";
}
@RequestMapping("/response/hello")
public void responseViewV3(Model model) {
model.addAttribute("data", "hello!");
}
}
@Controller
를 사용하고, HttpServletResponse
, OutputStream(Writer)
같은 HTTP 메시지 바디를 처리하는 파라미터가 없으면 요청 URL을 참고해서 논리 뷰 이름으로 사용ResponseEntity
는 HTTP 응답 코드를 설정할 수 있는데, @ResponseBody
를 사용하면 이런 것을 설정하기 까다롭다. @ResponseStatus(HttpStatus.ok)
애노테이션을 사용하면 된다.ResponseEntity
를 사용해야 한다.스프링 MVC는 다음의 경우에 HTTP 메시지 컨버터를 적용한다.
- HTTP 요청:
@RequestBody
,HttpEntity(RequestEntity)
- HTTP 응답:
@ResponseBody
,HttpEntity(ResponseEntity)
package org.springframework.http.converter;
public interface HttpMessageConverter<T> {
boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);
boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);
List<MediaType> getSupportedMediaTypes();
T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException;
void write(T t, @Nullable MediaType contentType,
HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException;
}
canRead()
, canWrite()
: 메시지 컨버터가 해당 클래스, 미디어타입을 지원하는지 체크read()
, write()
: 메시지 컨버터를 통해서 메시지를 읽고 쓰는 기능//일부 생략
0 = ByteArrayHttpMessageConverter
1 = StringHttpMessageConverter
2 = MappingJackson2HttpMessageConverter
ByteArrayHttpMessageConverter
: byte[]
데이터를 처리byte[]
, 미디어타입: */*
@RequestBody byte[] data
@ResponseBody return byte[]
쓰기 미디어 타입 application/octet-stream
StringHttpMessageConverter
: String
문자로 데이터를 처리String
, 미디어타입: */*
@RequestBody String data
@ResponseBody return "ok"
쓰기 미디어 타입 text/plain
MappingJackson2HttpMessageConverter
: application/jsonHashMap
, 미디어타입 application/json
관련@RequestBody HelloData data
@ResponseBody return helloData
쓰기 미디어타입 application/json
관련RequestMappingHandlerAdapter
는 바로 이 ArgumentResolver
를 호출해서 컨트롤러(핸들러)가 필요로 하는 다양한 파라미터의 값(객체)을 생성한다.//정확히는 HandlerMethodArgumentResolver
public interface HandlerMethodArgumentResolver {
boolean supportsParameter(MethodParameter parameter);
@Nullable
Object resolveArgument(MethodParameter parameter,
@Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
@Nullable WebDataBinderFactory binderFactory) throws Exception;
}
ArgumentResolver
의 supportsParameter()
를 호출해서 해당 파라미터를 지원하는지 체크하고, 지원하면 resolveArgument()
를 호출해서 실제 객체를 생성한다. 이렇게 생성된 객체가 컨트롤러 호출시 넘어가는 것이다.ArgumentResolver
를 만들 수 있다.HandlerMethodReturnValueHandler
ArgumentResolver
와 비슷한데, 응답 값을 변환하고 처리@RequestBody
를 처리하는 ArgumentResolver
가 있고, HttpEntity
를 처리하는 ArgumentResolver
가 있다. 이 ArgumentResolver
들이 HTTP 메시지 컨버터를 사용해서 필요한 객체를 생성.@ResponseBody
와 HttpEntity
를 처리하는 ReturnValueHandler
가 있다. 그리고 여기에서 HTTP 메시지 컨버터를 호출해서 응답 결과를 만든다.@RequestBody
, @ResponseBody
가 있으면 RequestResponseBodyMethodProcessor
(ArgumentResolver)HttpEntity
가 있으면 HttpEntityMethodProcessor
(ArgumentResolver)HandlerMethodArgumentResolver
HandlerMethodReturnValueHandler
HttpMessageConverter
WebMvcConfigurer
을 상속 받아서 스프링 빈으로 등록하면 된다.<html xmlns:th="http://www.thymeleaf.org">
th:href="@{/css/bootstrap.min.css}"
href="value1"
을 th:href="value2"
의 값으로 변경action
에 값이 없으면 현재 URL에 데이터를 전송./basic/items/add
/basic/items/add
th:xxx
값으로 변경. 만약 값이 없다면 새로 생성.th:xxx
로 변경할 수 있다.th:onclick="'location.href=' + '\'' + @{/basic/items/add} + '\''"
th:onclick="|location.href='@{/basic/items/add}'|"
<span th:text="'Welcome to our application, ' + ${user.name} + '!'">
<span th:text="|Welcome to our application, ${user.name}!|">
<tr th:each="item : ${items}">
items
컬렉션 데이터가 item
변수에 하나씩 포함되고, 반복문 안에서 item
변수를 사용할 수 있다.<td th:text="${item.price}">10000</td>
th:value="${item.id}"
item.getPrice()
)th:href="@{/basic/items/{itemId}(itemId=${item.id})}"
th:onclick="|location.href='@{/basic/items/{itemId}/edit(itemId=${item.id})}'|"
{itemId}
) 뿐만 아니라 쿼리 파라미터도 생성한다.th:href="@{/basic/items/{itemId}(itemId=${item.id}, query='test')}"
http://localhost:8080/basic/items/1?query=test
th:href="@{|/basic/items/${item.id}|}"
(리터럴 대체 문법 활용)@ModelAttribute
는 Item
객체를 생성하고, 요청 파라미터 값을 프로퍼티 접근법(setXxx)으로 입력.@ModelAttribute
로 지정한 객체를 자동으로 넣어준다.@ModelAttribute
에 지정한 name(value)
속성을 사용@ModelAttribute
의 이름을 생략하면 모델에 저장될 때 클래스명 사용. 클래스의 첫글자만 소문자로 변경해서 등록.@ModelAttribute
자체도 생략 가능. 대상 객체는 모델에 자동 등록@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId, @ModelAttribute Item item) {
itemRepository.update(itemId, item);
return "redirect:/basic/items/{itemId}";
}
/items/{itemId}/edit
: 상품 수정 폼/items/{itemId}/edit
: 상품 수정 처리@PathVariable
의 값은 redirect
에도 사용 할 수 있다.Post /add
+ 상품 데이터를 서버로 전송한다.Post /add
+ 상품 데이터를 서버로 다시 전송하게 된다./**
* RedirectAttributes
*/
@PostMapping("/add")
public String addItemV6(Item item, RedirectAttributes redirectAttributes) {
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/basic/items/{itemId}";
}
"redirect:/basic/items/" + item.getId()
처럼 URL에 변수를 더해서 사용하는 것은 URL 인코딩이 안되기 때문에 위험하다. RedirectAttributes
를 사용하자.RedirectAttributes
를 사용하면 URL 인코딩도 해주고, PathVariable
, 쿼리 파라미터까지 처리해준다.{itemId}
?status=true
<h2 th:if="${param.status}" th:text="'저장 완료!'"></h2>
추가${param.status}
: 타임리프에서 쿼리 파라미터를 편리하게 조회하는 기능