스프링 부트 환경에서 서블릿을 등록하고 사용해보자.
📌 참고!
서블릿은 톰캣 같은 웹 애플리케이션 서버를 직접 설치하고, 그 위에 서블릿 코드를 클래스 파일로 빌드해서 올린 다음, 톰캣 서버를 실행하면 된다. 하지만, 이 과정은 매우 번거롭다. 스프링 부트는 톰캣 서버를 내장하고 있으므로, 톰캣 서버 설치 없이 편리하게 서블릿 코드를 실행할 수 있다.
스프링 부트는 서블릿을 직접 등록해서 사용할 수 있도록 @ServletComponentScan
을 지원한다.
package hello.servlet;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.servlet.ServletComponentScan;
@ServletComponentScan
@SpringBootApplication
public class ServletApplication {
public static void main(String[] args) {
SpringApplication.run(ServletApplication.class, args);
}
}
package hello.servlet.basic;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@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");
System.out.println("request = " + request);
System.out.println("response = " + response);
String username = request.getParameter("username");
System.out.println("username = " + username);
response.setContentType("text/plain");
response.setCharacterEncoding("utf-8");
response.getWriter().write("hello " + username);
}
}
Servlet을 사용하기 위해서는 HttpServlet
을 상속받고 service()
를 오버라이딩해서 사용해야 한다. (접근제어자가 protected)
@WebServlet
: 서블릿 애노테이션HTTP 요청을 통해 매핑된 URL이 호출되면 서블릿 컨테이너는 service()
메서드를 실행한다.
결과는 다음과 같다.
HelloServlet.service
request = org.apache.catalina.connector.RequestFacade@5e4e72
response = org.apache.catalina.connector.ResponseFacade@37d112b6
username = ramos
스프링 부트 내부에 포함된 내장 톰캣이 띄워지면서 톰캣이 가지고 있는 서블릿 컨테이너를 통해 서블릿을 생성하고 등록해준다.
// HTTP 응답에서 Content-Length는 웹 애플리케이션 서버(WAS)가 자동으로 생성해준다.
HTTP 요청 메시지를 개발자가 직접 파싱해서 사용해도 되지만, 매우 불편하고 비효율적이다.
서블릿은 개발자가 HTTP 요청 메시지를 편리하게 사용할 수 있도록 HTTP 요청 메시지를 개발자 대신 파싱한다. 그리고 그 결과를 HttpServletRequest
객체에 담아 제공한다.
POST /save HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded
username=kim&age=20
HttpServletRequest
를 사용하면 다음과 같은 HTTP 요청 메시지를 편리하게 조회할 수 있다.
HttpServletRequest
는 추가로 여러가지 부가기능도 함께 제공한다.
임시 저장소 기능: 해당 HTTP 요청이 시작부터 끝날 때까지 유지되는 임시 저장소 기능
request.setAttribute(name, value)
request.getAttribute(name)
세션 관리 기능
request.getSession(creaet: true)
// HttpServletRequest, HttpServletResponse를 사용할 때 가장 중요한 점은 이 객체들이 HTTP 요청 메시지, HTTP 응답 메시지를 편리하게 사용하도록 도와주는 객체라는 점이다. 따라서 이 기능에 대해 깊이있는 이해를 하려면 HTTP 스펙이 제공하는 요청, 응답 메시지 자체를 이해해야 한다.
package hello.servlet.basic.request;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;
import java.io.IOException;
@WebServlet(name = "requestHeaderServlet", urlPatterns = "/request-header")
public class RequestHeaderServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
printStartLine(request);
printHeader(request);
printHeaderUtils(request);
printEtc(request);
response.getWriter().write("ok");
}
// start line 정보
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-test
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();
}
// Header 모든 정보
private void printHeaders(HttpServletRequest request) {
System.out.println("--- Headers - start ---");
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();
}
}
HTTP 요청 메시지를 통해 클라이언트에서 서버로 데이터를 전달하는 방법은 다음 3가지가 있다.
/url?username=hello&age=20
다음 전달 데이터를 클라이언트에서 서버로 전송해보자.
메시지 바디 없이, URL의 쿼리 파라미터를 사용해서 데이터를 전달한다.
쿼리 파라미터는 URL에 다음과 같이 '?'를 시작으로 보낼 수 있다. 추가 파라미터는 '&'로 구분하면 된다.
http://localhost:8080/request-param?username=ramos&age=20
package hello.servlet.basic.request;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Enumeration;
/**
* 1. 파라미터 전송 기능
* http://localhost:8080/request-param?username=ramos&age=20
*
* 2. 동일한 파라미터 전송 가능
* http://localhost:8080/request-param?username=ramos&username=sergio&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");
System.out.println("request.getParameter(username) = " + username);
String age = request.getParameter("age");
System.out.println("request.getParameter(age) = " + age);
System.out.println();
System.out.println("[이름이 같은 복수 파라미터 조회]");
System.out.println("request.getParameterValues(username)");
String[] usernames = request.getParameterValues(username);
for (String name : usernames) {
System.out.println("username=" + name);
}
response.getWriter().write("ok");
}
}
결과는 다음과 같다.
[전체 파라미터 조회] - start
username=ramos
age=20
[전체 파라미터 조회] - end
[단일 파라미터 조회]
request.getParameter(username) = ramos
request.getParameter(age) = 20
[이름이 같은 복수 파라미터 조회]
request.getParameterValues(username)
username=ramos
username=sergio
username=ramos&username=sergio
와 같이 파라미터 이름은 하나인데, 값이 중복인 경우엔 request.getParameterValues()
를 사용해야 한다. 이 경우, 첫 번째 값을 반환한다.
request.getParameter()
는 하나의 파라미터 이름에 대해서 단 하나의 값만 있을 때 사용해야 한다.
HTML의 Form을 사용해서 클라이언트에서 서버로 데이터를 전송하는 방법이다.
application/x-www-form-urlencoded
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/request-param" method="post">
username: <input type="text" name="username" />
age: <input type="text" name="age" />
<button type="submit">전송</button>
</form>
</body>
</html>
POST의 HTML Form을 전송하면 웹 브라우저는 다음 형식으로 HTTP 메시지를 만든다.
http://localhost:8080/request-param
application/x-www-form-urlencoded
username=ramos&age=20
application/x-www-form-urlencoded
은 GET 쿼리 파라미터 형식과 같다. 따라서 쿼리 파라미터 조회 메서드를 그대로 사용하면 된다. 클라이언트(웹 브라우저) 입장에선 두 방식에 차이가 있지만, 서버 입장에서는 둘의 형식이 동일하므로, request.getParameter()
로 편리하게 구분없이 조회할 수 있다.
request.getParameter()
는 GET URL 쿼리 파라미터 형식, POST HTML Form 형식 둘 다 지원한다.
application/x-www-form-urlencoded
라 한다.package hello.servlet.basic.request;
import org.springframework.util.StreamUtils;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@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);
System.out.println("messageBody = " + messageBody);
response.getWriter().write("ok");
}
}
Postman으로 테스트해보면 결과는 다음과 같다.
http://localhost:8080/request-body-string
messageBody = hello
JSON 형식을 사용할 때는 해당 데이터를 파싱해서 객체화 해줄 객체를 만들어줄 필요가 있다.
// HelloData.java
package hello.servlet.basic;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
public class HelloData {
private String username;
private int age;
}
JSON 방식의 API와 매핑해줄 서블릿을 생성해준다.
package hello.servlet.basic.request;
import com.fasterxml.jackson.databind.ObjectMapper;
import hello.servlet.basic.HelloData;
import org.springframework.util.StreamUtils;
import javax.servlet.ServletException;
import javax.servlet.ServletInpuStream;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@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 {
ServletInputSteram inputStream = request.getInputStream();
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
System.out.println("messageBody = " + messageBody);
HelloData helloData = objectMapper.readValue(messaageBody, HelloData.class);
System.out.println("helloData = " + helloData);
response.getWriter().write("ok");
}
}
{"username": "hello", "age": 20}
출력 결과는 다음과 같다.
messageBody={"username": "hello", "age": 20}
data.username=hello
data.age=20
📌 참고
JSON 결과를 파싱해서 사용할 수 있는 자바 객체로 변환하려면, Jackson, Gson 같은 JSON 변환 라이브러리를 추가해서 사용해야 한다. 스프링 부트로 Spring MVC를 선택하면 기본으로 Jackson 라이브러리(ObjectMapper
)를 함께 제공한다.
HttpServletResponse 객체 역시 HttpServletRequest와 같은 목적으로 HTTP 응답 메시지를 자동으로 작성해주기 위한 편의 객체이다.
Response 객체를 통해 응답 메시지를 생성할 때는 다음 3가지를 해주면 된다.
또한 다음과 같은 편의 기능도 제공해준다.
package hello.servlet.basic.response;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* http://localhost:8080/response-header
*
*/
@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); //200
//[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.serMaxAge(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");
}
}
content(response)
: setHeader를 통해 name/value로 값을 넣어도 되지만, 좀 더 편하게 작성할 수 있도록 편의 메서드를 제공한다.cookie(response)
: 쿠키 편의 메소드로 편의 메소드를 사용하지 않으면 setHeader 메소드에 Set-Cookie라는 name으로 value를 직접 입력해줘야 하지만, 편의 메서드를 통해 쿠키와 쿠키의 유효시간을 모두 손쉽게 설정할 수 있다.redirect(response)
: 편의메서드인 sendRedirect를 사용하여 리다이렉션할 경로만 파라미터로 넣어주면 자동으로 302 상태코드와 리다이렉션할 경로를 응답 메시지에 설정해준다.응답 메시지는 주로 다음 내용을 담아서 전달한다.
writer.println("ok");
)이 응답 데이터를 이용해 사용자에게 HTML 페이지를 보여주기도하고, 데이터를 가공해서 DOM 조작을 통해 페이지의 내용을 변경시킬수도 있다.
package hello.servlet.basic.response;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
@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>hello?</div>");
writer.println("</body>");
writer.println("</html>");
}
}
HTTP 응답으로 HTML을 반환할 때는 content-type을 text/html
로 지정해야 한다.
package hello.servlet.basic.response;
import com.fasterxml.jackson.databind.ObjectMapper;
import hello.servlet.basic.HelloData;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* http://localhost:8080/response-json
*
*/
@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.setHeader("content-type", "application/json");
response.setCharacterEncoding("utf-8");
HelloData data = new HelloData();
data.setUsername("ramos");
data.setAge(20);
//{"username":"ramos", "age":20}
String result = objectMapper.writeValueAsString(data);
response.getWriter().write(result);
}
}
HTTP 응답으로 JSON을 반환할 때는 content-type을 application/json
로 지정해야 한다.
Jackson 라이브러리가 제공하는 objectMapper.writeValueAsString()
를 사용하면 객체를 JSON 문자로 변경할 수 있다.