스프링 부트 환경에서 서블릿을 등록하고 사용해보자.
참고로 스프링 부트에서 서블릿을 등록하는 이유는 스프링 부트는 톰켓 서버를 내장하고 있기 때문에, 별도의 톰켓 서버 설치 없이 편리하게 서블릿 코드를 실행하기 위함이다.
(톰켓 서버 관련 설정이 엄청 오래걸린다...)
스프링 부트에서 서블릿을 직접 등록해서 사용할 수 있게 해주는 어노테이션이다.
해당 어노테이션이 붙은 클래스의 패키지 포함 하위 패키지의 서블릿 컴포넌트(필터, 서블릿, 리스너)를 스캔해서 빈으로 등록해준다. (자기 자신도 포함)
필터: @WebFilter
서블릿: @WebServlet
리스너: @WebListener
사용 예시)
@ServletComponentScan //서블릿 자동 등록
@SpringBootApplication
public class ServletApplication {
public static void main(String[] args) {
SpringApplication.run(ServletApplication.class, args);
}
}
서블릿을 만들고 싶으면 다음 3가지를 하면 된다.
@WebServlet
붙이기HttpServlet
인터페이스 상속받기HttpServlet
의 추상 메서드 service
구현서블릿 어노테이션이다. 주요 속성은 name, urlPatterns이 있다.
GenericServlet을 상속받고 추가로 HTTP 프로토콜의 기능 (doGet, doPost, ...)을 제공하는 인터페이스이다.
HTTP 요청을 통해 매핑된 URL이 호출되면 서블릿 컨테이너는 service
메서드를 실행한다.
서블릿이 요청에 응답하도록 서블릿 컨테이너에서 호출되는 HttpServlet
인터페이스의 추상 메소드이다
형태)
protected void service(HttpServletRequest request,
HttpServletResponse response)
참고) HTTP 요청 메시지를 로그로 확인하기
application.properties 에서 다음 설정을 추가하자.
logging.level.org.apache.coyote.http11=debug
-> 그러나 이는 성능 저하가 발생할 수 있으므로 개발 단계에서만 적용하자.
스프링 부트가 내장 톰켓 서버를 생성해주고 톰켓 서버가 서블릿 컨테이너를 생성하고 자동으로 서블릿을 등록한다.
요청이 들어오면 HTTP 요청 메시지를 기반으로 request 객체를 생성한다.
그 후, 서블릿 컨테이너의 서블릿의 service
메소드를 통해 response에 응답 정보를 담고 이 객체를 통해 HTTP 응답 메시지를 생성한다.
참고)
HTTP 응답에서 Content-Length는 웹 애플리케이션 서버가 자동으로 생성해준다.
HTTP 요청 메시지를 개발자가 직접 파싱해서 사용해도 되지만, 이는 매우 불편하다.
서블릿은 개발자가 HTTP 요청 메시지를 편리하게 사용할 수 있도록 개발자 대신에 HTTP 요청 메시지를 파싱한다. 그리고 그 결과를 HttpServletRequest
객체에 담아서 제공한다.
참고로 HttpServletRequset
는 추가로 여러가지 부가 기능도 함께 제공한다.
임시 저장소 기능
request.setAttribute(name, value)
request.getAttribute(name)
세션 관리 기능
request.getSession(create : true)
중요)
HttpServletRequest, HttpServletResponse를 사용할 때 가장 중요한 점은 이 객체들이 HTTP 요청 메시지, HTTP 응답 메시지를 편리하게 사용하도록 도와주는 객체라는 점이다. 따라서 이 기능에 대해서 깊이있는 이해를 하려면 HTTP 스펙이 제공하는 요청, 응답 메시지 자체를 이해해야 한다.
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 = hi
System.out.println("request.getQueryString() = " + request.getQueryString());
System.out.println("request.isSecure() = " + request.isSecure()); //https 사용 유무
System.out.println("--- REQUEST-LINE - end ---");
System.out.println();
}
--- REQUEST-LINE - start ---
request.getMethod() = GET
request.getProtocol() = HTTP/1.1
request.getScheme() = http
request.getRequestURL() = http://localhost:8080/request-header
request.getRequestURI() = /request-header
request.getQueryString() = username=hello
request.isSecure() = false
--- REQUEST-LINE - end ---
private void printHeaders(HttpServletRequest request) {
System.out.println("--- Headers - start ---");
/*
Enumeration<String> headerNames = request.getHeaderNames(); // 예전 방식, iterator 사용
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
System.out.println(headerName + " : " + headerName);
}
*/
request.getHeaderNames().asIterator()
.forEachRemaining(headerName -> System.out.println(headerName + " : " + headerName));
System.out.println("--- Headers - end ---");
System.out.println();
}
--- Headers - start ---
host: localhost:8080
connection: keep-alive
cache-control: max-age=0
sec-ch-ua: "Chromium";v="88", "Google Chrome";v="88", ";Not A Brand";v="99"
sec-ch-ua-mobile: ?0
upgrade-insecure-requests: 1
user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_0) AppleWebKit/537.36
(KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36
accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/
webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
sec-fetch-site: none
sec-fetch-mode: navigate
sec-fetch-user: ?1
sec-fetch-dest: document
accept-encoding: gzip, deflate, br
accept-language: ko,en-US;q=0.9,en;q=0.8,ko-KR;q=0.7
--- Headers - end ---
지금까지 HttpServletRequest를 통해서 HTTP 메시지의 start-line, header 정보 조회 방법을 이해했다. 이제 본격적으로 HTTP 요청 데이터를 어떻게 조회하는지 알아보자.
다음 3가지 방법을 사용한다.
서버로 데이터 전달하는 방법은 나와있는 3가지에서 벗어나지 않는다.
다음 데이터를 클라이언트에서 서버로 전송해보자. ( key = value)
메시지 바디 없이 URL의 쿼리 파라미터를 사용해서 데이터를 전달하자.
이는 검색, 필터, 페이징등에서 많이 사용하는 방식이다.
특징
쿼리 파라미터는 URL에 다음과 같이
?
을 시작으로 보내고 추가 파라미터는&
으로 구분한다.
http://localhost:8080/request-param?username=hello&age=20
서버에는 HttpServletRequest
가 제공하는 다음 메서드를 통해 쿼리 파라미터를 편리하게 조회할 수 있다.
//단일 파라미터 조회
String username = request.getParameter("username");
//파라미터 이름들 모두 조회
Enumeration<String> parameterNames = request.getParameterNames();
//파라미터를 Map 으로 조회
Map<String, String[]> parameterMap = request.getParameterMap();
//복수 파라미터 조회
String[] usernames = request.getParameterValues("username");
참고) 복수 파라미터에서 단일 파라미터 조회
username=hello&username=kim
과 같이 파라미터 이름은 하나인데, 값이 중복인 상황이면 어떻게 될까?
일단, request.getParameter() 는 하나의 파라미터 이름에 대해서 단 하나의 값만 있을 때 사용해야 한다.
지금처럼 중복일 때는 request.getParameterValues() 를 사용해야 한다. 참고로 이렇게 중복일 때 request.getParameter() 를 사용하면 request.getParameterValues() 의 첫 번째 값을 반환한다.
HTML의 Form을 사용해서 클라이언트에서 서버로 데이터를 전송해보자.
이는 주로 회원 가입, 상품 주문 등에서 사용된다.
특징
application/x-www-form-urlencoded
username=hello&age=20
참고)
GET 메서드애서 사용하는 쿼리 파라미터와 POST 메서드에서 HTML Form을 사용할 때 메시지 바디에 입력하는 쿼리 파라미터는 둘다 같은 형태이다. 그래서 이 둘을 요청 파라미터라 부른다.
HTTP Form을 이용하여 요청할 때 사용할 수 있는 메서드는 GET과 POST이지만 메시지 바디를 포함해야 하므로 사실상 POST만 사용한다고 생각하자.
(PUT, PATCH, DELETE는 사용 불가능하다.)
application/x-www-form-urlencoded
형식은 앞서 GET에서 살펴본 쿼리 파라미터 형식과 같다. 따라서 쿼리 파라미터 조회 메서드를 그대로 사용하면 된다.
클라이언트(웹 브라우저) 입장에서는 두 방식에 차이가 있지만, 서버 입장에서는 둘의 형식이 동일하므로, request.getParameter()
로 편리하게 구분없이 조회할 수 있다.
정리하면 request.getParameter()
는 GET URL 쿼리 파라미터 형식도 지원하고, POST HTML Form 형식도 둘 다 지원한다.
참고)
content-type은 HTTP 메시지 바디에 데이터를 포함하는 경우에 작성을 해줘야한다. GET 메서드는 메시지 바디가 없으므로 null이지만 POST는 메시지 바디가 있으므로 작성을 해야한다.
말 그대로 HTTP 메시지 바디에 데이터를 직접 담아서 요청한다. 주로 HTTP API에서 사용하며 데이터 형식은 JSON을 주로 사용하지만 모든 데이터 형식이 올 수 있다.
(XML, TEXT, 이미지 등...)
메서드는 POST, PUT, PATCH가 사용된다.
messageBody = hello
단순한 텍스트 메시지를 HTTP 메시지에 담아서 전송하고 서버에서 읽어보자.
HTTP 메시지 바디의 데이터는 다음과 같이 InputStream을 사용하면 직접 읽을 수 있다.
ServletInputStream inputStream = request.getInputStream();
String messageBody = StreamUtils.copyToString(inputStream,
StandardCharsets.UTF_8);
참고)
InputStream은 byte 코드를 리턴한다. 이를 우리가 읽을 수 있는 문자(String)으로 보려면 문자표를 지정해 주어야 한다.
.
.
.
.
.
다음으로 HTTP API에서 주로 사용하는 JSON 형식으로 데이터를 전달해보자.
messageBody = {"username": "hello", "age": 20}
JSON 형식으로 파싱하기 위해 객체를 하나 생성하자.
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class HelloData {
private String username;
private int age;
}
JSON 결과를 파싱해서 사용할 수 있는 자바 객체로 변환해주는 JSON 변환 라이브러리인 ObjectMapper
객체를 생성하자.
private ObjectMapper objectMapper = new ObjectMapper();
그 후, 다음과 같이 InputStream을 사용하여 데이터를 읽자.
ServletInputStream inputStream = request.getInputStream();
String messageBody = StreamUtils.copyToString(inputStream,
StandardCharsets.UTF_8);
...
HelloData helloData = objectMapper.readValue(messageBody,
HelloData.class);
그 후, HelloData의 Getter와 Setter을 이용해 데이터를 읽을 수 있다.
System.out.println("helloData.username = " + helloData.getUsername());
System.out.println("helloData.age = " + helloData.getAge());
출력 결과)
data.username=hello
data.age=20
참고)
JSON 결과를 파싱해서 사용할 수 있는 자바 객체로 변환하려면 Jackson, Gson 같은 JSON 변환 라이브러리를 추가해서 사용해야 한다. 스프링 부트로 Spring MVC를 선택하면 기본으로 Jackson 라이브러리( ObjectMapper )를 함께 제공한다.
참고)
HTML form 데이터도 메시지 바디를 통해 전송되므로 직접 읽을 수 있다. 하지만 편리한 파리미터 조회 기능( request.getParameter(...) )을 이미 제공하기 때문에 파라미터 조회 기능을 사용하면 된다.
HttpServletResponse의 역할은 크게 2가지 이다.
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");
}
sendRedirect
: 상태코드를 302로 바꾸고 RedirectHTTP 응답 메시지는 크게 다음 3가지 형태로 내용을 담아서 전달한다.
writer.println("ok")
하나씩 알아보자.
@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>");
}
}
text/html
로 지정해야 한다.@WebServlet(name = "responseJsonServlet", urlPatterns = "/response-json")
public class ResponseJsonServlet extends HttpServlet {
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
으로 지정해야 한다.objectMapper.writeValueAsString()
를 사용하면 객체를 JSON 문자로 변경할 수 있다.참고)
application/json
은 스펙상 utf-8 형식을 사용하도록 정의되어 있다. 그래서 스펙에서 charset=utf-8 과 같은 추가 파라미터를 지원하지 않는다. 따라서 application/json 이라고만 사용해야지application/json;charset=utf-8
이라고 전달하는 것은 의미 없는 파라미터를 추가한 것이 된다. response.getWriter()를 사용하면 추가 파라미터를 자동으로 추가해버린다. 이때는 response.getOutputStream()으로 출력하면 그런 문제가 없다.