Spring MVC : HttpServletRequest에 대한 정리

    HttpServletRequest의 역할


    HTTP 요청이 도착하면 위와 같은 HTTP 요청 메세지를 하나씩 파싱해야한다. 사실 저건 굉장히 간단하게 되어있는 것이고, 이런저런 Header가 붙고 데이터가 많아지면 개발자가 하나씩 챙기기 너무 어려운 문제가 된다. 특히나 요청이 올 때 마다 직접 파싱을 한다면 그야말로 헬게이트가 열린다. 이런 불합리(?)를 도와주기 위해 HttpServletRequest 객체가 지원된다.

    HttpServletRequest 객체는 HTTP 요청 메세지를 파싱해서, 객체 필드에 차곡차곡 잘 저장을 해두었다. 이 객체에서 특정 메서드를 활용하면 잘 파싱된 HTTP의 헤더, 데이터를 손쉽게 읽어올 수 있다. HttpServeltRequest는 이런 역할을 한다. 

    뿐만 아니라 몇 가지의 편리한 기능을 제공하기도 한다.

    1. 임시 저장소 기능
      • HTTP는 요청이 오고 응답이 나갈 때 까지가 생존 범위다. 이 기간동안 데이터를 저장해두고, 꺼내오고 싶을 때 꺼내올 수 있다. Http 메세지 안에는 작은 저장소가 있는데 그 곳에 데이터를 넣고 빼고, 명령어는 다음을 사용한다.
      • Request.setAttribute(name, value) / Request.getAttribute(name). 이 기능으로 잠깐 Model처럼 쓰기도 한다.
    2. 세션 관리 기능
      • 세션은 로그인을 유지할 때, 쿠키를 간략화해서 주는 것들이다(쿠키에 너무 많은 정보가 있으면 안되니..)
      • HttpRequestServlet은 이 세션을 관리해준다.
      • Request.getSession(create: True)

     

    HttpServletRequest 객체를 이용한 메세지 출력해보기


    HTTP 요청 메세지, Start Line 출력

    private void printStartLine(HttpServletRequest request) {
    
        System.out.println("=====START-LINE=====begin");
        System.out.println("request.getMethod() = " + request.getMethod());
        System.out.println("request.getProtocol() = " + request.getProtocol());
        System.out.println("request.getRequestURI() = " + request.getRequestURI());
        System.out.println("request.getRequestURL() = " + request.getRequestURL());
        System.out.println("request.getScheme() = " + request.getScheme());
        System.out.println("request.getQueryString() = " + request.getQueryString());
        System.out.println("request.isSecure() = " + request.isSecure());
        System.out.println("=====START-LINE=====end");
    }

    HTTP 요청 메세지 중, Start Line만 출력하도록 해본다. Service 메서드 안에서도 구현할 수는 있지만, 그렇게 할 경우 코드가 지저분해지기 때문에 편의 메서드를 만들고 그곳에서 각종 값이 출려될 수 있도록 한다. 여기서 is.Secure()는 보안이 적용되어있는지 물어보는 항목이다. 즉, HTTPS Scheme으로 되었는지 본다고 생각하면 될 것 같다.

    실제 출력 결과를 살펴보면 위와 같다. 

    • is.Secure() = False
      • http://localhost:8080으로 접근했기 때문에 보안은 적용되지 않았다.
    • getQueryString() = null
      • URI는 /request-header로만 접근했다. 따라서, Query String이 없어서 null값이 나온다.

     

    HTTP 요청 메세지, 모든 Header 출력해보기

        private void printHeaders(HttpServletRequest request) {
    
            System.out.println("====Header===Start");
    
    /*
            Enumeration<String> headerNames = request.getHeaderNames();
            while(headerNames.hasMoreElements()){
                String headerName = headerNames.nextElement();
                System.out.println(headerName + ": " + request.getHeader(headerName) );
            }
    */
            request.getHeaderNames().asIterator().forEachRemaining(
                    headerName -> System.out.println(headerName + ": " + request.getHeader(headerName))
            );
    
            System.out.println("====Header===end");
            System.out.println();
    
        }

    HTTP 요청 메세지의 모든 Header를 출력하는 코드는 위와 같이 작성할 수 있다. 크게 두 가지 타입으로 나눌 수 있다. Enumeration 타입으로 받은 후, 이걸 하나하나 출력하는 방법이 있다. 그리고 또 다른 방법은 Stream 형식으로 바꾼 후 람다식을 활용해서 출력하는 방법이다. 최근 자바에 람다식이 추가되면서, 아래 방법으로 코드를 간결화해서 사용한다고 한다. 

    출력 결과는 위처럼 Header가 모드 정상적으로 출력된 것을 볼 수 있다. 

     

    HTTP 요청 메세지, 편의성 Header 출력해보기

    HTTP 요청 메세제의 Header를 한 줄씩 출력해볼 수도 있지만, 필요한 것을 부분 부분 출력하는 메서드도 HttpServletRequest 객체는 지원한다. 여기서는 그렇게 한번 사용을 해보고자 한다. 

    private void printHeadersUtils(HttpServletRequest request){
    
        System.out.println("====Host 편의 조회====");
        System.out.println("request.getServerName() = " + request.getServerName());
        System.out.println("request.getServerName() = " + request.getServerPort());
    
        System.out.println();
    
    
        System.out.println("====Accept Language 편의 조회====");
        request.getLocales().asIterator().forEachRemaining(
                locale -> System.out.println("locale = " + locale)
        );
        System.out.println("request.getLocale() = " + request.getLocale());
        System.out.println();
    
    
        System.out.println("===== 쿠키 출력하기 ====");
        if(request.getCookies() != null){
            Cookie[] cookies = request.getCookies();
            for (Cookie cookie : cookies) {
                System.out.println(cookie.getName() + ": " + cookie);
            }
        }
        System.out.println();
    
        System.out.println("Content 편의 조회하기");
        System.out.println("request.getContentType() = " + request.getContentType());
        System.out.println("request.getContentType() = " + request.getContentLength());
        System.out.println("request.getCharacterEncoding() = " + request.getCharacterEncoding());
        System.out.println();
    
    
    }

    위의 코드를 작성해서 사용한다. 그리고 실행결과를 살펴본다.

    출력 결과는 위와 같다. 

    • Accept Language
      • 호스트마다 지원하는 Accept Language는 여러 개가 될 수 있다. 따라서 Locals()로 받아오게 되면, 이 모든 것들을 받아올 수 있다. 그래서 Iterator로 만든 후 람다식으로 하나하나 출력하게 할 수 있다.
      • 반면 locale()로 받아오게 되면 locale의 가장 앞에 있는 것만 출력이 된다. 
    • Content
      • Content에는 아무런 내용이 없는 것을 볼 수 있다. 이것은 Get 메서드로 요청을 보냈기 때문이다. POST 메서드로 요청을 보내게 되면 달라진다. 

     

    POST MAN으로 POST 요청해서 Content 내용확인하기

    POST MAN이라는 프로그램을 사용하면 원하는 HTTP 메서드로 쉽게 해당 URL로 요청 메세지를 발송할 수 있다. POST MAN으로 URI를 입력하고, BODY에 원하는 내용의 타입으로 원하는 내용을 적어주고 SEND를 보내면 실제로 POST 메서드로 발송된다. 

    실행 결과를 보면 CONTENT에 text/plain의 UTF-8로 인코딩 된 길이 5짜리가 들어간 것이 확인된다. 내가 실제로 발송했던 문자는 'HELLO'였다. 

     

    HttpServletRequest의 요청 데이터 실제로 조회해보기.

    HTTP 요청으로 서버에 데이터를 보내는 방법은 크게 세 가지가 있다.

    • GET 메서드 쿼리 파라미터에 데이터를 포함
    • POST - HTML FORM 형식으로 전달
    • HTTP 메세지 바디에 데이터 포함

    클라이언트는 위 세가지 방식으로 서버에 데이터를 보낼 수 있다. 그렇다면 각 방식들에 대해 한번씩 알아보고자 한다. 

    • GET + 쿼리 파라미터.
      • /URI?username=hello&age=20
      • GET은 메세지 바디 없이, URL의 쿼리 파라미터에 데이터를 포함해서 전달
      • 주로 검색, 필터, 페이징 등에서 많이 사용함. 
    • POST + HTML FORM
      • HTML Form에 입력된 데이터를 쿼리 파라미터 형식으로 메세지 바디에 적재해서 보냄.
      • Content-Type : application/x-www-form-urlencoded
      • 예) 회원 가입, 상품 주문, HTML FORM 사용
    • HTTP Message Body에 데이터를 직접 담아서 요청
      • HTTP API에서 주로 사용한다. JSON, XML, TEXT 등
      • PUT, POST, PATCH

     

    GET + 쿼리 파라미터로 단일 데이터 받고, 읽어보기


    쿼리 파라미터로 "?username=kim&age=20"을 GET으로 넘겨준다고 가정해보자. 이 때, 넘겨지는 데이터를 읽어보고자 한다. 서블릿은 HTTP 요청이 오면, 이미 이 데이터를 모두 파싱해서 request 객체에 잘 저장해뒀다. 개발자는 이 request 객체에 저장된 데이터를 읽어오기만 하면 된다. 

    Enumeration<String> parameterNames = request.getParameterNames();
    while(parameterNames.hasMoreElements()) {
        String paraName = parameterNames.nextElement();
        System.out.println(paraName + ": " + request.getParameter(paraName));
    }

    위 코드로 받아온 데이터를 하나씩 출력할 수 있다. 기본적으로 getParamterNames()를 하면 request에 있는 모든 파라메터의 이름을 가져온다. 이 파라메터의 이름을 key로 해서 검색을 하고, 저장되어있는 데이터를 끄집어 낼 수 있다.

    request.getParameterNames().asIterator().forEachRemaining(paramName ->
            System.out.println(paramName +": " + request.getParameter(paramName))
    );

    자바8부터는 람다식을 지원한다. 이터레이터를 만들어서 람다식을 활용해서 하나하나 부르는 방법도 사용할 수 있다. 

     

    GET + 쿼리 파라미터로 중복 데이터 받고, 읽어보기


    쿼리 파라미터로 항상 유일한 파라미터가 들어가면 좋겠지만 그렇지 않은 경우가 있다. 예를 들어 "?username=kim&username=kkk"가 들어가면 값을 제대로 읽어올까? getParameter()로 불러오게 되면 제대로 읽지 못한다. 왜냐하면 getParameter()는 하나의 값만 있을 때 사용이 가능하다. 그렇다면 어떻게 해야할까? 

    String[] usernames = request.getParameterValues("username");
    for (String username : usernames) {
        System.out.println("username = " + username);
    }

    위 코드로 동일한 파라미터 이름에 여러개의 값이 들어왔을 때 모든 값을 불러올 수 있다. getParameterValues()라는 코드를 사용하면 많은 도움이 된다. 

    실행 결과는 다음과 같다. 

     

    GET + 쿼리 파라미터 데이터 불러오기 정리


    • getParameterNames() + 람다식으로 하나씩 있는 파라미터는 호출 가능하다.
    • getParameterNames()로 불렀을 때, 여러 개의 값이 있는 파라미터는 가장 앞에 있는 값만 출력된다. 
    • 하나의 파라미터에 여러개의 값이 있는 경우는 getParameterValues()를 이용해 불러야한다.

     

    POST + HTML FORM 활용 실습


    <form action=“/save” method=“post”>
       <input type=“text” name=“username” />
       <input type=“text” name=“age” />
       <button type=“submit”>전송</button>
    </form>

    위 코드로 HTML을 표현할 수 있다. HTML을 표현하게 되면, 다음과 같은 형태의 HTML FORM이 구현된다. 

    • action
      • HTTP 메서드에서 사용될 URI다.
      • /save는 절대 경로, save는 상대경로가 된다. 
    • method 
      • 어떤 HTTP 메서드를 사용할지를 기입한다.

    위의 코드에 kim, 20을 입력하게 된다면 위의 HTML FORM 코드는 다음과 같이 변경된다. 

    POST /save HTTP/1.1
    Host: localhost:8080
    Content-Type: application/x-www-form-urlencoded
    
    username=kim&age=20

    앞서 이야기한 것처럼 HTML FORM은 GET에서 사용하는 쿼리 파라미터와 동일한 형태로 메세지 바디에 적재되고, POST로 날아가게 된다. 아래에 실습을 해보고자 한다.

    /webapp/basic/hello-form.html 경로
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
    <form action="/request-param" method="post">
        username: <input type="text" name="username" />
        age:      <input type="text" name="age" />
        <button type="submit">전송</button>
    </form>
    </body>
    </html>

    위에 표시된 경로에 파일을 하나 생성하고, 위 코드를 붙여넣는다. 위 코드에서 보면 POST /request-param 형태로 HTTP 요청 메세지가 생성될 것이고, Content-Type은 application/x-www-html-urlencoded로 되어 usernamd=?&age=?형태로 메세지 바디에 값이 저장되어 전달될 것임을 알 수 있다. 그래서 해야할 것은 /request-param 요청이 왔을 때 응답할 서블릿을 만들어줘야 한다. 

    @WebServlet(name = "requestParamServlet", urlPatterns = "/request-param")
    public class RequestParamServlet extends HttpServlet {
    
        @Override
        protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    
            request.getParameterNames().asIterator().forEachRemaining(
                    paramName -> System.out.println(request.getParameter(paramName))
            );
        }
    }

    간단하게 위의 클래스를 만들고 대응되는 서플링 맵핑을 해주었다. 값이 제대로 전달되는지만 보면 되기 때문이다. 실제로 localhost:8080/basic/hello-form.html로 접속해보면 HTML FORM이 뜨고, 필요한 값을 채워서 제출을 해봤다.

    확인해보면 POST 형식으로 PayLoad에 있는 데이터가 잘 넘어간 것을 볼 수 있다. 그리고 실제 콘솔에서는 값이 제대로 나왔는지를 볼 수 있다.

    값도 정상적으로 출력되는 것을 확인할 수 있다. 

    HTML FORM으로 입력된 데이터는 Content-Type을 x-www-html-urlencoded로 설정해주면, 웹브라우저가 쿼리 파라메터와 동일한 key&parameter 형식으로 메시지 바디에 데이터를 넣어준다. 그리고 이 HTTP 요청이 가면, 서블릿은 이 데이터를 파싱해서 request에 잘 저장해둔다.

    클라이언트 입장에서는 GET + 쿼리 파라미터로 보내는 것과, HTML FORM으로 보내는 것에 차이가 있을 수 있다. 그렇지만 서버 입장에서는 받은 데이터가 key=value 형식이기 때문에 GET으로 받으나 POST로 받으나 동일하게 데이터를 꺼낼 수 있게 된다. 

     

    위 테스트는 POST MAN으로도 편하게 할 수 있다. 

    POSTMAN에서 Content-Type을 선택해주면 굳이 HTML 파일을 따로 만들 필요도 없다. HTML FORM 형태에 필요한 KEY, VALUE를 채워주고 SEND를 보내기만 하면 된다. 

    위처럼 동일한 결과를 볼 수 있다.

     

    HTTP 요청 메세지 - API 메세지 바디 → InputStream으로 읽어야 함. 


    위 방법은 HTTP 메세지 바디에 내가 원하는 텍스트를 직접 싣고 전송하는 방법이다. 주로 HTTP API에서 사용하며, 전송형태는 JSON, XML, TEXT 형태로 보내게 된다. 데이터 형식은 주로 JSON형태로 보내게 된다. 이번 실습에서는 가장 단순한 TEXT를 보내보려고 한다. 

     

    Text로 메세지 보내기 → Request에서 InputStream 형태

    Text로 메세지를 보내게 되면, 이 Text는 request에 InputStream형태로 저장되게 된다. 이 때 InputStream은 바이트 코드로 되어있는데, 우리가 읽을 수 있게 문자열로 변경이 필요하다. 이 때, StringUtils에 있는 copyToString 메서드를 활용해서 String으로 바꾸어 줄 수 있다. 

    포스트맨으로 위와 같은 형식으로 HTTP 요청 메세지를 발송한다. 이에 대응하는 서블릿을 만들어본다.

    ServletInputStream inputStream = request.getInputStream();
    
    String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
    System.out.println("messageBody = " + messageBody);
    
    response.getWriter().write("OK");

    서블릿을 만들어본 후, 포스트맨을 통해서 실제로 요청을 해서 결과가 어떻게 나오는지를 본다. 

     

    JSON 형식으로 보내기 → InputStream을 객체로 Mapping 필요

    JSON 형식으로 보내도 값은 InputStream 형식으로 나온다. 그럼 먼저 InputStream의 바이트 코드를 우리가 읽을 수 있는, 문자열로 변환해주어야 한다. 문자열로 변환하면 JSON 형식으로 값이 그대로 되어있고, 이 JSON은 보통 객체로 파싱해서 사용을 해야한다. JSON 파싱을 위한 객체를 먼저 하나 만들어야 한다. 

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

    먼저 HelloData라고 들어올 JSON가 파라메터를 공유할 수 있는 클래스를 만들었다. 그리고 이제 서블릿에서 JSON과 이 객체를 맵핑해주는 작업을 해야한다.

    ServletInputStream inputStream = request.getInputStream();
    String msgBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
    
    HelloData helloData = objectMapper.readValue(msgBody, HelloData.class);
    
    System.out.println("helloData.getUsername() = " + helloData.getUsername());
    System.out.println("helloData.getUsername() = " + helloData.getAge());
    
    response.getWriter().write(200);

    자바는 JACKSON이라는 라이브러리가 있고, JACKSON 라이브러리는 ObjectMapper 객체를 제공한다. 이 객체는 readValue 기능을 이용해 JSON 데이터를 특정 클래스에 파싱해서 해당 클래스로 반환해주는 역할을 한다. 위처럼 코드를 구성하고 POST맨으로 아래의 JSON 데이터를 날려봤다. 

    이처럼 JSON을 날리면 되는데, 이 때 맵핑을 할 때는 key와 필드가 같은 것을 맵핑해서 데이터를 파싱해준다. 실제로 POST를 한 후의 결과를 확인해보면 아래와 같다. 

    댓글

    Designed by JB FACTORY