관리 메뉴

나구리의 개발공부기록

서블릿, 프로젝트 생성, Hello서블릿, HttpServletRequest(개요/기본사용법), HTTP요청 데이터(개요/GET 쿼리 파라미터/POST HTML Form,API메세지바디(텍스트/JSON)), HttpServletResponse, HTTP 응답데이터(텍스트/HTML/API JSON) 본문

인프런 - 스프링 완전정복 코스 로드맵/스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술

서블릿, 프로젝트 생성, Hello서블릿, HttpServletRequest(개요/기본사용법), HTTP요청 데이터(개요/GET 쿼리 파라미터/POST HTML Form,API메세지바디(텍스트/JSON)), HttpServletResponse, HTTP 응답데이터(텍스트/HTML/API JSON)

소소한나구리 2024. 2. 13. 15:52

  출처 : 인프런 - 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 (유료) / 김영한님  
  유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용  

https://inf.run/Gmptq


1. 프로젝트 생성

1) 스프링 부트 스타터

(1) 프로젝트 설정

  • Gradle - Groovy
  • Java: 21
  • Spring Boot: 3.x.x

(2) Metadata

  • Group: hello
  • Artifact: servlet
  • Packaging : War (Jar가 아닌 War에 주의 - JSP를 실행하기 위함)

(3) Dependencies

  • Spring Web
  • Lombok

(2) Postman 설치


2. Hello 서블릿 

** 참고

  • 서블릿은 톰캣 같은 WAS를 직접 설치하고 그 위에 서블릿 코드를 클래스 파일로 빌드해서 올린 다음 톰캣 서버를 실행하면 됨
  • 그러나 해당 과정이 매우 번거로워서 스프링 부트로 프로젝트를 생성하여 톰캣 서버를 별도설치 없이 서블릿 코드를 실행

1) 스프링 부트 서블릿 환경 구성

(1) ServletApplication - 수정

  • 스프링 부트 애플리케이션인 ServletApplication에 애노테이션 적용
  • @ServletComponentScan: 스프링 부트가 지원하는 서블릿 자동등록 애노테이션
@ServletComponentScan // 서블릿 자동 등록
@SpringBootApplication
public class ServletApplication {

	public static void main(String[] args) {
		SpringApplication.run(ServletApplication.class, args);
	}
}

 

(2) HelloServlet

  • basic 패키지를 생성 후 실제 동작하는 서블릿 코드 등록
  • @WebServlet(name: 서블릿 이름, urlPatterns: URL 매핑)
  • HTTP 요청을 통해 매핑된 URL이 호출되면 서블릿 컨테이너가 오버라이딩 된 메서드를 실행
@WebServlet(name="helloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet { // 서블릿은 HttpServlet을 상속 받아야 함

    @Override // ctrl + o 로 메서드 오버라이딩(자물쇠 잠겨있는 메서드 = protected)
    protected void service(HttpServletRequest request,
                           HttpServletResponse response) throws ServletException, IOException {
        System.out.println("HelloServlet.service");
        System.out.println("request = " + request); // HttpServletRequest 구현체 확인
        System.out.println("response = " + response); // HttpServletResponse 구현체 확인

        String username = request.getParameter("username"); // 쿼리파라미터 조회(?username=guri)
        System.out.println("username = " + username);

        // 응답메세지 보내보기
        // HTTP 헤더 정보
        response.setContentType("text/plain"); // 콘텐츠타입
        response.setCharacterEncoding("utf-8"); // 문자인코딩(옛날시스템이 아니면 대부분 utf-8)

        // HTTP 바디 메세지
        response.getWriter().write("hello "+username);
    }
}

 

(3) HTTP 요청 메세지 로그 확인하기

  • ~/application.properties 경로에 아래 코드 입력 후 서버 재시작 및 재요청을 보내보면 HTTP 요청 메시지를 로그로 출력하는 것을 확인할 수 있음
  • 운영서버에 이렇게 모든 요청 정보를 다 남기면 성능저하가 발생할 수 있으므로 개발 단계에서만 적용할 것
logging.level.org.apache.coyote.http11=debug
  • 강의에 있는 HTML 코드는 생략

3. HttpServletRequest

1) 개요

(1) HttpServletRequest역할

  • HTTP요청 메시지를 개발자가 직접 파싱해서 사용해도 되지만 매우 불편함
  • 서블릿은 개발자가 HTTP요청 메시지를 편리하게 사용할 수 있도록 개발자 대신하여 HTTP 요청메시지를 파싱 후 그 결과를HttpServletRequest객체에 담아서 제공함

(2) HTTP 요청메시지

  • HttpServletRequest를 사용하면 아래의 HTTP 요청 메시지를 편리하게 조회할 수 있으며 추가로 여러가지 부가 기능도 함께 제공함
  • START LINE: HTTP메서드, URL, 쿼리 스트링, 스키마, 프로토콜
  • 헤더: 헤더 조회
  • 바디: form 파라미터 형식조회, message body 데이터 직접 조회
POST /save HTTP/1.1 //STARTLINE : HTTP메소드, URL, 스키마, 프로토콜, 쿼리스트링 정보
Host: localhost:8080 // 헤더 : 헤더정보(Host, 콘텐츠타입 등)
Content-Type: application/x-www-form-urlencoded

username=kim&age=20 // 바디 : form파라미터 형식 조회, message body데이터 직접 조회, JSON 형식 등

 

(3) HttpServletRequest객체의 여러 부가기능

  • 임시 저장소 기능 : 해당 HTTP 요청이 시작부터 끝날 때까지 유지되는 임시 저장소 기능
    - 저장 : request.setAttribute(name, value)
    - 조회 : request.getAttribute(name)
  • 세션 관리 기능
    - requst.getSession(create: true)

** 중요!

  • HttpServletRequest, HttpServletResponse를 사용할 때 가장 중요한 점은 이 객체들이 HTTP 요청 메시지, HTTP 응답 메시지를 편리하게 사용하도록 도와주는 객체라는 것임
  • 이 기능에 대해 깊이있는 이해를 하려면 HTTP스펙이 제공하는 요청,응답 메시지 자체를 이해해야함.

2) 기본 사용법

(1) RequestHeaderServlet

  • basic 하위에 request 패키지를 생성 후 작성
  • startline정보, 헤더정보, 기타 정보드를 확인하는 방법들

** 참고

  • 로컬에서 테스트를 하면 IPv6 정보가 나오는데 IPv4 정보를 보고 싶으면 -Djava.net.preferIPv4Stack=true 옵션을 VM options에 넣어주면 됨
package hello.servlet.basic.request;

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

        //Header 모든 정보
        printHeaders(request);

        //Header 편리한 조회
        printHeaderUtils(request);

        // 기타 정보 - HTTP 메세지의 정보가 아닌 네트워크의 커넥션 정보
        printEtc(request);
    }

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

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

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




4. HTTP요청 데이터

1) 개요

  • 주로 3가지 방법으로 데이터를 전달

(1) GET - 쿼리 파라미터

  • 메시지 바디 없이 URL의 쿼리 파라미터에 데이터를 포함해서 전달
  • 예)검색, 필터, 페이징등에서 많이 사용하는 방식
  • /url**?username=hello&age=20*** 의 형태

(2) POST - HTML Form

  • 메시지 바디에 쿼리 파라미터와 비슷한 형식으로 전달, username=hello&age=20
  • 조회하는 방법이 쿼리파라미터와 호환이 됨
  • 예)회원 가입, 상품 주문, HTML Form 사용
  • Content-Type : application/x-www-form-urlencoded

(3) HTTP message body에 데이터 직접 담아서 요청

  • HTTP API에서 주로 사용
  • 데이터형식은 주로 JSON을 사용하며 XML, TEXT도 가능
  • POST, PUT, PATCH 등 사용가능

2) GET 쿼리 파라미터

(1) 특징

  • 메시지 바디 없이 URL의 쿼리 파라미터를 사용해서 데이터를 전달
  • 예) 검색, 필터, 페이징등에서 많이 사용하는 방식
  • 예) http://localhost:8080/request-param?username=hello&age=20&username=hello2...

(2) 쿼리 파라미터 조회 메서드

String username = request.getParameter("username"); //단일 파라미터 조회
Enumeration<String> parameterNames = request.getParameterNames(); //파라미터 이름들 모두 조회
Map<String, String[]> parameterMap = request.getParameterMap(); //파라미터를 Map으로 조회
String[] usernames = request.getParameterValues("username"); //복수 파라미터 조회

 

(3) RequestParamServlet

  • 파라미터 이름은 하나이고 값이 중복인 경우 getParameter로 조회하면 첫번째 값만 반환하므로 getParameterValues로 조회
package hello.servlet.basic.request;

@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");
        // 모든 요청의 파라미터를 조회
        // Iterator 사용방식으로 조회 ParamName = key, request.getParameter(paramName) = value
        request.getParameterNames().asIterator()
                        .forEachRemaining(paramName -> System.out.println(
                                paramName+ "= " + request.getParameter(paramName)));
        System.out.println("[전체 파라미터 조회] - end");

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

3) HTTP요청 데이터 - POST HTML Form

(1) 특징

  • 주로 회원가입, 상품주문 등에서 사용하는 방식
  • 메시지 바디에 쿼리 파라미터 형식으로 데이터를 전달, username=hello&age=20
  • content-type: application /x-www-form-urlencoded

(2) HTML Form을 생성하여 테스트

  • src/main/webapp/basic/hello-form.html 생성
  • 작성된 html폼에 데이터를 입력하면 localhost:8080/request-param, POST로 데이터가 전달
<!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>

 

(3) POST HTML Form 전송

  • application /x-www-form-urlencoded 형식과 쿼리 파라미터 형식이 동일 하므로 쿼리 파라미터 조회 메서드를 그대로 사용 가능함
  • 클라이언트(웹 브라우저)입장에서는 두 방식에 차이가 있지만, 서버 입장에서는 둘의 형식이 동일하므로 request.getParameter()로 편리하게 구분없이 조회가 가능

** 참고

  • GET URL 쿼리 파라미터 형식으로 데이터 전달 시에는 메시지 바디가 없으므로 content-type이 없으나 POST HTML Form 형식으로 데이터를 전달하면 HTTP  메시지 바디에 데이터를 포함하여 전송하기 때문에 content-type을 꼭 지정 해야함

(4) Postman으로 테스트

  • 이렇게 간단한 테스트는 포스트맨을 활용하면 좋음
  • HTTP 메소드: POST
  • URL 작성: http://localhost:8080/request-param
  • Body -> x-www-form-urlencoded 선택하면 Herder에서 자동으로 content-type이 설정이 되지만 혹시 모르니 꼭 확인 후 전송할 것

postman으로 전송한 모습

4) HTTP 요청데이터 - API 메시지바디(단순 텍스트)

(1) 특징

  • HTTP message body에 데이터를 직접 담아서 요청
  • HTTP API에서 주로 사용하며 JSON, XML, TEXT 사용 - 주로 JSON사용(사실상 표준)
  • POST, PUT, PATCH 등 사용

(2) RequestBodyStringServlet

  • InputStream을 사용해서 HTTP 메시지 바디의 데이터를 읽을 수 있음
  • inputStream은 바이트코드를 반환
  • 바이트 코드를 스트링으로 변환하려면 Charset을 지정해 주어야함
package hello.servlet.basic.request;

@WebServlet(name = "requestBodyStringServlet", urlPatterns = "/request-body-string")
public class RequestBodyStringServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        // 메시디 바디의 내용을 바이트 코드로 얻음
        ServletInputStream inputStream = req.getInputStream();
        
        // StreamUtils: 스프링이 제공하는 유틸(바이트코드를 스트링으로 변환), 바이트코드를 변환시에는 항상 인코딩 정보를 알려주어야 함
        String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
        System.out.println("messageBody = " + messageBody);
        resp.getWriter().write("ok");
    }
}

 

(3) Postman으로 테스트

  • Postman세팅 : Body - raw - text 선택
  • 보내기전 Headers의 Content-Type이 text/plain으로 바뀌었는지 확인
  • 텍스트 입력 후 Send하면 ok의 응답 메시지가 뜨고, IDE의 로그에 입력한 메시지가 출력된 것을 확인할 수 있음

5) HTTP 요청데이터 - API 메시지바디(JSON)

(1) JSON 형식 데이터 전송

  • POST http://localhost:8080/request-body-json
  • content-type: application/json
  • message body: {"username": "hello", "age":20} 전송
  • 결과: message body = {"username": "hello", "age":20}

(2) HelloData

  • JSON 형식으로 파싱할 수 있도록 객체 생성
  • lombok라이브러리를 사용하여 간단히 애노테이션으로 Getter, Setter를 생성
package hello.servlet.basic;

@Getter @Setter //lombok 라이브러리 사용으로 Getter Setter 자동으로 생성
public class HelloData {
    private String username;
    private int age;
}

 

(3) RequestBodyJsonServlet

  • 스프링 부트가 제공하는 Jackson 라이브러리인 ObjectMapper를 생성하여 messageBody에 담긴 JSON문자열을 HelloData의 형식으로 파싱
package hello.servlet.basic.request;

import java.nio.charset.StandardCharsets;

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

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

 

(3) Postman 테스트

  • POST / http://localhost:8080/request-body-json으로 메시지 바디에 JSON형태로 전송하면 ok응답과 함께 IDE의 로그에 전송한 데이터가 출력되는 것을 확인할 수 있음

postman으로 요청

** 참고

  • JSON결과를 파싱해서 사용하기위해 자바객체로 변환하려면 Jackson, Gson 같은 라이브러리를 추가해서 사용해야함
  • 스프링부트로 Spring MVC를 선택하면 기본 Jackson 라이브러리 ObjectMapper가 제공됨
  • HTML Form의 데이터도 메시지 바디를 통해 전송되므로 이렇게 직접 읽을 수 있음
  • 그러나 편리한 파라미터 조회 기능(request.getParameter(...))을 이미 제공하기 때문에 제공된 기능을 사용하는 것이 편리함 

5. HttpServletResponse - 기본사용법

1) HttpServletResponse의 역할

(1) HTTP 응답 메시지 생성

  • HTTP 응답코드 지정
  • 헤더 생성
  • 바디 생성

(2) 편의 기능 제공

  • Content-Type, 쿠키, Redirect

(3) ResponseHeaderServlet

  • response 패키지를 생성 후 작성
  • 작성 후 애플리케이션을 실행하여 매핑된 url로 접속해보면 /basic/gello-form.html로 리다이렉트 되고, 개발자 도구에서 네트워크 상태를 보면 response-header에 응답정보가 있는 것을 알 수 있음
package hello.servlet.basic.response;

@WebServlet(name = "responseHeaderServlet", urlPatterns = "/response-header")
public class ResponseHeaderServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest request,
                           HttpServletResponse response) throws ServletException, IOException {
        //[status-line]
        response.setStatus(HttpServletResponse.SC_OK); // 200

        //[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"); // 임의의 헤더를 생성

        //[message-body]
        PrintWriter writer = response.getWriter();
        writer.println("성공");

        //[Header 편의 메서드]
        content(response);
        cookie(response);
        redirect(response);
    }

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

    private static void cookie(HttpServletResponse response) {
        Cookie cookie = new Cookie("myCookie", "good");
        cookie.setMaxAge(600);
        response.addCookie(cookie);
    }

    private static void redirect(HttpServletResponse response) throws IOException {
        response.sendRedirect("/basic/hello-form.html");
    }
}

6. HTTP 응답데이터

1) 단순 텍스트,HTML

(1) 단순 텍스트 응답

  • writer.println("ok"); 기존 테스트로  응답 메세지 작성했던 방식

(2) HTML응답

  • HTTP API - messageBody JSON응답 

(3) ResponseHtmlServlet

  • 코드를 작성 후 매핑된 url로 접속하면 작성한 html이 렌더링 되어 화면에 출력된 모습을 확인할 수 있음
  • 페이지 소스 보기를 사용하면 결과 HTML도 확인할 수 있음
package hello.servlet.basic.response;

@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>안녕하세요</div>");
        writer.println("</body>");
        writer.println("</html>");
    }
}

2) HTTP 응답데이터 - API JSON

(1) ResponseJsonServlet

  • HTTP응답으로 JSON을 반환할 때는 conten-type을 application/json으로 지정해야함
  • Jackson라이브러리가 제공하는 objectMapper.writeValueAsString()메서드를 활용하여 객체를 JSON문자로 변경할 수 있음
  • 실행 후 매핑된 url로 접속하면 {"username":"kim","age":38}과 같이 JSON 형태로 응답값이 출력됨
package hello.servlet.basic.response;

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

    ObjectMapper objectMapper = new ObjectMapper();

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

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

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

 

** 참고

  • application/json은 스펙상 utf-8 형식을 사용하도록 정의되어 있어서 charset=utf-8과 같은 추가 파라미터를 지원하지 않기때문에 application/json만 입력해도 됨
  • response.getWriter()를 사용하면 추가파라미터를 자동으로 추가하는데 이럴때는 response.getOutputStream()으로 출력하면 추가 파라미터 없이 출력됨