15 min read

<Spring> 서블릿 사용해보기

해당 포스팅은 스프링 배우기 전 알아두면 좋은 개념과 연속적으로 진행되는 포스팅입니다.

프로젝트 설정하기

우선 https://start.spring.io/으로 접속해서 아래와 같이 스프링부트 프로젝트를 하나 만듭니다.구지 서블릿만을 사용하는데도 스프링부트를 쓰는 이유는 뭘까요?

만약 스프링부트의 도움 없이 서블릿을 사용하려면 WAS를 직접 설치하고 그 위에 서블릿을 달고 톰캣 서버를 실행시켜야합니다.하지만 스프링부트는 간단한 어노테이션하나로 서블릿을 사용할수 있게 해줍니다.(스프링 부트가 톰캣 서버를 내장하고 있기 때문에 가능합니다.)

Hello Servlet

프로젝트 빌드에 성공하면 main/basic이라는 패키지에 HelloServlet이라는 클래스를 생성한 후 아래와 같이 어노테이션,상속,오버라이딩을 진행해줍니다.

  • 어노테이션 : @WebServlet(name = "helloServlet", urlPatterns = "/hello")
  • 상속 : HttpServlet
  • override : protected void service(HttpServletRequest request, HttpServletResponse response)

확실하게 위의 3가지 사항을 적용시킨후,이제부터 앞선 포스팅에서 배운 것처럼 servie 메서드 내에서 저희가 원하는 동작을 코딩해봅시다.우선 해당 메서드가 호출이 잘 되는고 사용자 요청에 따라 response와 requset 객체를 제대로 생성하였는지 확인해봅시다.

아래의 내용을 메서드 안에 넣어줍니다.

		System.out.println("HelloServlet.service");
        System.out.println("request = " + request);
        System.out.println("response = " + response);

이후 url에 localhost:8080/hello으로 접속해보시면 아래와 같이 로그가 남은 것을 확인해 볼 수 있습니다.

이제 사용자의 요청에 따라 화면에 데이터를 랜더링 시켜봅시다.아래 코드를 추가시켜 줍니다.이후 localhost:8080/hello?username=brido라고 요청을 보내면 아래와 같이 브라우져의 화면에 동적으로 사용자가 넘긴 데이터가 넘어오는것을 확인할 수 있습니다.

		String username = request.getParameter("username");
        System.out.println("username = " + username);

        response.setContentType("text/plain");
        response.setCharacterEncoding("utf-8");
        response.getWriter().write("hello" + username);

사실 방금 저희가 진행한 일련의 과정이 웹 프로그래밍의 가장 중요하고 큰 흐름을 보여주는 예제입니다.사용자가 일종의 데이터를 넣어주면,웹서버에서는 해당 데이터를 통해 사용자가 원하는 데이터를 뿌려주면 됩니다.단순히 코드를 따라쳐보고 실행이 잘 되는것도 중요하지만 큰 틀에서 해당 예제를 바라보시면 좋은 공부가 될 것 같습니다.

Servlet Request 파헤치기

Request-Header

이번에는 HTTP 요청이 들어올 경우 어떻게 해당 요청이 파싱되어 최종적으로 개발자가 이용할 수 있는지에 대해 알아봅시다.HTTP Request의 경우 아래와 같이 다양한 정보를 가진 채로 요청이 들어옵니다.

위와 같은 요청 메세지는 대략 헤더와 바디로 나누어집니다.(디테일하게 보면 헤더 보다 앞서 Start Line이라는 부분이 따로 존재합니다)헤더의 경우 들어오는 요청이 어떤 동작을 취하는지 또 어떤 곳으로 보내지는지,어떤 형식으로 들어오는지 등 해당 리퀘스트에 대한 정보를 담고 있습니다.반면 메세지 바디 부분은 클라이언트가 서버에게 보내는 다양한 데이터들이 담겨 있습니다.

우선 위와 같은 Request 요청이 저희가 구동한 WAS에 도달하면 서블릿에서 해당 메세지를 파싱하여 HttpServletRequest를 통해 request 객체를 만들어 줍니다.그리고 이러한 객체를 사용해서 개발자들은 도착한 request의 다양한 정보들을 활용하여 원하는 방식으로 로직을 구현할 수 있습니다.

지금부터 Request Header를 다루는 다양한 방법들을 코드로 직접 구현해 봅시다.

@WebServlet(name = "requestHeaderServlet",urlPatterns = "/request-header")
public class RequestHeaderServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		printStartLine(req);
        printHeaders(req);   
    }

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

    private void printHeaders(HttpServletRequest request) {
        System.out.println("--- REQUEST-LINE - start ---");
        request.getHeaderNames().asIterator().forEachRemaining(
                headerName -> System.out.println(headerName+ " : " + headerName)
        );
        System.out.println("--- REQUEST-LINE - end ---");
        System.out.println();
    }
}

위의 코드는 간단하게 리퀘스트 요청이 올 경우,해당 요청으로부터 서블릿이 request 객체를 생성하고 해당 객체를 이용해서 요청 정보를 얻어오는 코드입니다.메서드를 그냥 사용만 하시면 정보가 바로 넘어오기에 어렵지는 않습니다.간단하게 따라 쳐보시고 넘어가겠습니다.다음으로는 Request 바디,즉 요청 데이터들을 다뤄봅시다.하지만 다루기에 앞서 클라이언트단에서 서버단으로 데이터를 전송하는 3가지 방법에 대해 먼저 알아보겠습니다.

Request-Body(클라이언트에서 서버로 데이터를 전달하는 방법)

  1. GET - 쿼리 파라미터

해당 방식의 대표적인 예가 바로 구글 검색입니다.아래와 같이 구글에 “스프링”이라고 검색을 할 경우 url 주소에 “스프링”이라는 단어가 함께 넘어가면서 검색이 진행되는것을 확인해 볼 수 있습니다.HTTP 요청의 메세지 바디에 아무것도 넣지않고 데이터를 전송하는 방식입니다.

그러면 서블릿에서 해당 방식으로 데이터 받아봅시다.아주 간단하게 http://localhost:8080?username=brido&age=25이라는 GET 요청을 보내봅시다.

@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("[단일 파라미터 조회] - start");
        String username = request.getParameter("username");
        String age = request.getParameter("age");
        System.out.println("age = " + age);
        System.out.println("username = " + username);
        System.out.println("[단일 파라미터 조회] - end");
        
        response.getWriter().write("OK");
    }
}

아래와 같이 쿼리 파리미터로 보낸 데이터들이 서버에 잘 전송되었음을 확인해볼 수 있습니다.

  1. POST - HTML FORM

아마 여러분들도 수많은 사이트에서 회원가입을 하시면서 이미 경험해보신 방식일 것입니다.회원가입,상품주문 등의 형태에서 데이터를 전송할 때 해당 방식을 주로 사용합니다.이는 메세지 바디에 쿼리파라미터 형식으로 데이터를 전달하는것이 특징이며 content-typeapplication/x-www-form-urlencoded로 전송됩니다.(처음 프론트엔드분들과 협업을 할 때,해당 content-type을 알지 못해서 시간을 많이 날렸습니다)

마찬가지로 서블릿에서 해당 방식을 사용해 데이터를 전송해 봅시다.앞서 만든 RequestParamServlet 서블릿을 동일하게 사용하면 됩니다.다만 이번에는 직접 URL창에 데이터를 쳐서 보내는것이 아니라 Postman을 이용해서 POST 방식으로 데이터를 전송하면 됩니다.아래와 같이 content-type을 맞추고 api를 전송시켜봅시다.1번 방법과 동일한 결과가 출력되는 것을 확인할 수 있습니다.

정리해보겠습니다. application/x-www-form-urlencoded 형식은 앞서 GET에서 살펴본 쿼리 파라미터 형식과 데이터를 보내는 방식이 동일합니다.따라서 쿼리 파라미터 조회 메서드를 그대로 사용해도 됩니다.클라이언트(데이터를 전송하는 곳)입장에서는 두 방식이 다르지만,해당 데이터를 받는 서버 입장에서는 형식이 동일하기에 requset.getParameter()메서드를 통해 두가지 방식 다 데이터를 전송 받을 수 있습니다.

  1. HTTP message Body에 직접 데이터 싣어서 보내기

최근에는 HTTP API 통신,즉 Rest API라는 구조를 채택할 경우 위와 같은 방식 중 JSON으로 데이터를 보내는 API가 굉장히 많이 사용됩니다.아래와 같이 주로 DTO 형식으로 클라이언트단에서 필요한 정보를 담아 서버에서 비지니스 로직을 처리하는 방식으로 자주 사용됩니다.

마찬가지로 서블릿에서도 해당 방식으로 데이터를 전송해봅시다.

우선 Json 형식의 데이터를 받기 위한 클래스를 하나 만들어 봅시다.(추후 DTO라고 생각하시면 됩니다.)

import lombok.Data;

@Data
public class HelloData {
    private String username;
    private int age;
}

그리고 나서 새로운 서블릿 클래스를 하나 만들어주시면 됩니다.

@WebServlet(name = "requestBodyJsonServlet", urlPatterns = "/request-body-json")
public class RequestBodyJsonServlet extends HttpServlet {

    //JSON DATA를 Object로 바꿔주는 객체 from Jackson Library
    //Springboot에는 자체 내장 되어있음
    private 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);

        System.out.println("messageBody = " + messageBody);
        HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);

        System.out.println("helloData.toString() = " + helloData.toString());

        response.getWriter().write("OK");
    }
}

위 서블릿을 테스트 해보기 위해서는 Postman에서 아래와 같이 POST방식으로 json의 형태로 HelloData 객체에 대한 데이터를 보내줘야합니다.

깔끔하게 데이터가 Json으로 변형되어 잘 전달 되는것을 확인해볼수 있습니다.

세부적인 코드들에 대해 알아봅시다.ServletInputStream의 경우 들어온 메세지 바디의 데이터를 바이트단위로 변형해서 전달받는 역할을 합니다.이후 StreamUtils의 copyToString() 메서드를 통해 바이트로 되어있는 데이터를 실제 스트링 형태로 바꿔서 읽어보면 데이터 잘 전송되어있음을 확인할 수 있습니다.

마지막으로 objectMapper의 경우, 들어온 데이터를 Json형태로 변경시켜주는 라이브러리의 객체라고 생각해주시면 됩니다.messagebody의 경우 string 형태로 존재하기에 해당 객체를 저희가 지정한 HelloData 클래스의 Json 형식으로 변경하기 위한 도구라고 생각하시면 됩니다.ObjectMapper의 경우 복잡하거나 가독성이 떨어지는 api응답을 reform하여 클라이언트단으로 넘겨줄때 자주사용되니 알아두시면 유용합니다.(기상청 데이터를 reform할때 종종 사용했습니다)

이렇게 서블릿에서 Request의 메세지 바디,즉 서버로 데이터를 전송하고 어떻게 이들을 다루는지에 대해 알아보았습니다.이후 스프링 mvc를 적용하더라도 데이터 전송방식에 있어서는 위의 3가지 방식을 벗어나지 않으니 확실하게 알아둡시다.

Servlet Response 파헤치기

Response-Header

HTTP Response도 Request와 비슷한 구조를 가지고 있습니다.첫번째 라인에서는 Status Line라고 해서 해당 요청에 대한 상태값을 나타내는 라인을 가지고 나머지는 헤더와 바디로 구성됩니다.서블릿에서 Response 응답을 다루는 방법은 request 요청이 들어올때 생성된 HttpServletResponse 객체를 사용합니다.아래 코드를 통해 직접 response를 다뤄봅시다. 우선 Response-Header 먼저 다루는 코드를 보겠습니다.

@WebServlet(name = "responseHeaderServlet",urlPatterns = "/response-header")
public class ResponseHeaderServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.setStatus(HttpServletResponse.SC_OK);
        content(response);
        response.setHeader("Cache-Control", "no-cache,no-store,no-validate");
        response.setHeader("Pragma", "no-cache");
        response.setHeader("my-header","brido");
        response.getWriter().write("성공");
        cookie(response);
        redirect(response);
    }

    private void content(HttpServletResponse response) {
        response.setContentType("text/plain");
        response.setCharacterEncoding("utf-8");
    }
}

첫번째 줄을 보시면 응답의 상태코드 및 content를 직접 지정해 줄 수 있습니다.또한 사용자가 만든 임의의 헤더를 response 헤더내에 위치시킬수도 있습니다.캐시에 대한 사용여부 또한 직접 조정가능합니다.

Response-Body

이번에는 Response의 body 부분에 데이터를 담아서 전달하는 법을 알아봅시다.크게 텍스트/HTML 응답과 Json형식 데이터 응답으로 나뉠 수 있습니다.사실 전자의 방식은 저희가 이미 접해보았습니다. response.getWriter().write("ok"); 코드를 이용해 실제로 response에 저희가 원하는 text를 넣어줄수 있었습니다.

HTML을 보내는 방식 또한 굉장히 간단합니다.아래의 코드와 같이 content-type과 encoding 방식을 지정하고 writer를 통해서 html 코드를 직접 넣어주면 전송할 수 있습니다.

@WebServlet(name = "responseHtmlServlet", urlPatterns = "/response-html")
public class ResponseHtmlServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html");
        resp.setCharacterEncoding("utf-8");

        PrintWriter writer = resp.getWriter();
        writer.println("<html>");
        writer.println("<body>");
        writer.println("<div>Hello Response</div>");
        writer.println("</body>");
        writer.println("</html>");
    }
}

이번에는 Json형식의 데이터를 response에 담아보겠습니다.Json으로 요청을 받을때와 마찬가지로 ObjectMapper를 사용합니다.이후 contentType과 encoding 타입을 지정해주고 원하는 데이터 형식의 객체를 생성합니다.(HelloData는 DTO라고 많이 표현합니다)최종적으로 ObjectMapper를 이용해서 JSON형태인 DTO 데이터를 String으로 response에 넣어주면 됩니다.

@WebServlet(name = "responseJsonServlet", urlPatterns = "/response-json")
public class ResponseJsonServlet extends HttpServlet {
    ObjectMapper mapper = new ObjectMapper();

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //Content-Type
        resp.setContentType("application/json");
        resp.setCharacterEncoding("utf-8");

        HelloData data = new HelloData();
        data.setAge(25);
        data.setUsername("Brido");

        String string = mapper.writeValueAsString(data);
        resp.getWriter().write(string);
    }
}

이렇게 해서 저희는 서블릿을 이용해서 HTTP의 요청과 응답을 다루는 방법을 간략하게 알아보았습니다.추후 배우게될 스프링 MVC는 위와 같은 서블릿 코드를 기반으로 놀라울 정도로 편하게 요청과 응답을 다룰수 있습니다.단순히 스프링을 쓰는 방법을 아는것도 굉장히 중요하지만 어떠한 불편함 때문에 스프링 프레임워크가 발생했을까를 배우는 점에서 서블릿은 굉장히 중요합니다!

Ref : 김영한 - 스프링 MVC 1편