서블릿
서블릿은 실행 시킬 수 있는 프로그램이 아니라 클라이언트의 요청을 처리하고, 그 결과를 반환하는 기술을 말합니다.
저희가 사용하는 JAVA는 OOP를 위한 언어로, 어떠한 기술을 구현할때 클래스 를 사용하여 구현합니다.
즉, 서블릿이라는 말은 웹 요청을 처리하는 로직이 구현된 단순 클래스 입니다. 별도의 서비스 같은게 아니죠.
더 자세한 내용은 2023-02-22-서블릿(Servlet)이란 을 참고해주시기 랍니다.
서블릿 자동 등록을 위한 @ServletComponentScan
패키지 내에서(default : 어노테이션이 달려있는 패키지를 포함한 하위 모든 패키지) 서블릿 클래스를 찾아서 서블릿 객체를 생성하여 서블릿 컨테이너에 등록합니다.
중요한 점은 내장 웹 서버일 경우에만 스캔합니다
@ServletComponentScan // 서블릿 자동 등록
@SpringBootApplication
public class ServletApplication {
public static void main(String[] args) {
SpringApplication.run(ServletApplication.class, args);
}
}
스캔 대상에 대한 기준
그럼 어떤 기준으로 스캔 대상을 선택하여 서블릿 컨테이너에 등록하는 걸까요?
해당 어노테이션 구현부를 들어가보면 주석으로 아래와 같은 내용이 적혀있습니다.
/**
* Enables scanning for Servlet components ({@link WebFilter filters}, {@link WebServlet
* servlets}, and {@link WebListener listeners}). Scanning is only performed when using an
* embedded web server.
* <p>
* Typically, one of {@code value}, {@code basePackages}, or {@code basePackageClasses}
* should be specified to control the packages to be scanned for components. In their
* absence, scanning will be performed from the package of the class with the annotation.
*
* @author Andy Wilkinson
* @since 1.3.0
* @see WebServlet
* @see WebFilter
* @see WebListener
*/
대략적으로 요약하자면,
- @WebFilter, @WebServlet, @WebListener 어노테이션이 붙어있는 클래스를 스캔함
- 내장 웹 서버일 경우에만 동작함
- 탐색 범위를 패키지로 지정할 수 있음
서블릿을 등록해보자!
서블릿 컨테이너에 등록하기 위한 @WebServlet()
위에서 나온 @WebServlet 라는 녀석을 통하여 Servlet 객체 를 등록할 수 있습니다. @WebServlet 어노테이션은 name, urlPatterns 를 파라미터로 받을 수 있으며,
name
은 서블릿 컨테이너에 등록되는 서블릿 객체의 고유값 이며, 서블릿 컨테이너는 이 name 값을 기준으로 서블릿들을 탐색합니다.
urlPatterns
은 클라이언트의 요청 URL 을 처리할 서블릿을 등록할 때 사용합니다.
아래는 사용에 대한 예시입니다.
@WebServlet(name = "requestParamServlet", urlPatterns = "/request-param")
@WebServlet() 사용을 위한 필수 조건 HttpServlet 상속
아무 클래스나 @WebServlet() 어노테이션을 붙이면 다 서블릿으로 사용할 수 있는 것은 아닙니다.
해당 어노테이션 사용에는 전제 조건이 필요한데 HttpServlet 라는 클래스를 상속 받은 클래스만 해당 어노테이션을 사용할 수 있습니다.
친절하게 @WebServlet() 클래스 파일을 보면 이를 알려주는 주석이 달려있습니다.
// ~~~
* The class on which this annotation is declared MUST extend
* {@link jakarta.servlet.http.HttpServlet}. <br>
* <br>
// ~~~
응답을 생성하는 로직 만들기
위에 @WebServlet() 어노테이션을 사용하기 위해서는 HttpServlet 라는 클래스를 상속 받는 것이 필수 라고 했었죠, 바로 이 HttpServlet 가 응답 을 할 수 있는 기능들을 포함하고 있습니다.
그 중에 protected void service(HttpServletRequest req, HttpServletResponse resp)
라는 함수가 존재하는데
서블릿 컨테이너는 클라이언트의 요청이 들어왔을때 URL 을 파싱하여 일치하는 서블릿 객체를 찾은 뒤 HttpServletRequest, HttpServletResponse 두 객체를 생성 후 service() 메소드를 호출하게 됩니다.
그렇다면 개발자가 원하는 응답을 만들어내기 위해서는 이 service() 메소드를 Override 하면됩니다.
요청 분석을 위한 HttpServletRequest
우리가 응답을하기 위해서는 사용자로부터의 상세 요구사항을 확인해야죠, 이것을 도와주는 클래스가 HttpServletRequest 클래스입니다.
서블릿 컨테이너 가 만들어서 넘겨준 이 클래스 객체를 사용하면 HTTP 표준에 의거하는 요청에 관련된 모든 내용을 꺼내볼 수 있습니다. (헤더, 바디 등등)
응답을 위한 HttpServletResponse
자 이제 정말로 사용자에게 응답을 전달해야합니다.
그냥 return 키워드를 사용하기에는 service 메소는 void를 반환하는 메소드입니다.
그럼 어떻게 응답은 던지냐?
이때 HttpServletResponse 객체를 사용하면됩니다.
이 객체 역시 서블릿 컨테이너 가 만들어서 넘겨준 객체이며 클라이언트에게 응답하기 위한 기능들을 포함하고 있습니다.
응답 던지기 예시
@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);
}
이런식으로 응답할 수 있습니다.
Request에서 얻을 수 있는 정보들
강의에서 나온 몇가지만 짚어봅시다.
Locale 정보를 얻는 getLocales()
Request Headers 에 포함된 Accept-Language 정보를 가져옵니다. 이때 locale 정보는 다수일 수 있고, 선호하는 locale 정보가 순차적으로 정렬되어있으며 가장 앞단에 존재하는 locale이 가장 선호하는 locale 정보입니다.
이 가장 선호하는 locale 정보를 바로 가져오고 싶을 경우 getLocate() 메소드를 사용하면 됩니다.
Content 정보를 얻는 getContentType(), getContentLength(), getCharacterEncoding()
주의! GET Method 는 Content가 없는 것이 표준 이라 type이 null이고 length가 -1이고 그렇다.
HTTP 요청 데이터 - 개요
HTTP 요청 메세지를 통해 클라이언트에서 서버로 데이터를 전달하는 방법 알아보자
GET - 쿼리 파라미터
- /url ?username=hello&age=20
- 메세지 바디 없이, URL의 쿼리 파라미터에 데이터를 포함해서 전달
- 예) 검색, 필터, 페이징등에서 많이 사용하는 방식
POST - HTML Form
- content-type: application/x-www-form-urlencoded
- 메시지 바디에 마치 쿼리 파라미터 형식으로 전달 username=hello&age=20
- 예) 회원 가입, 상품 주문, HTML Form 사용
HTTP message body
- HTTP API에서 주로 사용,JSON,XML,TEXT
- 데이터 형식은 주로 JSON사용
- 장점! 모든 HTTP 메소드 사용 가능!
HTTP 요청 데이터 - GET 쿼리 파라미터
쿼리 파라미터
요청 URL 에 입력하며 ?
로 부터 시작하고, 파라미터 추가 시 &
로 구분됩니다.
- http://localhost:8080/request-param ?username=hello&age=20
이 GET + 쿼리파라미터 요청을 sevlet 에서는 아래와 같이 처리할 수 있습니다.
@WebServlet(name = "requestParamServlet", urlPatterns = "/request-param")
public class ReuqestParamServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("[전체 파라미터 조회] - start");
req.getParameterNames().asIterator().forEachRemaining(param -> System.out.println(param));
System.out.println("[전체 파라미터 조회] - end");
System.out.println();
System.out.println("[단일 파라미터 조회]");
String username = req.getParameter("username");
String age = req.getParameter("age");
System.out.println("username = " + username);
System.out.println("age = " + age);
System.out.println();
}
}
중복 쿼리 파라미터
- http://localhost:8080/request-param ?username=hello&username=hello2&age=20
위와 같이 Key 값이 동일한 파라미터를 보낼 수 있습니다. 이럴때는 아래와 같은 방법으로 파라미터를 구분할 수 있습니다.
@WebServlet(name = "requestParamServlet", urlPatterns = "/request-param")
public class ReuqestParamServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("[이름이 같은 복수 파라미터 조회]");
String[] usernames = req.getParameterValues("username");
for (String name : usernames) {
System.out.println("username = " + name);
}
}
}
중복 상황에서 getParameter() 는 첫번째로 발견된 녀석 하나만 가져옵니다!
HTTP 요청 데이터 - POST HTML Form
이번에는 HTML의 Form을 사용해서 클라이언트에서 서버로 데이터를 전송해보자
특징
- content-type : application/x-www-form-urlencoded 이 컨텐츠 타입은 POST + Form 으로 Request를 요청하면 알아서 웹 브라우저가 만들어준다!
- 메시지 바디에 쿼리 파리미터 형식으로 데이터를 전달한다. username=hello&age=20
Request Paramter 는 GET 쿼리 파라미터와 POST Form 두 방식 모두 지원한다!
예를들어 아래 코드에 POST로 username=hello&age=20
를 요청한다 할 경우.
기존 쿼리 파라미터를 읽던 getParameter(“username”); 함수가 그대로 동작합니다.
@WebServlet(name = "requestParamServlet", urlPatterns = "/request-param")
public class ReuqestParamServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("[전체 파라미터 조회] - start");
req.getParameterNames().asIterator().forEachRemaining(param -> System.out.println(param));
System.out.println("[전체 파라미터 조회] - end");
System.out.println();
System.out.println("[단일 파라미터 조회]");
String username = req.getParameter("username");
String age = req.getParameter("age");
System.out.println("username = " + username);
System.out.println("age = " + age);
System.out.println();
}
}
그래서 함수 이름이 중의적인 getParamter입니다 쿼리 파라미터만 조회할 수 있었다면 getQueryParamter 였겠죠
HTTP 요청 데이터 - API 메시지 바디 - 단순 텍스트
HTTP message body 에 데이터를 직접 담아서 요청하는 방식
- HTTP API에서 주로 사용, JSON, XML, TEXT
- 데이터 형식은 주로 JSON 사용 (현시대 표준!)
- POST, PUT, PATCH
Body를 Byte단위로 읽어주는 InputStream
@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();
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
System.out.println("messageBody = " + messageBody);
response.getWriter().write("ok");
}
}
위와 같은 API 코드가 존재한다고 할 때 “hello” 라는 단순 텍스트를 HTTP Body에 실어서 Request를 보내면
request.getInputStream()
을 통해서 ServletInputStream 을 얻을 수 있습니다.
이 ServletInputStream 을 통해서 클라이언트가 요청한 HTTP의 Body를 “hello” 라는 텍스트의 Binary Data를 Byte 코드로 읽어들일 수 있습니다
바이트 코드란(Byte code)?
바이트 코드란 0과 1로 이뤄진 Binary Data를 바이트 단위로 분리한 것 입니다.
hello라는 텍스트를 16진수로 2Byte씩 읽으면 48 65 6C 6C 6F
로 읽을 수 있습니다.
이때 바이트 코드 는 단순 Binary Data를 몇 바이트씩 분리해서 보냐, 이 차이고 분리되는 방식은 동일하기에 1Byte씩 분리하냐 2Byte 씩 분리하냐 4 Byte 씩 분리하냐 내용에 차이는 없습니다.
하지만 이 분리된 바이트들을 어떻게 읽느냐 는 다른 범주입니다.
48 65 6C 6C 6F
라는 값을 A라는 집단은 “hello” 로 뜻으로 이해하고 쓰이는 반면,
B라는 집단은 “Hell” 로 이해할 수 있습니다.
읽는 방식에 따라 완전히 다른 의미로 변해버리죠.
꼭 Encoding을 해야하는 이유
이것이 바로 Bytecode에 대하여 Encoding, Decoding방식을 꼭 지정해줘야하는 이유입니다.
동일한 값은 읽는 방식에 따라 완전 다른의미로 해석될 수 있기 때문입니다.
HTTP 요청 데이터 - API 메시지 바디 - JSON
이번에는 HTTP API에서 주로 사용하는 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 형식 파싱 추가
보통은 JSON 형식을 그대로 쓰지 않고 객체로 변환해서 씁니다.
@WebServlet(name = "requestBodyJsonServlet", urlPatterns = "/request-body-json")
public class RequestBodyJsonServlet extends HttpServlet {
@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);
}
}
위 코드에 {"username": "hello", "age": 20}
라는 데이터를 HTTP BODY 에 넣어서 POST로 요청하면 정상적으로 로직이 동작합니다.
왜냐?! HTTP BODY에 JSON 넣는건 사실 그냥 TEXT를 넣는 것과 동일하기 때문입니다.
이 단순 문자열인 “JSON” 데이터를 객체로 만들기 위해서는 맵퍼가 필요합니다.
잠깐! HTML Form 도 알고보면 그냥 Body에 값 보내는거다
jackson의 ObjectMapper
Spring boot 에서 기본으로 지원하는 jackson라이브러리의 ObjectMapper입니다.
HelloData helloData = ObjectMapper.readValue(messageBody, HelloData.class);
위와 같은 방식으로 body에 담긴 json 값을 HelloData 객체로 만들 수 있습니다.
맵퍼 종류는 상당히 많고 맵퍼간의 성능차이도 좀 나는 편 입니다. 여기서는 ObjectMapper만을 사용했으나 나중에는 성능에 따라 여러 맵퍼를 사용하게 될 수 있습니다.
hello.servlet.basic.HelloData
HttpServletResponse - 기본 사용법
HttpServletResponse 역할
HTTP 응답 메시지 생성
- HTTP 응답코드 지정
- 헤더 생성
- 바디 생성
편의 기능 제공
- Content-Type : 요청 메세지의 형식 표시 (json이냐, text냐, form 이냐 등등)
- 쿠키 : HTTP 쿠키(웹 쿠키, 브라우저 쿠키)는 서버가 사용자의 웹 브라우저에 전송하는 작은 데이터 조각입니다. 브라우저는 그 데이터 조각들을 저장해 놓았다가, 동일한 서버에 재 요청 시 저장된 데이터를 함께 전송합니다. 쿠키는 두 요청이 동일한 브라우저에서 들어왔는지 아닌지를 판단할 때 주로 사용합니다. 이를 이용하면 사용자의 로그인 상태를 유지할 수 있습니다. 상태가 없는(stateless) HTTP 프로토콜에서 상태 정보를 기억시켜주기 때문입니다.
- Redirect : 서버에 어떤 URL을 요청했을 때, 서버가 리다이렉트를 지시하는 특정 HTTP 응답을 통해 웹 브라우저로 하여금 지정된 다른 URL로 재요청하라고 지시하는 것 특징으로는 클라이언트가 초기 요청한 URL 자체가 변경됨.
HttpServletResponse 를 이용하여 직접 헤더를 넘겨보자
@WebServlet(name = "requestBodyJsonServlet", urlPatterns = "/request-body-json")
public class RequestBodyJsonServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// [status-line]
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);
PrintWriter writer = response.getWriter();
writer.println("ok");
}
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");
}
}
위 코드를 사용하면 클라이언트에게 Status, Content-Type, Cache-Control, Pragma, my-header 정보를 넘길 수 있습니다.
-
Status
HTTP 응답의 상태 코드를 나타내는 헤더입니다. 예를 들어 200은 성공적인 요청을 의미하고 404는 찾을 수 없는 리소스를 의미합니다. -
Content-Type
HTTP 메시지의 본문이 어떤 미디어 타입인지를 나타내는 헤더입니다. 예를 들어 text/html은 HTML 문서를 의미하고 image/jpeg는 JPEG 이미지를 의미합니다. -
Cache-Control
HTTP 캐싱 메커니즘을 위한 디렉티브를 정하는 헤더입니다. 예를 들어 max-age=604800은 캐시된 응답이 604800초 동안 신선하다고 간주되는 것을 의미하고 must-revalidate는 캐시된 응답이 원래 서버와 재검증되거나 504(Gateway Timeout) 응답이 생성되도록 강제하는 것을 의미합니다. -
Pragma
HTTP/1.0 헤더로서 주로 Pragma: no-cache가 사용됩니다. 이는 캐시가 검증을 위해 원래 서버에 요청을 보내도록 강제하는 것을 의미합니다. 그러나 Pragma는 HTTP/1.1에서 명세되지 않았으므로 Cache-Control 헤더에 비해 신뢰성이 낮습니다. -
my-header 직접 만든 커스텀해더입니다.
HTML 전송하기
@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>");
}
}
주의점! : Content-Type 에 꼭 html을 명시해야한다!
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.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);
}
}
주의점! : Content-Type 에 application/json 명시해줘야한다 주의점! : JSON도 그냥 텍스트다 객체를 텍스트로 변환해줘야한다.