[인프런/스프링 MVC 1편] 2. 서블릿
서블릿
- 동적 웹페이지를 만들 때 사용되는 자바 기반의 웹 애플리케이션 프로그래밍 기술
- 웹 요청과 응답의 흐름을 간단한 메소드 호출만으로 체계적으로 다룰 수 있게 해줌
- 서버에서 실행되다가 웹 브라우저에서 요청을 하면 해당 기능을 수행한 후 웹 브라우저에 결과 전송
- 기존의 정적 웹 프로그램의 문제점을 보완하여 동적인 여러가지 기능 제공
- MVC 패턴에서 컨트롤러로 이용
* 스프링 부트 - 톰캣 서버 내장 -> 웹 애플리케이션 설치 없이 서블릿 코드 실행 가능
스프링 부트 서블릿 환경 구성
- hello.servlet.ServletApplication
- @ServeltComponentScan 추가: 서블릿 자동 등록
- hello.servlet.basic.HelloServlet
- HelloServlet extends HttpServlet
: HelloServlet 클래스는 HttpServlet을 상속 - @WebServlet(name = "helloServlet", urlPatterns = "/hello")
: 서블릿 어노테이션(name: 서블릿 이름, urlPatterns: URL 매핑) - ctrl + o -> 키 모양 service 메소드
: HTTP 요청을 통해 매핑된 URL이 호출되면 서블릿 컨테이너가 service 메소드 실행
service 메소드 내
1) HelloServlet.service 출력
2) request 객체, response 객체 출력 -> WAS에서 구현하는 Request, Response 인터페이스 구현체
3) 쿼리파라미터 출력
-> 실행(run)
-> localhost:8080/hello
-> localhost:8080/hello?username=kim (쿼리파라미터 입력 -> request 요청 객체)
-> 브라우저 빈 창 확인 (응답 보낸게 없으니까)
-> 콘솔창에서 출력한 내용들 확인
response 객체에 담기
ContentType, CharacterEncoding 설정 -> 응답객체의 헤더에 들어감
getWriter().write("내용") -> 응답메시지
-> 실행(run)
설정했던 ContentType, CharacterEncoding -> 응답 헤더
HTTP 요청 메시지 로그로 확인하기
- application.properties
logging.level.org.apache.coyote.http11=debug
-> 서버 다시 실행
서버가 받은 HTTP 요청 메시지 출력
- 인코딩된 것, 요청 메시지 정보
(나머지는 WAS가 자동으로 생성해줌)
서블릿 컨테이너 동작 방식
스프링부트 -> 내장 톰캣 서버 (WAS)의 서블릿 컨테이너가 서블릿 객체 생성
-> HTTP 요청 -> service 메소드 실행하면서 request, response 객체 전달
(메소드 실행하면 쓰레드가 서블릿 객체 호출, request, response 객체를 서블릿에 전달)
(서블릿 컨테이너에서 HTTP 요청 메시지 기반으로 request 객체 생성, response 객체 생성)
-> 서블릿이 종료되면서 response 객체에 HTTP 응답 정보 담아서 반환
Welcome 페이지 추가
(그냥 간편히 이동하기 위해 만들어 둔 것)
* webapp 경로에 index.html 을 두면 http://localhost:8080 호출시 index.html 페이지가 열린다.
HttpServletRequest 개요
- HttpServletRequest 역할
: 서블릿은 개발자가 HTTP 요청 메시지를 편리하게 사용할 수 있도록 개발자 대신에 HTTP 요청 메시지를 파싱함. 그리고 그 결과를 HttpServletRequest 객체에 담아서 반환 - HttpServletRequest의 부가기능
- 임시 저장소 기능 (해당 HTTP 요청 시작부터 끝까지 유지되는 임시 저장소 기능)
- 저장 : request.setAttribute(name, value)
- 조회: request.getAttribute(name) - 세션 관리 기능
request.getSession(create: true)
- 임시 저장소 기능 (해당 HTTP 요청 시작부터 끝까지 유지되는 임시 저장소 기능)
HttpServletRequest 기본 사용법
RequestHeaderServlet 클래스
서블릿 생성 -> 메소드 호출
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
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);
printHeaders(request);
printHeaderUtils(request);
printEtc(request);
}
http://localhost:8080/request-header 호출
1) HttpServletRequest의 start line 정보
// start-line
private static 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
System.out.println("request.getRequestURL() = " + request.getRequestURL()); // http://localhost:8080/request-header
System.out.println("request.getRequestURI() = " + request.getRequestURI()); // /request-header
System.out.println("request.getQueryString() = " + request.getQueryString()); // username=hi
System.out.println("request.isSecure() = " + request.isSecure()); // https 사용 유무
System.out.println("--- REQUEST-LINE - end ---");
System.out.println();
}
2) HttpServletRequest의 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();
}
3) HttpServletRequest의 Header 편리한 조회
//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();
}
4) HttpServletRequest의 기타 정보 제공
//기타 정보
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 요청 데이터
- GET - 쿼리 파라미터
- /url?usename=hello&age=20
- 메시지 바디 X , URL의 쿼리 파라미터에 데이터 포함하여 전달
- 예) 검색, 필터, 페이징 등
- POST - HTML Form
- content-type: application/x-www-form-urlencoded
- 메시지 바디에 쿼리 파라미터 형식으로 전달 username=hello&age=20
- 예) 회원가입, 상품주문, HTML Form 사용
- HTTP message body에 데이터를 직접 담아서 요청
- HTTP API(REST API)에서 주로 사용, JSON, XML, TEXT
1. HTTP 요청 데이터 - GET 쿼리 파라미터
- http://localhost:8080/request-param?username=hello&age=20&username=hello2
- 서버에서 HttpServletRequest가 제공하는 다음 메소드를 통해 쿼리 파라미터를 편리하게 조회할 수 있다.
- 쿼리 파라미터 조회 메소드
String username = request.getParameter("username"); // 단일 파라미터 조회(가장 많이 씀)
Enumeration<String> parameterNames = request.getParameterNames(); // 파라미터 이름들
모두 조회 (옛날 방식)
request.getParameterNames().asIterator() // 파라미터 이름들 모두 조회 (요즘 방식)
.forEachRemaining(paramName -> System.out.println(paramName + "=" + request.getParameter(paramName)));
Map<String, String[]> parameterMap = request.getParameterMap(); // 파라미터를 Map
으로 조회
String[] usernames = request.getParameterValues("username"); // 복수 파라미터 조회
* RequestParamServlet
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;
import java.util.Enumeration;
/**
* 1. 파라미터 전송 기능
* http://localhost:8080/request-param?username=hello&age=20
* 2. 동일한 파라미터 전송 기능
* http://localhost:8080/request-param?username=hello&age=20&username=hello2
*/
@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");
String age = request.getParameter("age");
System.out.println("username = " + username);
System.out.println("age = " + age);
System.out.println();
System.out.println("[이름이 같은 복수 파라미터 조회]");
String[] usernames = request.getParameterValues("username");
for (String name : usernames) {
System.out.println("username = " + name);
}
}
}
파라미터 전송
http://localhost:8080/request-param?username=hello&age=20&username=hello2
결과
복수 파라미터에서 단일 파라미터 조회
파라미터 이름은 username으로 하나인데, 값이 중복이면? request.getParameterValues() 사용
중복일 때 request.getParameter()를 사용하면 복수 파라미터 값 중 첫번째 값 반환
2. HTTP 요청 데이터 - POST HTML Form
- 메시지 바디에 쿼리 파라미터 형식으로 데이터 전달 (username=hello&age=20)
- content-type : application/x-www-form-urlencoded
- content-type은 HTTP 메시지 바디의 데이터 형식 지정
- GET URL 쿼리 파라미터 형식으로 클라이언트에서 서버로 데이터 전달할 때는 HTTP 메시지 바디를 사용하지 않기 때문에 content-type 없음 !
- POST HTML Form 형식으로 데이터 전달하면 HTTP 메시지 바디에 해당 데이터를 포함해서 보내기 때문에 바디에 포함된 데이터가 어떤 형식인지 content-type 꼭 지정!
- 이렇게 폼으로 데이터를 전송하는 형식을 application/x-www-form-urlencoded라고 함!
* 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>
* action = "/request-param" ---> 전에 만든 서블릿으로 전송
실행
값 입력 후 전송
웹 브라우저는 다음 형식으로 HTTP 메시지를 만듦
요청 URL : http://localhost:8080/request-param
content-type : application/x-www-form-urlencoded
message body : username=hello&age=20
application/x-www-form-urlencoded
형식은 GET에서의 쿼리 파라미터 형식과 같음 (username=kim&age=20)
따라서 쿼리 파라미터 조회 메소드 그대로 사용! (request.getParameter())
-> 클라이언트 입장에서는 두 방식에 차이가 있지만, 서버 입장에서는 둘의 형식이 동일하므로,
request.getParameer()로 편리하게 구분없이 조회
request.getParameter() - GET URL 쿼리 파라미터 형식도 지원, POST HTML Form 형식도 지원
Postman을 사용한 테스트
HTML Form 만들지 않고 간단한 테스트
Body -> x-www-form-urlencoded 선택
Headers -> Content-Type : application/x-www-form-urlencoded 확인
Send 후 결과 확인
3. HTTP 요청 데이터 - API 메시지 바디
- HTML 을 통한 데이터 전송이 아닌, HTTP message body에 데이터 직접 담아서 요청
- HTTP API (REST API)에서 주로 사용, JSON, XML, TEXT
- 데이터 형식은 주로 JSON 사용
- POST, PUT, PATCH 방식에서 주로 쓰임
1) 단순 텍스트
- 단순한 텍스트 메시지를 HTTP 메시지 바디에 담아서 전송하고 읽어보기
- HTTP 메시지 바디의 데이터를 InputStream을 사용해서 직접 읽을 수 있음
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 {
// inputStream : byte 코드 반환
ServletInputStream inputStream = request.getInputStream();
// byte를 문자로 변환
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
System.out.println("messageBody = " + messageBody);
// 응답화면에 띄울 내용
response.getWriter().write("ok");
}
}
Postman 으로 테스트
텍스트 : raw -> Text -> 텍스트 입력 (-> Body에 직접 입력)
Content-Type : text/plain 확인
Send 후 Body 응답 ok 확인 -> 콘솔창 text 확인
결과
POST http://localhost:8080/request-body-string
content-type : text/plain
message body : hello!
출력결과 : messageBody = hello!
2) JSON
JSON 형식으로 전송
POST http://localhost:8080/request-body-json
content-type : application/json
message body : {"username": "hello", "age": 20}
결과 : messageBody = {"username": "hello", "age": 20}
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 {
// JSON <-> 자바 객체로 변환해주는 스프링부트의 Jackson 라이브러리
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);
// JSON -> Java (messageBody로 받은 JSON 데이터를 HelloDao 객체에 파싱해서 넣음)
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 데이터 입력
출력결과
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] : HTTP 응답코드 지정 (SC_OK : 200)
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"); // 사용자정의 헤더
// [Header 편의 메소드]
content(response);
cookie(response);
redirect(response);
// [message body]
PrintWriter writer = response.getWriter();
writer.println("ok");
}
// Content 편의 메소드
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);
}
// redirect 편의 메소드
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 생성 (응답코드, 헤더 등) / 편의 메소드 적용
- 쿠키 편의 메소드 적용
- redirect 편의 메소드 적용
HTTP 응답 데이터
- 단순 텍스트 응답
- 앞에서 살펴봄 ( writer.println("ok"); )
- HTML 응답
- HTTP API - MessageBody JSON 응답
1) HttpServletResponse - HTML 응답
- HTTP 응답으로 HTML을 반환할 때는 content-type을 "text/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 {
// HTTP 응답으로 HTML을 반환할 때는 Content-Type을 text/html로 지정해야 함
response.setContentType("text/html");
// 인코딩을 utf-8로 해줘야 한글 안깨짐
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 응답
2) HTTPServletResponse - API JSON 응답
- HTTP 응답으로 JSON을 반환할 때는 Content-Type을 application/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 {
// JSON <-> 자바 객체 변환하는 Jackson 라이브러리
private ObjectMapper objectMapper = new ObjectMapper();
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// HTTP 응답으로 JSON을 반환할 때는 Content-Type을 application/json으로 지정해야 함
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
// 자바 객체 -> JSON 데이터
HelloData helloData = new HelloData();
helloData.setUsername("kim");
helloData.setAge(20);
// 자바 객체에 담긴 내용을 JSON 데이터로 파싱 -> {"username":"kim", "age":20}
String result = objectMapper.writeValueAsString(helloData);
response.getWriter().write(result);
}
}
-> JSON 응답