본문 바로가기
공부/springboot

SpringMVC(2) : 서블릿 요청 처리

by 샤샤샤샤 2023. 12. 8.

서블릿은  HTTP 통신을 더 쉽게 사용하기 위해 만들어진 자바 기능이다. 따라서 이를 이용한 HttpServletRequest 와 HttpServletResponse 역시 정보를 더 쉽게 추출해내거나 삽입하는 기능을 가지고 있다. 이 게시글에서는 HttpServletRequest 를 이용해 HTTP 요청 메시지의 내용을 꺼내는 다양한 방법과 HttpServletResponse를 이용해 다양한 방식으로 응답을 보내고 쿠키 등의 부가 설정을 하는 방법을 살펴볼 것이다.

 

HTTP 메시지의 구조

HTTP통신을 다루는 기술인만큼 HTTP 통신의 기본 규약을 알아야할 필요가 있다. 이 게시글에서는 오직 http 메시지의 구조만 잠깐 흩고 지나갈것이다. http 통신에 대한 더 깊은 이해를 바라면 이전 게시글을 참조하길 바란다.

출처: 인프런 김영한님

start line : 시작 라인.

               요청 : 통신 방식, 경로와 쿼리 파라미터, 통신 프로토콜로  이뤄진다.

               응답 : 통신 프로토콜, 상태코드, 이유 문구로 이뤄진다.

header : http 메시지에 대한 모든 정보가 기록된다. 메시지 바디의 타입, 길이, 압축 방식 등 수많은 정보가 기록될수 있으며,  요청의 경우 server의 주소 정보가 적힌다. 응답의 경우에는 내려준 정보를 쿠키로 설정할지, 다른 url로 페이지를 이동시킬지 등을 정할수 있다. get방식의 요청의 경우 header에 정보가 담긴다.

body : 실질적인 정보가 담기는 공간. 헤더 다음에 한줄 공백을 만들고 작성한다. post 방식의 요청을 보내면 정보가 get 방식과 같은 형태로 body에 담기며, 이외에도 다양한 정보가 담길수 있다. 메시지 성격에 따라 body가 아예 없을수도 있다.

 

 

HTTP 요청 처리 (HttpServletRequest)

본론에 들어가기에 앞서 서블릿을 사용하기 위한 공통 코드를 살펴보자.

@WebServlet(name = "서블릿 이름", urlPatterns = "이 서블릿이 호출될 url")
public class RequestHeaderServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
       서블릿이 호출될때 실행될 코드
    }

@WebServlet : 어떤 url에 접근할때 서버는 서블릿 컨테이너에 저장된 여러개의 서블릿중 어떤 객체를 사용해야 하는지 모른다. 이 어노테이션은 이를 서버에 지정해주는 기능을 하는 스프링 어노테이션이다.

HttpServlet : 실질적인 서블릿 구현체. 서블릿을 사용하고자 하면 이를 상속받은 다음, 해당 서블릿이 호출될때 실행되야 하는 코드를 작성하면 된다.

service : 이 메서드는 접근 제어자가 public 인 것과 protected 인 두개가 존재하는데, public의 경우 파라미터가 HttpServletResponse/Request 가 아닌 ServleResponse/Request 이기 때문에 각종 정보를 추출해내는 메서드를 사용할수 없다.

 

start-line 의 정보를 추출하는 코드는 다음과 같다.

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

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

 

 

header 정보를 추출하는 방법은 다음과 같다.

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

    private static void printHeader(HttpServletRequest request) {
        System.out.println("--- Headers - 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("--- Headers - end ---");
        System.out.println();
    }
 }

주석 안의 내용과 밖의 내용은 모두 같은 일을 한다. 주석 아래 코드는 자바 9부터 지원하는 asIterator()  함수를 사용해서 header 의 모든 내용을 조회한다.

 

특정 header 만 조회하는 메소드는 아래와 같다.

@WebServlet(name = "requestHeaderServlet", urlPatterns = "/request-header")
public class RequestHeaderServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        printHeaderUtils(request);
    }
    
    //Header 편리한 조회
    private void printHeaderUtils(HttpServletRequest request) {
        System.out.println("--- Header 편의 조회 start ---");
        System.out.println("[Host 편의 조회]");
        System.out.println("request.getServerName() = " +
                request.getServerName()); //Host 헤더
        System.out.println("request.getServerPort() = " +
                request.getServerPort()); //Host 헤더
        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();
    }
 }

 

 

기타 header 정보 조회 방법은 다음과 같다.

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

    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 메시지 문자열 파싱 기능하여 사용자가 알아보고 쉽게 보여주는 역할을 한다. 모두 외우기 보다는 그때그때 필요한 메서드를 사용하면 된다.

 

GET/POST 요청 파라미터 받기

GET과 POST 로 보내는 데이터는 담기는 공간이 다르지만(Get는 url, Post 는 body), 결국 둘 모두 똑같은 쿼리 파라미터 형태로 데이터를 가공해 전송한다. 따라서 서버에서 파싱하는 방법 역시 똑같다.

@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("RequestParamServlet.service");

        System.out.println("[전체 파라미터 조회] - start");
        request.getParameterNames().asIterator()
                        .forEachRemaining(
                        paramName -> System.out.println(paramName + " = " + request.getParameter(paramName)));
        request.getParameterNames();
        System.out.println("[전체 파라미터 조회] - end");
        System.out.println();

        System.out.println("[단일 파라미터 조회]");
        String username = request.getParameter("username");
        String age = request.getParameter("age");

        System.out.println("username = " + username);
        System.out.println("age = " + age);
        System.out.println();
    }
}

파라미터 역시 조회 메서드가 존재한다. 전체 조회는 asIterator 를 통해 해결 가능하다.

그러나 만약 클라이언트가 같은 이름의 파라미터를 여러개 보낸다면, 그때는 getPrameter() 함수로 얻을수 있는 값은 첫번째 등록된 값 뿐이고 나중에 등록된 파라미터는 옳바르게 파싱하지 못한다.

그런 경우에는 아래와 같은 코드를 사용하면 된다.

@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("RequestParamServlet.service");

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

같은 이름의 파라미터가 여러개일때는 getPrameterValues() 함수를 사용하면 배열 형태로 값을 가져올수 있다.

 

Body에 담긴 데이터 받기

@WebServlet(name = "requestBodyServlet", 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);
    }
}

자바에서 Http메시지 Body 의 내용을 받기 위해서는 우선적으로 Byte 로 변환해 받아야 하는데(이미지, 동영상, 텍스트 등 다양한 형태의 데이터를 받기 위해서), inputStream() 함수를 사용하면 자동적으로 Byte 단위로 메시지가 변환된다.

만약 문자열 데이터라면 자바에서 제공하는 StreamUtils의 copyToString([문자열로 전환할 내용], [인코딩 방식]) 함수를 사용하면 된다.

 

JSON 데이터 받기

HelloData 클래스

@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 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.getUsername() = " + helloData.getUsername());
        System.out.println("helloData.getAge() = " + helloData.getAge());
    }
}

json 은 단지 문자열을 보기 좋게 정리한 하나의 약속일뿐, 특정한 데이터 타입이 아니다.

따라서 문자열 받는 것과 똑같은 방식으로 처리가 가능하나, 이 방식으로 처리하고 끝나면 재가공이 필요한 단점이 존재한다. 이를 해결하기 위해 jackson 라이브러리의 ObjectMapper 클래스 사용이 가능하다. 이 클래스를 사용하면 문자열로 파싱된 json 데이터를, 다시 파싱하여 json데이터를 이용해 지정된 클래스의 객체를 생성한다. 이때 데이터 클래스 필드는 데이터 타입과 이름이 json데이터와 같아야 하며, setter 함수가 존재해야 한다.