서블릿

김민우·2022년 7월 22일
0

MVC

목록 보기
2/16
post-thumbnail
post-custom-banner

스프링 부트 환경에서 서블릿을 등록하고 사용해보자.
참고로 스프링 부트에서 서블릿을 등록하는 이유는 스프링 부트는 톰켓 서버를 내장하고 있기 때문에, 별도의 톰켓 서버 설치 없이 편리하게 서블릿 코드를 실행하기 위함이다.
(톰켓 서버 관련 설정이 엄청 오래걸린다...)

@ServletComponentScan

스프링 부트에서 서블릿을 직접 등록해서 사용할 수 있게 해주는 어노테이션이다.
해당 어노테이션이 붙은 클래스의 패키지 포함 하위 패키지의 서블릿 컴포넌트(필터, 서블릿, 리스너)를 스캔해서 빈으로 등록해준다. (자기 자신도 포함)

필터: @WebFilter
서블릿: @WebServlet
리스너: @WebListener

사용 예시)


@ServletComponentScan //서블릿 자동 등록
@SpringBootApplication
public class ServletApplication {
	public static void main(String[] args) {
    	SpringApplication.run(ServletApplication.class, args);
	}
}

서블릿을 만들고 싶으면 다음 3가지를 하면 된다.

  1. @WebServlet 붙이기
  2. HttpServlet 인터페이스 상속받기
  3. HttpServlet의 추상 메서드 service 구현

@WebServlet

서블릿 어노테이션이다. 주요 속성은 name, urlPatterns이 있다.

  • name : 서블릿 이름
  • urlPatterns : URL 매핑

HttpServlet

GenericServlet을 상속받고 추가로 HTTP 프로토콜의 기능 (doGet, doPost, ...)을 제공하는 인터페이스이다.

HTTP 요청을 통해 매핑된 URL이 호출되면 서블릿 컨테이너는 service 메서드를 실행한다.

Service 메소드

서블릿이 요청에 응답하도록 서블릿 컨테이너에서 호출되는 HttpServlet 인터페이스의 추상 메소드이다

형태)

protected void service(HttpServletRequest request, 
HttpServletResponse response)
  • request : 요청 메시지에 대한 정보
  • response : 응답 메시지에 대한 정보

참고) HTTP 요청 메시지를 로그로 확인하기
application.properties 에서 다음 설정을 추가하자.
logging.level.org.apache.coyote.http11=debug
-> 그러나 이는 성능 저하가 발생할 수 있으므로 개발 단계에서만 적용하자.


내장 톰켓 서버 생성 과정

스프링 부트가 내장 톰켓 서버를 생성해주고 톰켓 서버가 서블릿 컨테이너를 생성하고 자동으로 서블릿을 등록한다.

요청이 들어오면 HTTP 요청 메시지를 기반으로 request 객체를 생성한다.
그 후, 서블릿 컨테이너의 서블릿의 service 메소드를 통해 response에 응답 정보를 담고 이 객체를 통해 HTTP 응답 메시지를 생성한다.

참고)
HTTP 응답에서 Content-Length는 웹 애플리케이션 서버가 자동으로 생성해준다.


HttpServiceRequest

HTTP 요청 메시지를 개발자가 직접 파싱해서 사용해도 되지만, 이는 매우 불편하다.
서블릿은 개발자가 HTTP 요청 메시지를 편리하게 사용할 수 있도록 개발자 대신에 HTTP 요청 메시지를 파싱한다. 그리고 그 결과를 HttpServletRequest 객체에 담아서 제공한다.

  • 시작 라인
    • HTTP 메서드
    • URL
    • 쿼리 스트링
    • 스키마, 프로토콜
  • 헤더
    • 헤더 조회
  • 바디
    • form 파라미터 형식 조회
    • message body 데이터 직접 조회

참고로 HttpServletRequset는 추가로 여러가지 부가 기능도 함께 제공한다.

임시 저장소 기능

  • 해당 HTTP 요청이 시작부터 끝날 때 까지 유지되는 임시 저장소 기능
    • 저장 : request.setAttribute(name, value)
    • 조회 : request.getAttribute(name)

세션 관리 기능

  • request.getSession(create : true)

중요)
HttpServletRequest, HttpServletResponse를 사용할 때 가장 중요한 점은 이 객체들이 HTTP 요청 메시지, HTTP 응답 메시지를 편리하게 사용하도록 도와주는 객체라는 점이다. 따라서 이 기능에 대해서 깊이있는 이해를 하려면 HTTP 스펙이 제공하는 요청, 응답 메시지 자체를 이해해야 한다.

HttpServletRequest가 제공하는 기본 기능들

1. 시작 라인 정보

 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-header
	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();
}

결과

--- REQUEST-LINE - start ---
  request.getMethod() = GET
  request.getProtocol() = HTTP/1.1
  request.getScheme() = http
  request.getRequestURL() = http://localhost:8080/request-header
  request.getRequestURI() = /request-header
  request.getQueryString() = username=hello
  request.isSecure() = false
  --- REQUEST-LINE - end ---
 

2. 헤더 정보

private void printHeaders(HttpServletRequest request) {
	System.out.println("--- Headers - start ---");

/*
	Enumeration<String> headerNames = request.getHeaderNames(); // 예전 방식, iterator 사용
	while (headerNames.hasMoreElements()) {
		String headerName = headerNames.nextElement();
		System.out.println(headerName + " : " + headerName);
	}
*/

	request.getHeaderNames().asIterator()
		.forEachRemaining(headerName -> System.out.println(headerName + " : " + headerName));

	System.out.println("--- Headers - end ---");
	System.out.println();
}
  • 참고로 주석 처리한 부분은 예전 방식이다.
  • 헤더에 대한 정보는 반복자(iterator)와 forEachRemaining 메서드를 사용하여 순차적으로 출력할 수 있다.

결과

--- Headers - start ---
  host: localhost:8080
  connection: keep-alive
  cache-control: max-age=0
  sec-ch-ua: "Chromium";v="88", "Google Chrome";v="88", ";Not A Brand";v="99"
  sec-ch-ua-mobile: ?0
  upgrade-insecure-requests: 1
  user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_0) AppleWebKit/537.36
  (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36
  accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/
  webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
  sec-fetch-site: none
  sec-fetch-mode: navigate
  sec-fetch-user: ?1
  sec-fetch-dest: document
  accept-encoding: gzip, deflate, br
  accept-language: ko,en-US;q=0.9,en;q=0.8,ko-KR;q=0.7
--- Headers - end ---

지금까지 HttpServletRequest를 통해서 HTTP 메시지의 start-line, header 정보 조회 방법을 이해했다. 이제 본격적으로 HTTP 요청 데이터를 어떻게 조회하는지 알아보자.


HTTP 요청 데이터

다음 3가지 방법을 사용한다.

  • GET - 쿼리 파라미터
  • POST - HTML Form
  • HTTP message body에 데이터를 직접 담아서 요청

서버로 데이터 전달하는 방법은 나와있는 3가지에서 벗어나지 않는다.

GET - 쿼리 파라미터

다음 데이터를 클라이언트에서 서버로 전송해보자. ( key = value)

  • username = hello
  • age = 20

메시지 바디 없이 URL의 쿼리 파라미터를 사용해서 데이터를 전달하자.
이는 검색, 필터, 페이징등에서 많이 사용하는 방식이다.

특징

  • GET 메서드이므로 content-type은 null이다.
  • GET 메서드이므로 메시지 바디 또한 없다.

쿼리 파라미터는 URL에 다음과 같이 ?을 시작으로 보내고 추가 파라미터는 &으로 구분한다.
http://localhost:8080/request-param?username=hello&age=20

서버에는 HttpServletRequest가 제공하는 다음 메서드를 통해 쿼리 파라미터를 편리하게 조회할 수 있다.

//단일 파라미터 조회 
String username = request.getParameter("username"); 

//파라미터 이름들 모두 조회
Enumeration<String> parameterNames = request.getParameterNames(); 

//파라미터를 Map 으로 조회
Map<String, String[]> parameterMap = request.getParameterMap();

//복수 파라미터 조회
String[] usernames = request.getParameterValues("username"); 

참고) 복수 파라미터에서 단일 파라미터 조회
username=hello&username=kim과 같이 파라미터 이름은 하나인데, 값이 중복인 상황이면 어떻게 될까?
일단, request.getParameter() 는 하나의 파라미터 이름에 대해서 단 하나의 값만 있을 때 사용해야 한다.
지금처럼 중복일 때는 request.getParameterValues() 를 사용해야 한다. 참고로 이렇게 중복일 때 request.getParameter() 를 사용하면 request.getParameterValues() 의 첫 번째 값을 반환한다.

POST - HTML Form

HTML의 Form을 사용해서 클라이언트에서 서버로 데이터를 전송해보자.
이는 주로 회원 가입, 상품 주문 등에서 사용된다.

특징

  • content-type : application/x-www-form-urlencoded
  • 메시지 바디에 쿼리 파라미터 형식으로 데이터를 전달한다.
    username=hello&age=20

참고)
GET 메서드애서 사용하는 쿼리 파라미터와 POST 메서드에서 HTML Form을 사용할 때 메시지 바디에 입력하는 쿼리 파라미터는 둘다 같은 형태이다. 그래서 이 둘을 요청 파라미터라 부른다.

HTTP Form을 이용하여 요청할 때 사용할 수 있는 메서드는 GET과 POST이지만 메시지 바디를 포함해야 하므로 사실상 POST만 사용한다고 생각하자.
(PUT, PATCH, DELETE는 사용 불가능하다.)

application/x-www-form-urlencoded 형식은 앞서 GET에서 살펴본 쿼리 파라미터 형식과 같다. 따라서 쿼리 파라미터 조회 메서드를 그대로 사용하면 된다.

클라이언트(웹 브라우저) 입장에서는 두 방식에 차이가 있지만, 서버 입장에서는 둘의 형식이 동일하므로, request.getParameter() 로 편리하게 구분없이 조회할 수 있다.

정리하면 request.getParameter() 는 GET URL 쿼리 파라미터 형식도 지원하고, POST HTML Form 형식도 둘 다 지원한다.

참고)
content-type은 HTTP 메시지 바디에 데이터를 포함하는 경우에 작성을 해줘야한다. GET 메서드는 메시지 바디가 없으므로 null이지만 POST는 메시지 바디가 있으므로 작성을 해야한다.

API 메시지 바디

말 그대로 HTTP 메시지 바디에 데이터를 직접 담아서 요청한다. 주로 HTTP API에서 사용하며 데이터 형식은 JSON을 주로 사용하지만 모든 데이터 형식이 올 수 있다.
(XML, TEXT, 이미지 등...)
메서드는 POST, PUT, PATCH가 사용된다.

문자 전송

단순한 텍스트 메시지를 HTTP 메시지에 담아서 전송하고 서버에서 읽어보자.
HTTP 메시지 바디의 데이터는 다음과 같이 InputStream을 사용하면 직접 읽을 수 있다.

ServletInputStream inputStream = request.getInputStream();
String messageBody = StreamUtils.copyToString(inputStream,
						StandardCharsets.UTF_8);

참고)
InputStream은 byte 코드를 리턴한다. 이를 우리가 읽을 수 있는 문자(String)으로 보려면 문자표를 지정해 주어야 한다.

.
.
.
.
.

JSON 형식 전송

다음으로 HTTP API에서 주로 사용하는 JSON 형식으로 데이터를 전달해보자.

JSON 형식으로 파싱하기 위해 객체를 하나 생성하자.

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class HelloData {

    private String username;
    private int age;
}

JSON 결과를 파싱해서 사용할 수 있는 자바 객체로 변환해주는 JSON 변환 라이브러리인 ObjectMapper 객체를 생성하자.

private ObjectMapper objectMapper = new ObjectMapper();

그 후, 다음과 같이 InputStream을 사용하여 데이터를 읽자.

ServletInputStream inputStream = request.getInputStream();
String messageBody = StreamUtils.copyToString(inputStream, 
								StandardCharsets.UTF_8);

...


HelloData helloData = objectMapper.readValue(messageBody,
							HelloData.class);

그 후, HelloData의 Getter와 Setter을 이용해 데이터를 읽을 수 있다.

System.out.println("helloData.username = " + helloData.getUsername());
System.out.println("helloData.age = " + helloData.getAge());

출력 결과)

data.username=hello
data.age=20

참고)
JSON 결과를 파싱해서 사용할 수 있는 자바 객체로 변환하려면 Jackson, Gson 같은 JSON 변환 라이브러리를 추가해서 사용해야 한다. 스프링 부트로 Spring MVC를 선택하면 기본으로 Jackson 라이브러리( ObjectMapper )를 함께 제공한다.

참고)
HTML form 데이터도 메시지 바디를 통해 전송되므로 직접 읽을 수 있다. 하지만 편리한 파리미터 조회 기능( request.getParameter(...) )을 이미 제공하기 때문에 파라미터 조회 기능을 사용하면 된다.


HttpServletResponse

HttpServletResponse의 역할은 크게 2가지 이다.

  • HTTP 응답 메시지 생성
    • HTTP 응답 코드 지정
    • 헤더 생성
    • 바디 생성
  • 편의 기능 제공
    • Content-type, 쿠키, Redirect

편의 기능

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");
}
  • Redirect를 하기 위해선 상태 코드를 300대로 바꿔야 한다.
  • sendRedirect : 상태코드를 302로 바꾸고 Redirect

HTTP 응답 데이터

HTTP 응답 메시지는 크게 다음 3가지 형태로 내용을 담아서 전달한다.

  • 단순 텍스트 응답
    • 앞서 살펴본 writer.println("ok")
  • HTML 응답
  • HTTP API
    • 메시지 바디 JSON 응답

하나씩 알아보자.

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>");
    }
}
  • HTTP 응답으로 HTML을 리턴할 때는 content-type을 text/html로 지정해야 한다.

API JSON

@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);
    }
}
  • 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()으로 출력하면 그런 문제가 없다.

post-custom-banner

0개의 댓글