
스프링 부트에서 서블릿을 등록하고 사용해보자. 스프링 부트는 톰캣 서버를 내장하고 있기 때문에 편리하게 서블릿 코드를 실행할 수 있다.
스프링 부트는 서블릿을 직접 등록해서 사용할 수 있게 @ServletComponentScan을 지원한다.
package hello.servlet;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
@ServletComponentScan // 서블릿 자동 등록
@SpringBootApplication
public class ServletApplication {
public static void main(String[] args) {
SpringApplication.run(ServletApplication.class, args);
}
}
이제 서블릿 코드를 작성해보자.
package hello.servlet.basic;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.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);
}
}
HTTP 요청을 통해 매핑된 “/hello” 가 호출되면, 서블릿 컨테이너는 service() 메서드를 실행하는 것이다. 스프링 부트를 실행하고, http://localhost:8080/hello?username=world로 접속하고, 콘솔을 확인해보면…

스프링 부트를 실행하면, 스프링 부트가 내장 톰캣 서버(WAS)를 띄워준다. 보다시피 톰캣은 서블릿 컨테이너를 통해서 서블릿을 생성해준다.

그리고 나서 username에 “world” 라고 해줬는데, 그럼 웹 브라우저가 HTTP 요청 메시지를 만들어준다.

요청을 받은 웹 애플리케이션 서버가 request와 response 객체를 만들어서 싱글톤으로 떠 있는 helloServlet을 호출한다. 그리고 필요한 작업이 끝나면 WAS 서버가 response 정보를 가지고 HTTP 응답 메시지를 만들어서 반환해주는 것이다.

그러면 웹 브라우저에서 “hello world” 라고 볼 수 있는 것이다.

HTTP 요청 메시지를 직접 파싱할 수도 있지만, 그럼 너무 번거롭다. 서블릿은 HTTP 요청 메시지를 편리하게 사용할 수 있게 대신 메시지를 파싱해준다. 그 파싱된 결과를 HttpServletRequest 객체에 담아서 주는거다.
HttpServletRequest를 사용하면 아래와 같은 HTTP 요청 메시지에서 정보를 입맛에 맞게 뽑아낼 수 있다.

START LINE(HTTP 메서드, URL, 쿼리 스트링, 스키마 및 프로토콜)과 헤더, 바디(폼 파라미터 형식 조회, 메시지 바디 직접 조회)와 같은 식으로 읽을 수 있도록 지원한다. 추가로, 여러가지 부가기능도 제공하는데, 대표적으로 임시 저장소 기능과 세션 관리 기능이 있다.
임시 저장소 기능은 HTTP 메시지가 살아있는 동안 request.setAttribute(name, value)로 값을 넣고, request.getAttribute(name)으로 값을 꺼낼 수 있도록 한다. 세션 관리 기능은 request.getSession(create: true)로 로그인 유지 같은 기능에 사용할 수 있다.
하지만, 여기서 중요한 점은 HttpServletRequest, HttpServletResponse를 사용할 때 이 객체들이 HTTP 요청 메시지, HTTP 응답 메시지를 편리하게 사용하도록 도와주는 객체라는 것이다. 이 기능에 대해 깊이 있게 이해하기 위해서는 HTTP 스펙이 제공하는 요청과 응답 메시지 자체를 이해해야 한다.
이제 HttpServletRequest가 제공하는 기본 기능들에 대해 코드로 작성해보자.
package hello.servlet.basic.request;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Enumeration;
@WebServlet(name = "requestHeaderServlet", urlPatterns = "/request-header")
public class RequestHeaderServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// Start Line 정보 불러오기
printStartLine(request);
printHeaders(request);
printHeaderUtils(request);
printEtc(request);
}
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() = null
request.isSecure() = false
--- REQUEST-LINE - end ---
*/
}
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 + " = " + headerName));
System.out.println("--- Headers - end ---");
System.out.println();
/*
--- Headers - start ---
host = host
connection = connection
cache-control = cache-control
sec-ch-ua = sec-ch-ua
sec-ch-ua-mobile = sec-ch-ua-mobile
sec-ch-ua-platform = sec-ch-ua-platform
upgrade-insecure-requests = upgrade-insecure-requests
user-agent = user-agent
accept = accept
sec-fetch-site = sec-fetch-site
sec-fetch-mode = sec-fetch-mode
sec-fetch-user = sec-fetch-user
sec-fetch-dest = sec-fetch-dest
accept-encoding = accept-encoding
accept-language = accept-language
cookie = cookie
--- Headers - end ---
*/
}
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();
/*
--- Header 편의 조회 start ---
[Host 편의 조회]
request.getServerName() = localhost
request.getServerPort() = 8080
[Accept-Language 편의 조회]
locale = ko_KR
locale = ko
locale = en_US
locale = en
request.getLocale() = ko_KR
[cookie 편의 조회]
JSESSIONID: 3C011FA5B1E74348DA237197DA296731
[Content 편의 조회]
request.getContentType() = null
request.getContentLength() = -1
request.getCharacterEncoding() = UTF-8
--- Header 편의 조회 end ---
*/
}
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();
/*
--- 기타 조회 start ---
[Remote 정보]
request.getRemoteHost() = 0:0:0:0:0:0:0:1
request.getRemoteAddr() = 0:0:0:0:0:0:0:1
request.getRemotePort() = 50899
[Local 정보]
request.getLocalName() = localhost
request.getLocalAddr() = 0:0:0:0:0:0:0:1
request.getLocalPort() = 8080
--- 기타 조회 end ---
*/
}
}
HTTP 메시지의 START LINE, 헤더 정보에 대한 조회 방법을 봤으니, 이제 HTTP 요청 데이터를 통해 클라이언트에서 서버로 데이터를 어떤 식으로 전달하는지 알아보자.
아래 3가지 방법을 잘 알아야 나중에 헷갈리지 않는다.
첫 번째는 GET 방식의 쿼리 파라미터다. 보통 URL들을 보면, /url?username=hello&age=20과 같은 형식으로 되어 있는 것을 볼 수 있다. 보통 GET 방식은 Body에 데이터를 보내지 않고, URL의 쿼리 파라미터에 데이터를 포함해서 전달한다. 예를 들어, 검색을 하거나 어떤 내용을 필터링 하든가 페이징과 같은 작업에서 많이 사용된다.
두 번째는 POST 방식 중에서 HTML 폼을 전송할 때다. 대표적으로 회원가입 할 때를 생각해보면 된다.
전송 버튼을 누르면 웹 브라우저가 HTML 메시지를 생성한다. Content-Type을 보면 application/x-www-form-urlencoded로 되어 있다. 메시지 Body에 데이터를 넣는다. 근데 여기서 주의할 점이 있는데 보면 쿼리 파라미터랑 비슷하게 생겼다. username=hello&age=20과 같은 형식이 x-www-form-urlencoded라는 방식인 것이다.
세 번째는 HTTP 메시지 바디에 데이터를 직접 담아서 요청한다. HTTP API에 주로 사용된다. 그냥 안에 JSON, XML 또는 텍스트 정보를 그대로 담아서 클라이언트에서 서버로 전달한다. POST, PUT, PATCH 등 여러 방식을 사용할 수 있다.
username에 “hello”, age에 20를 서버로 메시지 Body 없이 그냥 URL의 쿼리 파라미터를 사용해서 전달해보자. 쿼리 파라미터는 URL에 다음과 같이 ?를 시작으로 보낼 수 있다. 추가 파라미터는 &로 구분하면 된다.
package hello.servlet.basic.request;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 1. 파라미터 전송 기능
* http://localhost:8080/request-param?username=hello&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)));
request.getParameterNames();
System.out.println("[전체 파라미터 조회] - end");
System.out.println();
System.out.println("[단일 파라미터 조회] - start");
String username = request.getParameter("username");
String age = request.getParameter("age");
System.out.println("username = " + username);
System.out.println("age = " + age);
System.out.println("[단일 파라미터 조회] - end");
System.out.println();
System.out.println("[이름이 같은 복수 파라미터 조회] - start");
String[] usernames = request.getParameterValues("username");
for (String s : usernames) {
System.out.println("username = " + s);
}
System.out.println("[이름이 같은 복수 파라미터 조회] - end");
System.out.println();
}
}
/*
[전체 파라미터 조회] - start
username = hello
age = 20
[전체 파라미터 조회] - end
[단일 파라미터 조회] - start
username = hello
age = 20
[단일 파라미터 조회] - end
[이름이 같은 복수 파라미터 조회] - start
username = hello
[이름이 같은 복수 파라미터 조회] - end
*/
username이라는 파라미터 이름은 하나인데, 값이 중복되면 어떻게 해야 할까? 위의 코드와 같이 값이 하나만 있을 경우에는 그냥 request.getParameter()를 사용하면 된다. 값이 중복될 경우에 request.getParameterValues()를 사용해야 한다. 값이 중복인데 request.getParameter()를 사용하게 되면 request.getParameterValues()의 첫 번째 값만 반환된다. 근데 이렇게 중복으로 보내는 경우는 거의 없다고 봐도 무방하다. 그냥 알아두기만 하자.
이번엔 HTML의 폼을 사용해서 서버로 데이터를 전송해보자. 이 방식의 특징은 데이터가 일단 메시지 Body에 들어가기 때문에 application/x-www-form-urlencoded라는 Content-Type이 있다. 그리고 메시지 Body에 쿼리 파라미터 형식으로 데이터를 전달한다.
POST로 HTML 폼을 전송하게 되면 웹 브라우저는 아래와 같이 HTTP 메시지를 생성한다.
http://localhost:8080/request-paramapplication/x-www-form-urlencodedusername=hello&age=20
보다시피 전송을 누르면 웹 브라우저가 HTTP 요청 메시지를 만들어준다. Content-Type은 application/x-www-form-urlencoded, 내용은 쿼리 파라미터와 같은 형식으로 보낸다.
앞서 살펴본 GET 방식에서의 쿼리 파라미터와 같은 형식이기 때문에 request.getParameter()는 2가지 방식을 다 지원한다. 그냥 쓰면 된다. 클라이언트 입장에서는 두 방식에 차이는 있지만, 서버 입장에서는 request.getParameter()로 편리하게 둘 다 조회 가능하다.
이처럼 Content-Type은 HTTP 메시지 Body의 데이터 형식을 지정한다. GET 방식의 URL 쿼리 파라미터 형식으로 서버로 데이터를 전달할 때는 HTTP 메시지 Body를 사용하지 않기 때문에 Content-Type이 없다. 하지만, POST 방식의 HTML 폼 형식으로 데이터를 전달하면 HTTP 메시지 Body에 해당 데이터를 포함해서 보내기 때문에 Body에 포함된 데이터가 어떤 형식인지 꼭 지정해줘야 한다.
먼저 메시지 Body에 아주 단순한 텍스트 메시지를 담아서 전송하고, 읽어보자.
package hello.servlet.basic.request;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.util.StreamUtils;
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");
// messageBody = hello!
}
}
http://localhost:8080/request-body-string으로 POST 요청을 보냈다. Content-Type은 text/plain, Message Body는 “hello!” 다.
다음은 JSON 형식으로 데이터를 서버로 전송해보자. JSON이기 때문에 Content-Type은 application/json이다. 그리고 JSON 형식이기 때문에 JSON 형식의 데이터를 객체로 바꿀 수 있도록 파싱할 수 있게 만들고 서블릿 코드를 작성해야 한다.
package hello.servlet.basic;
import lombok.Getter;
import lombok.Setter;
@Getter @Setter
public class HelloData {
private String username;
private int age;
}
package hello.servlet.basic.request;
import com.fasterxml.jackson.databind.ObjectMapper;
import hello.servlet.basic.HelloData;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.util.StreamUtils;
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 {
ServletInputStream inputStream = request.getInputStream();
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
System.out.println("messageBody = " + messageBody); // messageBody = {"username": "hello", "age": 20}
HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
System.out.println("helloData.username = " + helloData.getUsername()); // helloData.username = hello
System.out.println("helloData.age = " + helloData.getAge()); // helloData.age = 20
response.getWriter().write("ok");
}
}
코드를 보면, JSON 결과를 파싱해서 사용할 수 있는 자바 객체로 변환하기 위해 Jackson이라는 JSON 변환 라이브러리를 추가해줬다. 스프링 부트로 Spring MVC를 선택하면 기본으로 Jackson 라이브러리(ObjectMapper)를 함께 제공한다.
HttpServletResponse는 간단하다. HTTP 응답 코드, 헤더, 바디를 포함한 HTTP 응답 메시지를 생성해준다. 추가로 Content-Type, 쿠키, Redirect와 같은 편의 기능도 제공한다. 바로 서블릿 코드를 작성해보자.
package hello.servlet.basic.response;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
@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");
// [Headers 편의 메서드]
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");
}
}
HTTP 응답 메시지는 위의 코드에서의 단순 텍스트 응답, HTML 응답, HTTP API로 응답하는 총 3가지 방식이 있다.
먼저 HTML 응답이다.
package hello.servlet.basic.response;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.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>안녕!</div>");
writer.println("</body>");
writer.println("</html>");
}
}
http://localhost:8080/response-html로 접속해서 소스를 확인해보면 아래와 같은 HTML 응답을 확인할 수 있다.

마지막으로 JSON 형식의 데이터로 응답받는 경우다.
package hello.servlet.basic.response;
import com.fasterxml.jackson.databind.ObjectMapper;
import hello.servlet.basic.HelloData;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
@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);
}
}
http://localhost:8080/response-json으로 접속하면 아래와 같이 JSON 형식의 데이터 응답을 볼 수 있다.

HTTP 응답으로 JSON을 반환할 때는 Content-Type을 application/json로 지정해야 한다.