1. Hello 서블릿
프로젝트 환경
Java: 21
Spring Boot: 3.x.x
Dependencies: Spring Web, Lombok
Packaging: War → 주의: JSP를 실행하기 위해서 Jar 대신 War로 설정해야 한다.
프로젝트를 생성한 뒤 [File - Setting]에서 [Annotation Processors]를 검색하고, Enable annotation processing 체크 (재시작)
- 임의의 테스트 class를 만들고 @Getter 확인
아래 사이트에서 OS에 맞는 Postman 다운로드 받고 설치하기
스프링 부트 서블릿 환경 구성
참고
서블릿은 톰캣 같은 웹 애플리케이션 서버(WAS)를 직접 설치하고, 그 위에 서블릿 코드를 class 파일로 빌드해서 올린 다음, 톰캣 서버를 실행하면 된다. 그러나 이 과정은 매우 번거롭다.
스프링 부트는 톰캣 서버를 내장하고 있으므로, 톰캣 서버 설치 없이 편리하게 서블릿 코드를 실행할 수 있다.
a. @ServletComponentScan
스프링 부트는 서블릿을 직접 등록해서 사용할 수 있도록 이 애노테이션을 지원한다. 아래와 같이 추가하면 Spring이 자동으로 현재 내 패키지를 포함한 하위 패키지를 다 훑은 다음, Servlet을 다 찾아서 자동으로 등록하고 실행할 수 있도록 도와준다.
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);
}
}
b. 실제 동작하는 서블릿 코드 등록하기
Intellij 단축키/자동화 코드
Ctrl + O: 상속받은 class에서 override 또는 implement할 메소드 선택
soutm: 메소드 명 출력 (sout 뒤에 붙는 값에 따라 parameter나 value 값도 출력 가능)
@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);
}
}
@WebServlet(name = "", urlPatterns = "") 서블릿 애노테이션 (속성에 중복 값이 있으면 안 됨)
- name: 서블릿 이름 / urlPatterns: URL 매핑
request.getParameter("");
- HTTP 요청 메시지에 있는 파라미터 값을 꺼내올 수 있음
HTTP 요청을 통해 매핑된 URL이 호출되면 서블릿 컨테이너는 다음 메서드를 실행한다.
protected void service(HttpServletRequest request, HttpServletResponse response)
HelloServlet.service
request = org.apache.catalina.connector.RequestFacade@3bb9d1f7
response = org.apache.catalina.connector.ResponseFacade@6ebc2787
username = kim
c. HTTP 요청 메시지 로그로 확인하기
application.properties에 다음 설정을 추가하고 서버를 다시 시작한 뒤 요청해보면, 서버가 받은 HTTP 요청 메시지를 출력하는 것을 확인할 수 있다.
logging.level.org.apache.coyote.http11=trace
참고
운영 서버에 이렇게 모든 요청 정보를 다 남기면 성능 저하가 발생할 수 있기 때문에 개발 단계에서만 사용하는 것이 좋다.
서블릿 컨테이너 동작 방식
스프링 부트를 실행하면 내장 톰캣 서버가 생성된다. 톰캣 서버는 내부에 서블릿 컨테이너 기능을 가지고 있고, 서블릿 객체를 생성해 서블릿 컨테이너에서 관리한다.
웹 브라우저의 요청에 따라 HTTP 요청 메세지가 만들어지고 서버에 전달된다.
이후 전달받은 HTTP 요청 메시지를 기반으로 request와 response 객체를 만들어 service 메서드에 해당 객체를 넘겨 실행한다. 이후 메서드 내부에서 필요한 작업이 끝나면 WAS 서버가 response 객체 정보로 HTTP 응답을 생성해 웹 브라우저에 전달한다.
참고
HTTP 응답에서 Content-Length는 WAS가 자동으로 생성해준다.
Welcome 페이지 추가
webapp 경로에 index.html 파일을 만들어 두면 http://localhost:8080 호출 시 index.html 페이지가 열린다.
2. HttpServletRequest
개요
HttpServletRequest 역할
HTTP 요청 메시지를 개발자가 직접 파싱해서 사용해도 되지만 늘 그렇게 하는 건 매우 불편할 것이다. 서블릿은 개발자 대신 HTTP 요청 메시지를 파싱하고 그 결과를 HttpServletRequest 객체에 담아서 제공한다.
HttpServletRequest를 사용하면 다음과 같은 HTTP 요청 메시지를 편리하게 조회할 수 있다.
POST /save HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded
username=kim&age=20
- Start Line - HTTP 메서드, URL, 쿼리 스트링, 스키마&프로토콜
- 헤더 - 헤더 조회
- 바디 - form 파라미터 형식 조회, message body 데이터 직접 조회
이외의 여러가지 부가 기능도 함께 제공한다.
임시 저장소 기능
- 해당 HTTP 요청이 시작부터 끝날 때까지 유지되는 기능
- 저장: request.setAttribute(name, value)
- 조회: request.getAttribute(name)
세션 관리 기능
- request.getSession(create: true)
중요💫
HttpServletRequest와 HttpServletResponse를 사용할 때 가장 중요한 점은 이 객체들이 HTTP 요청 메시지, HTTP 응답 메시지를 편하게 쓰도록 도와주는 객체라는 것이다. 따라서 이 기능에 대해 깊이 있게 이해하려면 HTTP 스펙이 제공하는 요청, 응답 메시지 자체를 이해해야 한다.
기본 사용법
참고
로컬에서 테스트하면 IPv6 정보가 나오는데, IPv4 정보를 보고 싶으면 다음 옵션을 VM options에 넣어주면 된다.
-Djava.net.preferIPv4Stack=true
a. HTTP 메시지 start-line 정보 조회
--- 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 ---
- isSecure(): https 사용 유무
b. HTTP 메시지 헤더 정보 조회
헤더 정보 조회 코드
//Header 모든 정보
private void printHeaders(HttpServletRequest request) {
System.out.println("--- Headers - start ---");
// 방법 1
/*
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
System.out.println(headerName + ": " + request.getHeader(headerName));
}
*/
// 방법 2
request.getHeaderNames().asIterator()
.forEachRemaining(headerName -> System.out.println(headerName + ": "
+ request.getHeader(headerName)));
System.out.println("--- Headers - end ---");
System.out.println();
}
헤더 정보 조회 결과
--- Headers - start ---
host: localhost:8080
connection: keep-alive
sec-ch-ua: "Google Chrome";v="129", "Not=A?Brand";v="8", "Chromium";v="129"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
upgrade-insecure-requests: 1
user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 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.7
sec-fetch-site: none
sec-fetch-mode: navigate
sec-fetch-user: ?1
sec-fetch-dest: document
accept-encoding: gzip, deflate, br, zstd
accept-language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7
cookie: csrftoken=5TAmjYZ7z1t8m4Z6Jz8g8Pf9nFrANF7Fosg2e8kLsriSEI9pajYFVn7bi5XEn7OG
--- Headers - end ---
c. HTTP 메시지 기타 정보 조회
기타 정보는 HTTP 메시지의 정보는 아니다.
- Remote 정보: 요청에 대한 정보(내부에서 network connection이 맺어진 정보로 알 수 있음)
- Local 정보: 내 서버에 대한 정보
--- 기타 조회 start ---
[Remote 정보]
request.getRemoteHost() = 0:0:0:0:0:0:0:1
request.getRemoteAddr() = 0:0:0:0:0:0:0:1
request.getRemotePort() = 63431
[Local 정보]
request.getLocalName() = 0:0:0:0:0:0:0:1
request.getLocalAddr() = 0:0:0:0:0:0:0:1
request.getLocalPort() = 8080
--- 기타 조회 end ---
3. HTTP 요청 데이터
개요
HTTP 요청 메시지를 통해 클라이언트에서 서버로 데이터를 전달하는 방법은 여러가지가 있지만, 주로 다음 3가지 방법을 사용한다.
GET - 쿼리 파라미터
- /url?username=hi&age=20
- 메시지 바디 없이, URL의 쿼리 파라미터에 데이터를 포함해서 전달한다.
- ex. 검색, 필터, 페이징 등에서 많이 사용하는 방식
POST - HTML Form
- content-type: application/x-www-form-unlencoded
- 메시지 바디에 쿼리 파라미터 형식으로 전달한다.
- username=hi&age=20
- ex. 회원가입, 상품 주문, HTML Form 사용
HTTP message body
- HTTP API에서 주로 사용하며, 데이터 형식은 주로 JSON을 사용한다. (JSON, XML, TEXT)
- POST, PUT, PATCH
GET - 쿼리 파라미터
아래 데이터를 클라이언트에서 서버로 전송하려고 한다. 이때 메시지 바디 없이 URL의 쿼리 파라미터를 사용해 데이터를 전달한다.
- username=hi
- age=20
쿼리 파라미터는 URL에 다음과 같이 ?를 시작으로 보낼 수 있다. 추가 파라미터는 &로 구분하면 된다.
http://localhost:8080/request-param?username=hi&age=20
서버에서는 HttpServletRequest가 제공하는 다음 메서드를 통해 쿼리 파라미터를 편리하게 조회할 수 있다.
// 전체 파라미터 조회
request.getParameterNames().asIterator().forEachRemaining(paramName ->
System.out.println(paramName + "=" + request.getParameter(paramName)));
// 파라미터 이름들 모두 조회
Enumeration<String> parameterNames = request.getParameterNames();
// 파라미터를 Map으로 조회
Map<String, String[]> parameterMap = request.getParameterMap();
// 단일 파라미터 조회
System.out.println("request.getParameter(username) = " + username);
// 이름이 같은 복수 파라미터 조회
String[] usernames = request.getParameterValues("username");
for (String name : usernames) {
System.out.println("username=" + name);
}
- request.getParameter(): 하나의 파라미터 이름에 대해 단 하나의 값만 있을 때 사용해야 한다. 중복일 때 사용하면 request.getParameterValues()의 첫번째 값을 반환한다.
- request.getParameterValues(): 하나의 파라미터 이름에 대해 중복인 값이 있을 때 사용한다.
POST - HTML Form
이번에는 HTML의 Form을 사용해서 클라이언트에서 서버로 데이터를 전송해보려고 한다. 이 방식을 사용하면 content-type은 application/x-www-form-urlencoded로 들어가게 되고, 메시지 바디에 쿼리 파라미터 형식으로 데이터가 전달된다.
- username=kim&age=20
a. Form을 만들어 테스트
webapp 경로에 form.html을 만들어 서버에서 확인해보면 Form이 나타나는 걸 볼 수 있다. POST의 HTML Form을 전송하면 웹 브라우저는 다음 형식으로 HTTP 메시지를 만든다.
- 요청 URL: http://localhost:8080/request-param
- content-type: application/x-www-form-urlencoded
- message body: username=kim&age=20
request.getParameter()는 GET URL 쿼리 파라미터 형식도 지원하고, POST HTML Form 형식도 지원한다. 클라이언트(웹 브라우저) 입장에서는 두 방식에 차이가 있지만, 서버 입장에서는 둘의 형식이 동일하므로, request.getParameter()로 편리하게 구분없이 조회할 수 있다.
참고
content-type은 HTTP 메시지 바디의 데이터 형식을 지정한다.
GET URL 쿼리 파라미터 형식으로 클라이언트에서 서버로 데이터를 전달할 때는 HTTP 메시지 바디를 사용하지 않기 때문에 content-type이 없다.
POST HTML Form 형식으로 데이터를 전달하면 HTTP 메시지 바디에 해당 데이터를 포함해서 보내기 때문에 바디에 포함된 데이터가 어떤 형식인지 content-type을 꼭 지정해야 한다. 이렇게 폼으로 데이터를 전송하는 형식을 application/x-www-form-urlencoded라고 한다.
b. Postman으로 테스트
HTML Form을 만들기 귀찮다면 Postman으로 간단하게 테스트할 수 있다.
API 메시지 바디 - 단순 텍스트
가장 단순한 텍스트 메시지를 HTTP 메시지 바디에 담아서 전송하면, 그 값을 꺼내 서버에서 읽어볼 수 있다. InputStream을 사용해서 HTTP 메시지 바디의 데이터를 직접 읽을 수 있다.
@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");
}
content-type은 text/plain으로 들어가고 서버엔 messageBody = hello!가 출력된다.
참고
inputStream은 byte 코드를 반환한다. byte 코드를 우리가 읽을 수 있는 문자(String)로 보려면 문자표(Charset)를 지정해줘야 한다. 여기서는 UTF_8 Charset을 지정해줬다.
API 메시지 바디 - JSON
이번에는 HTTP API에서 주로 사용하는 JSON 형식으로 데이터를 전달해보려고 한다.
JSON 형식으로 파싱할 수 있도록 객체를 하나 생성해야 한다. Lombok에서 제공하는 애노테이션 덕분에 코드를 간결하게 작성할 수 있다.
@Getter @Setter
public class HelloData {
private String username;
private int age;
}
JSON 결과를 파싱해서 사용할 수 있는 자바 객체로 변환하려면 Jackson, Gson 같은 JSON 변환 라이브러리(JSON to Object)를 추가해서 사용해야 한다. 스프링 부트로 Spring MVC를 선택하면 기본으로 Jackson 라이브러리(ObjectMapper)를 함께 제공한다.
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.getUsername() = " + helloData.getUsername());
System.out.println("helloData.getAge() = " + helloData.getAge());
response.getWriter().write("ok");
}
content-type은 application/json으로 들어가고, 서버엔 아래 내용이 출력된다.
{"username":"kim","age":20}]
messageBody = {"username":"kim","age":20}
helloData.getUsername() = kim
helloData.getAge() = 20
참고
HTML Form 데이터도 메시지 바디를 통해 전송되므로 InputStream을 통해 직접 읽을 수 있다. 하지만 편리한 파라미터 조회 기능(request.getParameter(""))을 제공하므로 이 기능을 사용하면 된다.
4. HttpServletResponse
역할
HTTP 응답 메시지 생성
- HTTP 응답 코드 지정(200, 300, 400, ...)
- 헤더 및 바디 생성
편의 기능 제공
- Content-Type, 쿠키, Redirect
기본 사용법
아래 코드처럼 HttpServletResponse에서 제공하는 다양한 메서드를 통해 응답 코드와 header, message body를 설정할 수 있다.
@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"); // cache 무효화
response.setHeader("Pragma", "no-cache"); // cache 무효화
response.setHeader("my-header","hello"); // 임의의 header 생성 가능
// [Header 편의 메서드]
content(response);
cookie(response);
redirect(response);
// [message body]
PrintWriter writer = response.getWriter();
writer.println("ok"); // println이라 3 byte로 계싼됨
}
a. Content 편의 메서드
- Content-Type: text/plain;charset=utf-8
- Content-Length: 3
위 헤더를 아래 코드로 간단하게 작성할 수 있다.
private void content(HttpServletResponse response) {
//response.setHeader("Content-Type", "text/plain;charset=utf-8");
response.setContentType("text/plain");
response.setCharacterEncoding("utf-8");
//response.setContentLength(3); //(생략시 자동 생성)
}
b. 쿠키 편의 메서드
- Set-Cookie: myCookie=good; Max-Age=600;
위 헤더를 아래 코드로 간단하게 작성할 수 있다.
private void cookie(HttpServletResponse response) {
//response.setHeader("Set-Cookie", "myCookie=good; Max-Age=600");
Cookie cookie = new Cookie("myCookie", "good");
cookie.setMaxAge(600); //600초
response.addCookie(cookie);
}
c. redirect 편의 메서드
- Status Code: 302
- Location: /basic/hello-form.html
위 헤더를 아래 코드로 간단하게 작성할 수 있다.
private void redirect(HttpServletResponse response) throws IOException {
// response.setStatus(HttpServletResponse.SC_FOUND); // 302
// response.setHeader("Location", "/basic/hello-form.html");
response.sendRedirect("/basic/hello-form.html");
}
5. HTTP 응답 데이터
단순 텍스트, HTML
HTTP 응답으로 HTML을 반환할 때는 content-type을 text/html로 지정해야 한다.
@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>");
}
작성된 HTML은 개발자 모드에서 확인할 수 있다.
API JSON
HTTP 응답으로 JSON 형식 데이터를 반환할 때는 content-type을 appllication/json으로 지정해야 한다.
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);
}
소스 코드는 개발자 모드에서 확인할 수 있다.