개발은 아름다워

[ Spring ] HTTP 요청 데이터와 HtttpServletRequest,HttpServletResponse 객체의 역할 본문

스프링

[ Spring ] HTTP 요청 데이터와 HtttpServletRequest,HttpServletResponse 객체의 역할

do_it_zero 2024. 10. 12. 11:33

HttpServlet은 뭘까?

클라이언트에서 서버로 요청시, 브라우저는 HTTP 프로토콜에 맞춰서 요청 메서지를 만들어서 서버로 보낸다.

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

서버에는 위와 같은 HTTP 요청 메세지가 온다. 그렇다면 서버에서는 HTTP 요청 메세지를 어떻게 처리해야하는걸까? HTTP 요청 메세지를 개발자가 직접 파싱해서 사용해도 되지만, 이 경우는 매우 매우 불편할 것이다. 그래서 서블릿은 개발자가 HTTP 요청 메세지를 편리하게 사용할 수 있도록 개발자 대신에 HTTP 요청 메세지를 파싱한다. 그리고 그 결과를 HttpServletRequest 객체에 담아서 제공한
다.

또한 임시저장소 기능과 세션 관리 기능도 포함하고 있다.

즉, HttpServletRequest 란 HTTP 요청 메세지에 대한 데이터들을 갖고 있는 객체!

HttpServletRequest 기본 사용법

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


    // start line 정보
    private void printStartLine(HttpServletRequest request){
        System.out.println("--REQUEST-LINE - start --");
        System.out.println("request.getMethod() = " + request.getMethod());
        System.out.println("request.getProtocol() = " + request.getProtocol());
        System.out.println("request.getScheme() = " + request.getScheme());
        System.out.println("request.getRequestURL() = " + request.getRequestURL());
        System.out.println("request.getRequestURI() = " + request.getRequestURI());
        System.out.println("request.getQueryString() = " + request.getQueryString());
        System.out.println("request.isSecure() = " + request.isSecure());
        System.out.println("--- REQUEST-LINE - end ---");
        System.out.println();
    }

    // 헤더 정보
    private void printHeaders(HttpServletRequest request){
        System.out.println("--- Headers - start ---");
        request.getHeaderNames().asIterator()
                .forEachRemaining(
                    headerName -> 
                    System.out.println(headerName+": " +request.getHeader(headerName))
                );
        System.out.println("---Headers - end ---");
        System.out.println();
    }

    // 헤더 편리한 조회
    private void printHeaderUtils(HttpServletRequest request){
        System.out.println("--- Header 편의 조희 start ---");
        System.out.println("[Host 편의 조회]");
        System.out.println("request.getServerName() = " + request.getServerName());
        System.out.println("request.getServerPort() = " + 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("[cookie 편의 조회]");
        if(request.getCookies() != null){
            for(Cookie cookie : request.getCookies()){
                System.out.println(cookie.getName() + ": " + cookie.getValue());
            }
        }
        System.out.println();
        
        System.out.println("[Content 편의 조회]");
        System.out.println("request.getContentType() = " + request.getContentType());
        System.out.println("request.getContentLength() = " + request.getContentLength());
        System.out.println("request.getCharacterEncoding() = " + request.getCharacterEncoding());
        System.out.println("--- Header 편의 조회 end ---");
        System.out.println();
    }

    // 기타 정보 조회 
    private void printEtc(HttpServletRequest request){
        System.out.println("--- 기타 조회 start ---");

        System.out.println("[Remote 정보]");
        System.out.println("request.getRemoteHost() = " + request.getRemoteHost());
        System.out.println("request.getRemoteAddr() = " + request.getRemoteAddr());
        System.out.println("request.getRemotePort() = " + request.getRemotePort());
        System.out.println();

        System.out.println("[Local 정보]");
        System.out.println("request.getLocalName() = " + request.getLocalName());
        System.out.println("request.getLocalAddr() = " + request.getLocalAddr());
        System.out.println("request.getLocalPort() = " + request.getLocalPort());

        System.out.println("--- 기타 조회 end ---");
        System.out.println();
    }
}

위 코드로 HTTP 요청 메세지를 HttpServletRequest가 start-line,header정보 조회를 하는지 알 수 있었다. 이번에는 HTTP 요청 데이터를 어떻게 조회하는지 알아보자

HTTP 요청 데이터 조회하기

먼저 HTTP 요청 메세지를 통해 클라리언트에서 서버로 데이터를 전달하는 방법을 알아보자
주로 다음 3가지 방법을 사용한다.

  1. GET - 쿼리 파라미터
  • /url?username=j&age=20
  • 메시지 바디 없이 URL 쿼리 파라미터에 데이터를 포함해서 전달
  • 주로 검색,필터,페이징등에 많이 사용된다.
  1. POST - HTML FORM
  • contenct-type: application/x-www-form-urlencoded
  • 메시지 바디에 쿼리 파라미터 형식으로 전달 username=jj&age=20 / 형식은 쿼리 파라미터인데 데이터는 메세지 바디에 담긴다
  • 주로 회원 가입,상품 주문,HTML Form에 사용됨

3.HTTP message body에 데이터 직접 담기

  • HTTP API에서 주로 사용하며 JSON,XML,TEXT 등의 형식이 쓰임
  • 데이터 형식은 주로 JSON 사용함

GET 쿼리 파라미터

메세지 바디 없이 URL의 쿼리 파라미터를 사용해서 데이터를 전달
서버에서는 HttpServletRequest가 제공하는 메서드들을 통해 쿼리 파라미터를 편리하게 조회할 수 있다.

http://localhost:8080/request-param?username=hello&age=20
위와 같이 URL에 ? 뒤에 데이터를 전달하는 방식이다.

쿼리 파라미터 조회 메서드

getParmeter("username") // 단일 파라미터 조회
getParameterNames() // 파라미터 이름들 모두 조회
getParameterMap() // 파라미터를 Map으로 조회
getParameterValues("username") // 복수 파라미터 조회

@WebServlet(name = "requestParamServlet",urlPatterns = "/request-param")
public class RequestParamServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("[전체 파라미터 조회] - start");
        
        req.getParameterNames().asIterator()
            .forEachRemaining(paramName -> System.out.println(paramName + "=" + req.getParameter(paramName)));
        System.out.println("[전체 파라미터 조회] - end");
        System.out.println();
        
        System.out.println("[단일 파라미터 조회]");
        String username = req.getParameter("username");
        System.out.println("request.getParameter(username) = " + username);

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

        System.out.println("[이름이 같은 복수 파라미터 조회]");
        System.out.println("request.getParameterValues(username)");
        String[] usernames = req.getParameterValues("username");
        for(String name : usernames){
            System.out.println("username="+name);
        }

        resp.getWriter().write("ok");
    }
}

결과
[전체 파라미터 조회] - start
username=hello
age=20
[전체 파라미터 조회] - end

[단일 파라미터 조회]
request.getParameter(username) = hello
request.getParameter(age) = 20

[이름이 같은 복수 파라미터 조회]
request.getParameterValues(username)
username=hello

HTML Form 으로 POST 요청

이번에는 HTML의 Form을 사용해서 서버로 데이터 전송
주로 회원 가입, 상품 주문 등에서 사용하는 방식이다.

다음과 같은 특징이 있다.

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

HTML 요청 Form

<!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의 HTML Form을 전송하면 웹 브라우저는 다음 형식으로 HTTP 메시지를 만든다.
요청 URL: http://localhost:8080/request-param
content-type: application/x-www-form-urlencoded

message body: username=hello&age=20

application/x-www-form-urlencoded 형식은 앞서 GET에서 살펴본 쿼리 파라미터 형식과 같다. 메세지 바디에 데이터가 있지만 쿼리 파라미터 형식으로 전달할 뿐이다. 따라서 쿼리 파라미터 조회 메서드를 그대로 사용하면 된다.

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

HTML Form 요청을 처리하는 서블릿은 위와 같다

@WebServlet(name = "requestParamServlet",urlPatterns = "/request-param")
public class RequestParamServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("[전체 파라미터 조회] - start");
        
        req.getParameterNames().asIterator()
            .forEachRemaining(paramName -> System.out.println(paramName + "=" + req.getParameter(paramName)));
        System.out.println("[전체 파라미터 조회] - end");
        System.out.println();
        
        System.out.println("[단일 파라미터 조회]");
        String username = req.getParameter("username");
        System.out.println("request.getParameter(username) = " + username);

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

        System.out.println("[이름이 같은 복수 파라미터 조회]");
        System.out.println("request.getParameterValues(username)");
        String[] usernames = req.getParameterValues("username");
        for(String name : usernames){
            System.out.println("username="+name);
        }

        resp.getWriter().write("ok");
    }
}

HTML Form 요청 결과

Host: localhost:8080
Connection: keep-alive
Content-Length: 21
Cache-Control: max-age=0
sec-ch-ua: "Not/A)Brand";v="8", "Chromium";v="126", "Microsoft Edge";v="126"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
Origin: http://localhost:8080
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0
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.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: http://localhost:8080/basic/hello-form.html
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: ko,en;q=0.9,en-US;q=0.8

username=hello&age=20]
[전체 파라미터 조회] - start
username=hello
age=20
[전체 파라미터 조회] - end

[단일 파라미터 조회]
request.getParameter(username) = hello
request.getParameter(age) = 20

[이름이 같은 복수 파라미터 조회]
request.getParameterValues(username)
username=hello

여기서 중요한 것은 HTML Form 요청일 경우에는 메세지 바디에 데이터가 들어가지만, content-type: application/x-www-form-urlencoded 이기 때문에 서버입장에서는 쿼리 파라미터 형식으로 받아들여서 처리하게 된다. 그래서 HttpServletRequest 객체인 request가 request.getParameter() 요청 데이터를 조회할 수 있는 것이다.

API 메세지 바디

HTTP message body에 데이터를 직접 담아서 요청하는 방법이다.

  • HTTP API 에서 주로 사용된다.
  • 데이터 형식은 JSON,XML,TEXT 등 이 있고 주로 데이터 형식은 JSON이 사용된다.
  • POST,PUT,PATCH
  • InputStream을 사용하면 HTTP 메시지 바디의 데이터를 직접 읽을 수 있다.

API 메세지 바디 JSON 데이터를 처리할 서블릿

- API 메세지 바디 JSON 요청 데이터를 JSON 형식으로 파싱할 객체
@Getter @Setter
public class HelloData {
 private String username;
 private int age;
}


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

    private ObjectMapper objectMapper = new ObjectMapper();

    @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);

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

        resp.getWriter().write("ok");
    }    
}

JSON 데이터 형식

- POST http://localhost:8080/request-body-json
- content-type: application/json
- message body: {"username": "hello", "age": 20}

결과

messageBody = {"username": "hello","age": 20}
helloData.username = hello
helloData.age = 20

HTTP API JSON 형식으로 content-type: appplication/json 요청시에 서버에서는 JSON데이터를 파싱해 줄 필요가 있기 때문에 HelloData 객체가 파싱된 데이터를 입력받았다.
이 때 중요한 것은 서버 요청시 클라이언트에서 어떤 content-type으로 보내는지 확인하는 것이 중요하다.

HttpServletResponse는 뭘까?

HttpServletResponse의 역할은 뭘까?

  • HTTP 응답 메세지를 생성한다. HTTP 응답코드를 지정하고 헤더와 바디를 생성한다.
  • Content-Type,쿠키,Redirect

기본 사용법

@WebServlet(name = "responseHeaderServlet",urlPatterns = "/response-header")
public class ResponseHeaderServlet extends HttpServlet{
    
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        
        // [ 상태코드 ]
        resp.setStatus(HttpServletResponse.SC_BAD_GATEWAY);

        // [ 응답 헤더 ]
        resp.setHeader("Content-Type", "text.plain:charset=utf-8");
        resp.setHeader("Cache-Control", "no-cache,no-store,must-revalidate");
        resp.setHeader("Pragma", "no-cache");
        resp.setHeader("my-header", "hello");

        // [ 헤더 편의 메서드 ]
        content(resp); 
        cookie(resp);
        redirect(resp);
    }

    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");
        response.addCookie(cookie);
    }

    private void redirect(HttpServletResponse response) throws IOException{
        // 상태 코드 302
        // Location : /basic/hello-form.html
        response.sendRedirect("/basic/hello-form.html");
        
    }
}

브라우져에서 http://localhost8080/response-header로 요청 후
브라우져 검사를 열어서 네트워크 탭에서 header를 확인해보면 된다.

HTTP 응답 데이터 - 단순 텍스트,HTML

HTTP 응답 메세지는 주로 다음 내용을 담아서 전달한다.

  • 단순 텍스트 응답
  • HTML 응답
  • HTTP API - MessageBody JSON 응답
@WebServlet(name = "responsrHtmlServlet",urlPatterns = "/response-html")
public class ResponseHtmlServlet extends HttpServlet{


    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // Content-Type: text/html;charset=utf-8
        resp.setContentType("text/html");
        resp.setCharacterEncoding("utf-8");
        
        PrintWriter write = resp.getWriter();
        write.println("<html>");
        write.println("<body>");
        write.println("  <div>안녕?</div>");
        write.println("</body>");
        write.println("</html>");
    }
}

클라이언트에서 http://localhost8080/response-html 요청을 보내면 서버에서는 응답데이터에 위와 같은 데이터를 내려준다. 브라우저는 응답 받은 데이터를 화면에 보여준다.

HTTP 응답 데이터 - API JSON

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

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

        HelloData data = new HelloData();
        data.setUsername("kim");
        data.setAge(20);

        // {"username":"kim","age":20}
        String result = objectMapper.writeValueAsString(data);
        
        resp.getWriter().write(result);
    }
    
}

클라이언트에서 http://localhost:8080/response-json으로 요청하면 응답 헤더의 content-Type은 application/json 형식으로 CharacterEncoding은 utf-8로 그리고 메세지 바디로 받은 응답 데이터를 브라우져가 해석하여 화면에 보여준다.

정리

클라이언트가 요청한 데이터는 HttpServletRequest 객체가 받아서 처리하는 것을 배울 수 있었고, 서버에서 HttpServletResponse 객체를 통해 브라우져에 데어터를 응답값으로 보내는 것을 배울 수 있었다. 여기서 중요한 점은 클라이언트와 서버에서 content-type을 일치시켜야한다는 것이다. 형식이 일치하지 않으면 의도한대로 데이터를 보내거나 받을 수 없기 때문이다.

참고

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1/dashboard