2022. 6. 11. 18:41ㆍLanguage`/Spring
Servlet
자바 Servlet이란 자바를 사용하여 웹페이지를 동적으로 생성하는 서버측 프로그램이다
서블릿은 "Tomcat"같은 WAS를 직접 설치하고, 그 위에 서블릿 코드를 클래스 파일로 빌드해서 올린다음에 톰캣 서버를 실행해야 한다
이 과정은 매우 복잡하기 때문에 Spring Boot를 활용하면 내장 톰캣 서버가 존재하기 때문에 별도로 톰캣 서버를 설치할 필요없이 서블릿 코드를 실행할 수 있다
start.spring.io에서 스프링 프로젝트를 생성하면 다음과 같은 2개의 자바 파일이 생성이 된다
- Packaging : WAR
여기서 main메소드가 포함된 ServletTestApplication에 @ServletComponentScan을 붙여주면 서블릿이 자동으로 등록된다
@ServletComponentScan
@SpringBootApplication
public class ServletPracticeApplication {
public static void main(String[] args) {
SpringApplication.run(ServletPracticeApplication.class, args);
}
}
이제 기본적인 Servlet 코드 구조를 알아보자
@WebServlet(name = "helloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
}
}
일단 @WebServlet부터 살펴보자
1. name
name은 "서블릿의 이름"을 설정하는 속성이다
name을 지정하지 않으면 해당 클래스의 맨 앞글자만 소문자로 바꾼 이름이 등록된다
2. value & urlPatterns
value와 urlPatterns는 서블릿의 URL 목록을 설정하는 속성이다
@WebServlet(name = "helloServlet", urlPatterns = "/hello")
위와 같은 서블릿은 "/hello"로 Request가 들어올 경우 해당되는 메소드를 실행한다
@WebServlet(name = "helloServlet", urlPatterns = {"/hello", "/spring", "/jpa"})
urlPatterns와 value는 여러개의 URL에 대해서 한꺼번에 매핑시킬 수 있다
3. loadOnStartup
서블릿은 브라우저에서 "최초 요청"시에 init()를 실행한 후에 메모리에 로드되어서 기능을 수행한다
이 때 최초 요청에 대해서 실행시간이 길어질 수 있다는 단점이 존재한다
>> 이러한 단점을 보완하기 위해서 loadOnStartUp을 사용한다
loadOnStartUp은 톰캣 컨테이너가 실행되면서 "미리" 서블릿을 실행한다
loadOnStartup에 설정한 숫자가 0보다 크다면 톰캣 컨테이너가 실행됨과 동시에 해당 서블릿이 초기화된다
설정한 숫자는 우선순위와 동일하며 작은숫자부터 먼저 초기화된다
4. initParams
initParams은 서블릿 초기 파라미터를 미리 부여하는 속성이다
@WebServlet(name = "helloServlet", urlPatterns = "/hello", initParams = {
@WebInitParam(name = "name", value = "Faker"),
@WebInitParam(name = "age", value = "20")
})
public class HelloServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String name = getInitParameter("name");
String age = getInitParameter("age");
System.out.println("name = " + name);
System.out.println("age = " + age);
}
}
위와 같이 initParams는 내부적으로 @WebInitParams을 여러개 지정해서 해당 URL에 대해서 request가 들어올 때 기본적으로 설정한 파라미터를 가지게 한다
실제 localhost:8080/hello로 request를 보냈을 경우 console에는 다음 로그가 찍히게 된다
System.out.println("none = " + none); // none = null
initParams으로 지정하지 않은 파라미터를 가져오게 되면 null을 반환받게 된다
5. asyncSupported
서블릿 3.0에서부터 지원하는 "비동기 서블릿"기능이다
원래 서블릿은 1개의 Request에 대해서 1개의 thread가 일을 담당해서 response까지 해주었다
일반적으로는 잘 동작하지만, 서버에서 "연결을 유지한 채 지속적으로 통신하는 기능"을 구현하기에는 적합하지 않았다
- 왜냐하면 1개의 thread가 request/response에 대한 처리를 모두 하기 때문
따라서 "비동기 서블릿"을 통해서 Request 처리하는 Thread & Response 처리하는 Thread를 분리함으로써 응답을 비동기로 처리할 수 있게 되었다
@WebServlet(name = "helloServlet", urlPatterns = "/hello", asyncSupported = true)
서블릿 컨테이너 동작 방식
서블릿 컨테이너는 스프링 컨테이너처럼 서블릿 객체들을 관리해주는 컨테이너이다
HttpServletRequest
Servlet은 HTTP Request Message를 개발자가 사용하기 쉽도록 알아서 Parsing해서 HttpServletRequest객체에 담아준다
request에서 특별한 기능 중 하나는 "임시 저장소"기능이다
내부 Attribute라는 임시 저장소 박스에다가 데이터를 저장할 수 있고, 저장된 데이터를 조회할 수 있다
- 저장 : request.setAttribute("데이터 이름", "데이터 값")
- 조회 : request.getAttribute("데이터 이름")
HttpServletRequest를 통해서 URL상의 쿼리 파라미터 값도 얻을 수 있고 HTTP Request Message상의 헤더도 조회할 수 있다
HTTP 요청 데이터
보통 Client → Server로 데이터를 전달하는 방식은 총 3가지가 존재한다
1. GET : 쿼리 파라미터
GET방식은 URL뒤에다가 {key=value}형식으로 붙여서 데이터를 요청하는 방식이다
URL과 쿼리 파라미터간에 구분자는 "?"이고, 쿼리 파라미터끼리의 구분자는 "&"이다
GET으로 데이터를 전달할 경우 HttpServletRequest는 다음 메소드를 활용해서 쿼리 파라미터를 조회할 수 있다
request.getParameter("key 이름") → (String) |
단일 파라미터 조회 |
request.getParameterNames() → Enumeration<String> |
파라미터 key들을 모두 조회 |
request.getParameterMap() → Map<String, String[]) |
파라미터를 Map형식으로 조회 >> key : value가 1 : N일 경우 |
request.getParameterValues("key 이름") → String [] |
key이름에 해당하는 모든 value들을 조회 |
1:N일경우 단일 파라미터로 조회할 경우 쿼리 파라미터상에서 가장 앞쪽에 있는 value가 return된다
이렇게 key:value가 1:N일 경우 1) key이름을 통해서 모든 value를 조회하거나, 2) Map형식으로 조회하면 된다
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("text/plain");
response.setCharacterEncoding("utf-8");
// key:value = 1:N
Map<String, String[]> parameterMap = request.getParameterMap();
int index = 0;
for(String s : parameterMap.keySet()){
response.getWriter().write("key : " + s + "\n");
for(String value : parameterMap.get(s)){
response.getWriter().write((++index) + "번째 value : " + value + "\n");
}
}
}
2. POST : HTML Form (Message Body에 쿼리 파라미터 형식으로 담겨서 전달)
메시지 바디에 쿼리 파라미터 형식으로 Form Data를 요청하는 방식이다
POST방식으로 데이터를 요청할 때는 반드시 content-type을 다음과 같이 지정해야 한다
content-type: application/x-www-form-urlencoded
POST방식은 보통 Client → Server로 데이터를 보내고 해당 데이터를 저장하려는 용도로 사용한다
@WebServlet(name = "postTest", urlPatterns = "/request-param")
public class PostTest extends HttpServlet {
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String username = request.getParameter("username");
String age = request.getParameter("age");
response.setContentType("text/plain");
response.setCharacterEncoding("utf-8");
response.getWriter().write("username : " + username + "\nage : " + age);
}
}
POST방식으로 전달된 데이터도 GET방식과 동일하게 getParameter로 데이터를 읽을 수 있다
3. HTTP API (Message Body에 직접 데이터를 담아서 요청)
Message Body에 직접 데이터를 담아서 요청하는 경우는 주로 HTTP API(XML, TEXT, "JSON")에서 사용한다
대부분은 JSON형식을 사용하고 메소드는 {POST / PUT / PATCH}방식으로 보낸다
이 경우는 이전에 GET/POST와는 약간 다르게 데이터를 읽는다
왜냐하면 Body에 Data가 그대로 들어있기 때문에 "request.getInputStream()"으로 Data를 그대로 읽은 다음에 StreamUtils의 copyToString을 통해서 String으로 parsing해야 한다
@WebServlet(name = "bodyStringTest", urlPatterns = "/request-body-string")
public class BodyStringTest 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);
response.getWriter().write(messageBody);
}
}
위는 messageBody에 String Data를 담아서 보낸 결과이다
이제 Body에 JSON형식의 데이터를 담아서 보내고, 이 JSON형식의 데이터를 읽어보자
JSON형식으로 데이터를 전달할 경우 반드시 "content-type: application/json"로 설정해줘야 한다
근데 JSON의 경우 자바 입장에서는 "객체"로 Mapping을 해줘야 사용할 수 있을 것이다
따라서 JSON의 key에 해당하는 field를 새로운 자바 객체를 생성해서 넣어주자
Spring Boot에서는 기본적으로 {JSON - 객체}를 mapping해주는 "Jackson 라이브러리"를 제공해준다
우리는 Jackson 라이브러리의 "ObjectMapper"를 이용해서 {JSON - 객체}간의 Mapping을 해줄 것이다
@WebServlet(name = "bodyJsonTest", urlPatterns = "/request-body-json")
public class BodyJsonTest extends HttpServlet {
private final 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);
HelloData helloData = objectMapper.readValue(messageBody, HelloData.class); // {JSON - 객체} Mapping
response.getWriter().write(String.valueOf(helloData));
}
}
messageBody를 가져오는 부분까지는 String Data를 읽을때랑 동일하지만, 그 이후에 JSON형식의 데이터를 객체로 Parsing을 해야 하기 때문에 "ObjectMapper의 readValue메소드"를 통해서 Parsing을 하였다
HttpServletResponse
HttpServletRequest는 Client로부터 발생한 HTTP Request Message를 편리하게 읽을 수 있는 객체였다면, HttpServletResponse는 서버내부에서 어떠한 logic을 수행하고 나서, "HTTP Response Message"를 손쉽게 작성할 수 있는 객체이다
일단 response.setStatus() 통해서 HTTP Response Message상에서의 상태 코드를 지정할 수 있다
그리고 encoding정보와 cookie, redirect등도 지정할 수 있다
HTTP 응답 데이터
1. 단순 텍스트 / HTML
단순 텍스트는 content-type:text/plain으로 설정하고 HTML은 content-type:text/html로 설정해서 response해주면 된다
html의 경우 response.getWriter를 얻어서 직접 모든 html코드를 적어줘야 한다
>> 이것이 서블릿의 가장 큰 단점이다
2. JSON
JSON형식의 데이터를 response해주려면 이전에 json에 대한 request와 반대로 진행하면 된다
백엔드 입장에서는 "객체"를 objectMapper의 "writeValueAsString()"을 통해서 JSON으로 변환해주고 이 변환한 것을 그대로 response해주면 된다
그리고 당연히 content-type: application/json으로 설정해줘야 한다
@WebServlet(name = "responseJsonTest", urlPatterns = "/response-body-json")
public class ResponseJsonTest extends HttpServlet {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
HelloData helloData = new HelloData();
helloData.setUsername("faker");
helloData.setAge(40);
String result = objectMapper.writeValueAsString(helloData);
response.getWriter().write(result);
}
}