-
[Spring] 서블릿의 기본적인 개요, Request, Response, GET, POST, JSON, HTTP messageBack-end/Spring 2022. 3. 18. 14:54
안녕하세요 이번 포스팅에서는 포스팅 누락으로 서블릿을 간단히 사용하는 방법이랑 여러 HTTP METHOD에 대해서 알아보겠습니다!!
스프링 부트 서블릿 환경 구성
@ServletComponentScan
스프링 부트는 서블릿을 직접 등록해서 사용할 수 있도록 @ServletComponentScan 을 지원합니다. 다음과 같이 프로젝트를 생성하고 추가합시다.
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); } }
서블릿 등록하기
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); } }
@WebServlet 서블리 어노테이션
- name : 서블릿 이름
- urlPatterns : URL 매핑
HTTP 요청을 통해 매핑된 URL이 호출되면 서블릿 컨테이너는 다음 메서드를 실행합니다.
protected void service(HttpServletRequest request, HttpServletResponse response)
http://localhost:8080/hello?username=world
위의 주소로 요청을 하게 되면 콘솔에는 다음과 같이 찍힙니다.
HTTP 요청 메시지 로그로 확인하기
application.proerties에 다음과 같이 추가합니다.
서버를 다시 시작하고 요청해보면 서버가 받은 HTTP 요청 메시지를 출력하는 것을 확인할 수 있습니다.
하지만 운영 서버에 이렇게 모든 요청 정보를 다 남기면 성능 저하가 발생할 수 있습니다. 그래서 개발 단계에서만 적용해야 합니다.
서블릿 컨테이너 동작 방식 설명
내장 톰캣 서버가 서블릿 컨테이너를 생성하고 @WebServlet이 달린 클래스를 찾아서 name으로 싱글톤 형태로 객체를 등록합니다.
그리고 http request가 오면 내장 톰캣 서버가 request 객체와 response객체를 생성해서 was에 넘겨줍니다.
welcome page 추가
지금부터 개발할 내용을 편리하게 참고할 수 있도록 welcome 페이지를 만들어 보겠습니다.
webapp 경로에 index.html을 두면 http://localhost:8080 호출 시 index.html 페이지가 열립니다.
경로 : main/webapp/index.html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <ul> <li><a href="basic.html">서블릿 basic</a></li> </ul> </body> </html>
이번에 학습할 내용은 다음 basic.html입니다.
main/webapp/basic.html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <ul> <li>hello 서블릿 <ul> <li><a href="/hello?username=servlet">hello 서블릿 호출</a></li> </ul> </li> <li>HttpServletRequest <ul> <li><a href="/request-header">기본 사용법, Header 조회</a></li> <li>HTTP 요청 메시지 바디 조회 <ul> <li><a href="/request-param?username=hello&age=20">GET - 쿼리 파라미터</a></li> <li><a href="/basic/hello-form.html">POST - HTML Form</a></ li> <li>HTTP API - MessageBody -> Postman 테스트</li> </ul> </li> </ul> </li> <li>HttpServletResponse <ul> <li><a href="/response-header">기본 사용법, Header 조회</a></li> <li>HTTP 응답 메시지 바디 조회 <ul> <li><a href="/response-html">HTML 응답</a></li> <li><a href="/response-json">HTTP API JSON 응답</a></li> </ul> </li> </ul> </li> </ul> </body> </html>
HttpServletRequest - 개요
HttpServletRequest 역할
HTTP 요청 메시지를 개발자가 직접 파싱 해서 사용해도 되지만, 매우 불편할 것입니다. 서블릿은 개발자가 HTTP 요청 메시지를 편리하게 사용할 수 있도록 개발자 대신에 HTTP 요청 메시지를 파싱 합니다. 그리고 그 결과를 HttpServletRequest 객체에 담아서 제공합니다. HttpServletRequest를 사용하면 다음과 같은 HTTP 요청 메시지를 편리하게 조회할 수 있습니다.
임시 저장소 기능
해당 HTTP 요청이 시작부터 끝날 때까지 유지되는 임시 저장소 기능도 있습니다.
저장 : request.setAttribute(name, value)
조회 : request.getAttribute(name)
세션 관리 기능
request.getSession(create : true)
중요
HttpServletRequest, HttpServletResponse를 사용할 때 가장 중요한 점은 이 객체들이 HTTP 요청 메시지, HTTP 응답 메시지를 편리하게 사용하도록 도와주는 객체라는 점입니다.
따라서 이 기능에 대해서 깊이 있는 이해를 하려면 HTTP 스펙이 제공하는 요청, 응답 메시지 자체를 이해해야 합니다.
HttpServletRequest- 기본 사용법
HttpServletRequest가 제공하는 기본 기능들을 알아봅시다.
import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.*; import java.io.IOException; //http://localhost:8080/request-header?username=hello @WebServlet(name = "requestHeaderServlet", urlPatterns = "/request-header") public class RequestHeaderServlet extends HttpServlet { @Override protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { printStartLine(request); printHeaders(request); printHeaderUtils(request); printEtc(request); response.getWriter().write("ok"); } } start-line 정보 //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.getProtocal() = " + 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 ---"); /* Enumeration<String> headerNames = request.getHeaderNames(); while (headerNames.hasMoreElements()) { String headerName = headerNames.nextElement(); System.out.println(headerName + ": " + request.getHeader(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(); } //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(); }
printStartLine 결과
printHeaders 결과
printHeaderutils 결과
HTTP 요청 데이터 - 개요
HTTP 요청 메시지를 통해 클라이언트에서 서버로 데이터를 전달하는 방법을 알아보겠습니다.
주로 다음 3가지 방법을 사용합니다.
POST-HTML Form 예시
HTTP 요청 데이터- GET 쿼리 파라미터
메시지 바디 없이, URL의 쿼리 파라미터를 사용해서 데이터를 전달해봅시다.
예) 검색, 필터, 페이징 등에서 많이 사용하는 방식
쿼리 파라미터는 URL에 다음과 같이?를 시작으로 보낼 수 있습니다. 추가 파라미터는 &으로 구분하면 됩니다.
서버에서는 HttpServletRequest가 제공하는 다음 메서드를 통해 쿼리 파라미터를 편리하게 조회할 수 있습니다.
쿼리 파라미터 조회 메서드
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=hello&age=20 * <p> * 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 resp) throws ServletException, IOException { System.out.println("[전체 파라미터 조회] - start"); /* Enumeration<String> parameterNames = request.getParameterNames(); while (parameterNames.hasMoreElements()) { String paramName = parameterNames.nextElement(); System.out.println(paramName + "=" + request.getParameter(paramName)); } */ 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); } resp.getWriter().write("ok"); } }
결과
복수 파라미터에서 단일 파라미터 조회
username=hello&username=kim과 같이 파라미터 이름은 하나인데, 값이 중복이면 어떻게 될까요?? request.getParameter()는 하나의 파라미터 이름에 대해서 단 하나의 값만 있을 때 사용해야 합니다. 지금처럼 중복일 때는 request.getParameterValues()를 사용해야 합니다. 참고로 이렇게 중복일 때 request.getParameter()를 사용하면 request.getParameterValues()의 첫 번째 값을 반환합니다.
HTTP 요청 데이터 - POST HTML Form
이번에는 HTML의 Form을 사용해서 클라이언트에서 서버로 데이터를 전송해보겠습니다.
주로 회원 가입, 상품 주문 등에서 사용하는 방식입니다.
경로 : src/main/webapp/basic/hello-form.html
<!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>
실행해봅시다.
주의
웹 브라우저가 결과를 캐시 하고 있어서, 과거에 작성했던 html 결과가 보이는 경우도 있습니다. 이때는 웹 브라우저의 새로 고침을 직접 선택해주면 됩니다. 물론 서버를 재시작하지 않아서 그럴 수도 있습니다
POST의 HTML Form을 전송하면 웹 브라우저는 다음 형식으로 HTTP 메시지를 만듭니다.
application/x-www-form-urlencoded 형식은 앞서 GET에서 살펴본 쿼리 파라미터 형식과 같습니다. 따라서 쿼리 파라미터 조회 메서드를 그대로 사용하면 됩니다.
클라이언트 입장에서는 두 방식에 차이가 있지만, 서버 입장에서는 둘의 형식이 동일하므로, request.getParameter()로 편리하게 구분 없이 조회할 수 있습니다.
참고
content-type은 HTTP 메시지 바디의 데이터 형식을 지정합니다.
GET URL 쿼리 파라미터 형식으로 클라이언트에서 서버로 데이터를 전달할 때는 HTTP 메시지 바디를 사용하지 않기 때문에 content-type이 없습니다.
POST HTML Form 형식으로 데이터를 전달하면 HTTP 메시지 바디에 해당 데이터를 포함해서 보내기 때문에 바디에 포함된 데이터가 어떤 형식인지 content-type을 꼭 지정해야 합니다. 이렇게 폼으로 데이터를 전송하는 형식을 application/x-www-form-urlencoded 라 합니다.
HTTP 요청 데이터 - API 메시지 바디 - 단순 텍스트
HTTP message body에 데이터를 직접 담아서 요청할 수 있습니다.
HTTP API(REST API)에서 주로 사용하며 JSON, XML, TEXT 형식으로 전송합니다.
그중에서 데이터 형식은 주로 JSON을 사용합니다.
POST, PUT, PATCH로 message body에 데이터를 담을 수 있습니다.
먼저 가장 단순한 텍스트 메시지를 HTTP 메시지 바디에 담아서 전송하고 읽어봅시다.
HTTP 메시지 바디의 데이터를 InputStera을 사용해서 직접 읽을 수 있습니다.
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-bodystring") 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"); } }
참고
inputStream은 byte 코드를 반환합니다. byte 코드를 우리가 읽을 수 있는 문자(String)로 보려면 문자 표 (Charset)를 지정해주어야 합니다. 여기서는 UTF_8 Charset을 지정해주었다.
Postman을 사용해서 테스트를 해봅시다.
위와 같이 설정하고 post 요청을 하면 결과가 콘솔에 출력이 됩니다.
HTTP 요청 데이터 - API 메시지 바디 - JSON
이번에는 HTTP API에서 주로 사용하는 JSON 형식으로 데이터를 전달해봅시다.
JSON 형식 파싱 추가
JSON 형식으로 파싱 할 수 있게 객체를 하나 생성합시다.
import lombok.Getter; import lombok.Setter; @Getter @Setter public class HelloData { private String username; private int age; }
import com.fasterxml.jackson.databind.ObjectMapper; import hello.servlet.basic.HelloData; 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; /** * http://localhost:8080/request-body-json * * JSON 형식 전송 * content-type: application/json * message body: {"username": "hello", "age": 20} * */ @WebServlet(name = "requestBodyJsonServlet", urlPatterns = "/request-bodyjson") 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"); } }
Postman으로 다음과 같이 실행합니다.
출력 결과
참고
JSON 결과를 파싱 해서 사용할 수 있는 자바 객체로 변환하려면 Jackson, Gson 같은 JSON 변환 라이브러리를 추가해서 사용해야 합니다. 스프링 부트로 Spring MVC를 선택하면 기본으로 Jackson 라이브러리(ObjectMapper)를 함께 제공합니다.
HttpServletResponse - 기본 사용법
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, mustrevalidate"); 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"); } }
Content 편의 메서드
쿠키 편의 메서드
redirect 편의 메서드
HTTP 응답 데이터 - 단순 텍스트, HTML
HTTP 응답 메시지는 주로 다음 내용을 담아서 전달합니다.
1. 단순 텍스트 응답 (writer.println("ok"))
2. HTML 응답
3. HTTP API - MessageBody JSON 응답
HttpServletResponse - HTML 응답
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>안녕?</div>"); writer.println("</body>"); writer.println("</html>"); } }
HTTP 응답으로 HTML을 반환할 때는 content-type을 text/html로 지정해야 합니다.
HTTP 응답 데이터 - API JSON
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("kim"); data.setAge(20); //{"username":"kim","age":20} String result = objectMapper.writeValueAsString(data); response.getWriter().write(result); } }
HTTP 응답으로 JSON을 반환할 때는 content-type을 application/json으로 지정해야 합니다.
Jackson 라이브러리가 제공하는 objectMapper.writeValueAsString()을 사용하면 객체를 JSON 문자로 변경할 수 있습니다.
참고
application/json 은 스펙상 utf-8 형식을 사용하도록 정의되어 있습니다. 그래서 스펙에서 charset=utf-8과 같은 추가 파라미터를 지원하지 않습니다.
따라서 application/json 이라고만 사용해야지 application/json;charset=utf-8이라고 전달하는 것은 의미 없는 파라미터를 추가한 것이 됩니다.
response.getWriter()를 사용하면 추가 파라미터를 자동으로 추가해버린다. 이때는 response.getOutputStream()으로 출력하면 그런 문제가 없습니다.
'Back-end > Spring' 카테고리의 다른 글
[Spring] 스프링 MVC - 구조 이해 (0) 2022.03.20 [Spring] MVC 프레임 워크 만들기 ( 1 ), 프론트 컨트롤러, 어댑터 구조 (0) 2022.03.18 [Spring]서블릿의 기본 사용법 예제 ( 2 ), JSP로 회원 웹 애플리케이션 개발 그리고 MVC패턴의 등장, MVC의 한계 (0) 2022.03.18 [Spring] 서블릿의 기본 사용법 예제 ( 1 ) (0) 2022.03.18 [Spring] 빈 스코프(Bean Scope) (0) 2022.03.07