서블릿

서블릿은 실행 시킬 수 있는 프로그램이 아니라 클라이언트의 요청을 처리하고, 그 결과를 반환하는 기술을 말합니다.

저희가 사용하는 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 MethodContent가 없는 것이 표준 이라 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

picture 1

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 DataByte 코드로 읽어들일 수 있습니다

바이트 코드란(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도 그냥 텍스트다 객체를 텍스트로 변환해줘야한다.