이 글은 스프링 [스프링 MVC 1편]을 듣고 정리한 내용입니다
서블릿 이란?
- Dynamic Web Page를 만들때 사용되는 자바 기반의 웹 애플리케이션 프로그래밍 기술이다.
- 웹을 만들때 다양한 요청(request)와 응답(response)가 있기 마련이고, 이 요청과 응답에는 규칙이 존재한다. 이러한 요청과 응답을 일일이 처리하려면 힘들고, 서블릿은 이러한 웹 요청과 응답의 흐름을 메서드 호출만으로 체계적으로 다룰수 있게 해주는 기술이다.
서블릿 동작 과정
- 서블릿은 자바 클래스로 웹 애플리케이션을 작성한 뒤, 이후 웹 서버 안에 있는 웹 컨테이너에서 이것을 실행하고, 웹 컨테이너는 서블릿 인스턴스를 생성 후 서버에서 실행되다가 웹 브라우저에서 서버에 요청(request)를 하면 요청에 맞는 동작을 수행하고 웹 브라우저에 HTTP형식으로 응답(response)한다.
서블릿 컨테이너란?
- 서블릿을 담고 관리해주는 컨테이너이다.
- 서블릿 컨테이너는 구현되어있는 servlet 클래스의 규칙에 맞게 서블릿을 관리해주며 클라이언트에서 요청을 하면 컨테이너는 HttpServletRequest, HttpServletResponse 두 객체를 생성하며 post,get 여부에 따라 동적인 페이지를 생성하여 응답을 보낸다.
HttpServletRequest란?
- http 프로토콜의 request정보를 서블릿에게 전달하기 위한 목적으로 사용한다.
- 헤더정보, 파라미터, 쿠키, URI, URL 등의 정보를 읽어들이는 메서드와 Body Stream을 읽어 들이는 메서드를 가지고 있다.
HttpServletResponse란?
- WAS는 어떤 클라이언트가 요청을 보냈는지 알고 있고, 해당 클라이언트에게 응답을 보내기 위한 HttpServletResponse 객체를 생성하여 서블릿에게 전달하고 이 객체를 활용하여 content type, 응답 코드, 메세지 등을 전송한다.
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 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;
// servlet 어노테이션
@WebServlet(name = "helloServlet", urlPatterns = "/hello") // name: 서블릿 이름, urlPatterns: URL 매핑 (이름은 아무거나 해도됨)
public class HelloServlet extends HttpServlet {//원래 서블릿은 HttpServlet 클래스를 상속받아야 함.
@Override // HTTP 요청을 통해 URL이 호출되면 서블릿 컨테이너는 다음 메서드(service)를 실행한다.
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//control+O 에서 protected형 service 선택해야함
System.out.println("HelloServlet.service");
System.out.println("request = " + request);
System.out.println("response = " + response);
//아래와 같이 servlet이 개발자가 요구하는 값을 알아서 파싱해줌.
String username = request.getParameter("username");//username 이라는 쿼리파라미터에 해당하는 값을 가져옴.
System.out.println("username = " + username);
response.setContentType("text/plain"); // http 헤더 정보 - content type
response.setCharacterEncoding("utf-8");// 문자 인코딩은 요즘에는 utf-8을 보통 씀
response.getWriter().write("hello " + username); //write란? -> http 메시지 바디에 데이터가 들어감
}
}
logging.level.org.apache.coyote.http11=debug
*참고
- 운영 서버에 모든 요청 정보를 다남김녀 성능저하가 발생할 수 있으므로 개발할때, 참고용으로만 적용하자!
Servlet Container
기능을 가지고 있다.웹 브라우저가 HTTP 메세지를 다음과 같이 만들어서 서버쪽에 던져준다.
그럼 서버는 reqeust,responser객체를 만들어서 helloServlet을 호출해준다. 거기에서 sevice메서드를 호출하면서 request,response를 넘겨준다.
필요한 작업을 한 후, helloServlet이 종료되고 나가면서 WAS서버가 response를 가지고 HTTP 응답 메세지를 반환한다. -> 그럼 웹 브라우저에서 hello world메세지를 볼 수 있게 된다.
welcome 페이지 추가(index.html)
학습할 내용 안내 페이지 추가(basic.html)
HttpServletRequest
객체에 담아서 제공한다. POST /save HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded
username=kim&age=20
HttpServletRequeset
객체는 추가로 다음 여러가지 부가기능도 제공한다.request.setAttribute(name,value)
request.getAttribute(name)
중요
- HttpServletRequest, HttpServletResponse 객체들은 HTTP 요청,응답 메세지를 편리하게 사용하도록 도와주는 개체이다
- HTTP 스펙이 제공하는 요청, 응답 메시지를 잘 이해해야한다.
package hello.servlet.basic.request;
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.util.Enumeration;
@WebServlet(name= "requestHeaderServlet", urlPatterns = "/request-header")
public class RequestHeaderServlet extends HttpServlet {
@Override //protected인 service를 만들어야 한다.
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
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-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();
}
// 기타 정보
// 기타정보는 HTTP 메시지의 정보는 아니다
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();
}
}
/url?username=hello&age=24
content-type: application/x-www-form-urlencoded
username=hello&age=24
String username = request.getParameter("username"); //단일 파라미터 조회
Enumeration<String> parameterNames = request.getParameterNames(); //파라미터 이름들 모두 조회
Map<String, String[]> parameterMap = request.getParameterMap(); //파라미터를 Map 으로 조회
String[] usernames = request.getParameterValues("username"); //복수 파라미터 조회
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=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("name = " + name);
}
System.out.println();
response.getWriter().write("ok");
}
}
request.getParamterValues()
사용해야 한다.request.getParameter()
사용하면 첫번째 값을 반환한다.application/x-www-form-urlencoded
username=hello&age=20
requset.getParameter()
는 GET URL 쿼리 파라미터 형식도 지원하고, POST HTML Form형식도 지원한다.*참고
- content-type: HTTP 메시지 바디의 형식을 지정한다
- GET URL 쿼리 파라미터 방식에서는 메시지 바디를 사용 안하므로, content-type이 없다
- POST HTML Form 방식에서는 메시지 바디에 데이터를 포함해서 보내므로 데이터가 어떤 형식인지 content-type를 지정해야 한다.
(즉 포스트맨에서 body의x-www-form-urlencoded
선택해야함)
- 폼으로 데이터를 전송하는 형식을application/x-www-form-urlencoded
라 한다.
package hello.servlet.basic.request;
@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(); //이렇게하면, message body의 내용을 바이트코드로 바로 얻을 수 있다.
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);// 바이트코드를 문자로 변환
//inputStream은 byte코드를 반환한다.
//byte코드를 우리가 읽을 수 있는 문자로 보려면 문자표(charset)을 지정해주어야 한다.
System.out.println("messageBody = " + messageBody);
response.getWriter().write("ok");
}
}
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;
@WebServlet(name="requestBodyJsonServlet", urlPatterns = "/request-body-json")
public class RequestBodyJsonServlet extends HttpServlet {
//JSON 결과를 파싱해서 사용할 수 있는 자바 객체로 변환하려면 JSON 변환 라이브러리 추가해서 사용해야 한다.
//스프링에서 기본으로 Jackson 라이브러리(objectMapper) 제공
private ObjectMapper objectMapper = new ObjectMapper();
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
ServletInputStream inputStream = req.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());
resp.getWriter().write("okay");
}
}
HTTP 응답 메시지 생성
HttpServletResponse 사용법
package hello.servlet.basic.response;
@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이긴 하지만, 200 보다는 이렇게 적는것이 나음
//[response-headers] //이런식으로 지정해줘도 되고, 아래 편의메서드 내용처럼 지정해줘도 된다.
response.setHeader("Content-Type", "text/plain;charset=utf-8"); //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);
PrintWriter writer = response.getWriter();
writer.println("오케이입니다");
}
//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);
}
//리다이렉트 편의 메서드
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"); //이렇게 편하게 사용가능 (위 두줄과 같음)
}
}
writer.println("ok")
) - HTTP API - Message Body JSON 응답
text/html
로 지정한다.package hello.servlet.basic.response;
@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>");
}
}
application/json
으로 지정하면 된다.objectMapper.writeValueAsString()
을 통해 객체를 JSON 문자로 변경할 수 있다.package hello.servlet.basic.response;
@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);
}
}
*참고
application/json
은 스펙상 utf-8형식을 사용한다. 그래서 스펙에서charset=utf-8
과 같은 추가 파라미터를 지원하지 않는다.- 그러므로 content-type을 지정할때,
application/json
이라고만 하면 된다.application/json;charset=utf-8
이라고 전달하는건 의미없는 파라미터를 추가하는 것이 된다.