Spring

[인프런/스프링 MVC 1편] 2. 서블릿

주니어주니 2023. 4. 26. 19:14

 

 

서블릿

 

  • 동적 웹페이지를 만들 때 사용되는 자바 기반의 웹 애플리케이션 프로그래밍 기술
  • 웹 요청과 응답의 흐름을 간단한 메소드 호출만으로 체계적으로 다룰 수 있게 해줌
  • 서버에서 실행되다가 웹 브라우저에서 요청을 하면 해당 기능을 수행한 후 웹 브라우저에 결과 전송
  • 기존의 정적 웹 프로그램의 문제점을 보완하여 동적인 여러가지 기능 제공
  • MVC 패턴에서 컨트롤러로 이용

 

 

 

 

 

* 스프링 부트 - 톰캣 서버 내장 -> 웹 애플리케이션 설치 없이 서블릿 코드 실행 가능

 

 

 

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

 

 

- hello.servlet.ServletApplication 

 

  • @ServeltComponentScan 추가: 서블릿 자동 등록

 

 

- hello.servlet.basic.HelloServlet 

 

  • HelloServlet extends HttpServlet
    : HelloServlet 클래스는 HttpServlet을 상속
  • @WebServlet(name = "helloServlet", urlPatterns = "/hello")
    : 서블릿 어노테이션(name: 서블릿 이름, urlPatterns: URL 매핑)
  • ctrl + o -> 키 모양 service 메소드
    : HTTP 요청을 통해 매핑된 URL이 호출되면 서블릿 컨테이너가 service 메소드 실행

 

 

 

 

 

 

service 메소드 내

1) HelloServlet.service 출력

2) request 객체, response 객체 출력 -> WAS에서 구현하는 Request, Response 인터페이스 구현체

3) 쿼리파라미터 출력

 

-> 실행(run)

 

-> localhost:8080/hello

-> localhost:8080/hello?username=kim (쿼리파라미터 입력 -> request 요청 객체)

 

-> 브라우저 빈 창 확인 (응답 보낸게 없으니까)

 

-> 콘솔창에서 출력한 내용들 확인

 

 

 

 

 

response 객체에 담기

ContentType, CharacterEncoding 설정 -> 응답객체의 헤더에 들어감

getWriter().write("내용") -> 응답메시지

 

-> 실행(run)

 

 

 

 

 

설정했던 ContentType, CharacterEncoding -> 응답 헤더

 

 


 

 

HTTP 요청 메시지 로그로 확인하기

 

- application.properties

 

logging.level.org.apache.coyote.http11=debug

 

 

-> 서버 다시 실행

 

 

서버가 받은 HTTP 요청 메시지 출력

- 인코딩된 것, 요청 메시지 정보 

(나머지는 WAS가 자동으로 생성해줌)

 

 


 

서블릿 컨테이너 동작 방식 

 

 

 

스프링부트 -> 내장 톰캣 서버 (WAS)의 서블릿 컨테이너가 서블릿 객체 생성 

-> HTTP 요청 -> service 메소드 실행하면서 request, response 객체 전달

(메소드 실행하면 쓰레드가 서블릿 객체 호출, request, response 객체를 서블릿에 전달)

(서블릿 컨테이너에서 HTTP 요청 메시지 기반으로 request 객체 생성, response 객체 생성) 

-> 서블릿이 종료되면서 response 객체에 HTTP 응답 정보 담아서 반환 

 

 


 

Welcome 페이지 추가 

(그냥 간편히 이동하기 위해 만들어 둔 것) 

 

* webapp 경로에 index.html 을 두면 http://localhost:8080 호출시 index.html 페이지가 열린다.

 

 

 

 


 

 

HttpServletRequest 개요

  • HttpServletRequest 역할
    : 서블릿은 개발자가 HTTP 요청 메시지를 편리하게 사용할 수 있도록 개발자 대신에 HTTP 요청 메시지를 파싱함. 그리고 그 결과를 HttpServletRequest 객체에 담아서 반환
  • HttpServletRequest의 부가기능
    • 임시 저장소 기능 (해당 HTTP 요청 시작부터 끝까지 유지되는 임시 저장소 기능)
      - 저장 : request.setAttribute(name, value)
      - 조회: request.getAttribute(name)
    • 세션 관리 기능 
      request.getSession(create: true) 

 

 

HttpServletRequest 기본 사용법

 

RequestHeaderServlet 클래스

서블릿 생성 -> 메소드 호출

 

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

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

 

 

http://localhost:8080/request-header 호출 

 

 

 

 

1) HttpServletRequest의 start line 정보

 

// start-line
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
    System.out.println("request.getRequestURL() = " + request.getRequestURL());     // http://localhost:8080/request-header
    System.out.println("request.getRequestURI() = " + request.getRequestURI());     // /request-header
    System.out.println("request.getQueryString() = " + request.getQueryString());   // username=hi
    System.out.println("request.isSecure() = " + request.isSecure());               // https 사용 유무
    System.out.println("--- REQUEST-LINE - end ---");
    System.out.println();
}

 

 

 

 

2) HttpServletRequest의 Header 정보

 

// 헤더 정보
private 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();
}

 

 

 

 

3) HttpServletRequest의 Header 편리한 조회 

 

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

 

 

 

 

4) HttpServletRequest의 기타 정보 제공

 

//기타 정보
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 요청 데이터

 

  1. GET - 쿼리 파라미터
    • /url?usename=hello&age=20
    • 메시지 바디 X , URL의 쿼리 파라미터에 데이터 포함하여 전달
    • 예) 검색, 필터, 페이징 등
  2. POST - HTML Form
    • content-type: application/x-www-form-urlencoded
    • 메시지 바디에 쿼리 파라미터 형식으로 전달 username=hello&age=20
    • 예) 회원가입, 상품주문, HTML Form 사용
  3. HTTP message body에 데이터를 직접 담아서 요청
    • HTTP API(REST API)에서 주로 사용, JSON, XML, TEXT

 

 

 

1. HTTP 요청 데이터 - GET 쿼리 파라미터

  • http://localhost:8080/request-param?username=hello&age=20&username=hello2
  • 서버에서 HttpServletRequest가 제공하는 다음 메소드를 통해 쿼리 파라미터를 편리하게 조회할 수 있다.
  • 쿼리 파라미터 조회 메소드
String username = request.getParameter("username"); 			// 단일 파라미터 조회(가장 많이 씀)
Enumeration<String> parameterNames = request.getParameterNames(); 	// 파라미터 이름들
모두 조회 (옛날 방식)
request.getParameterNames().asIterator()				// 파라미터 이름들 모두 조회 (요즘 방식) 
                .forEachRemaining(paramName -> System.out.println(paramName + "=" + request.getParameter(paramName)));

Map<String, String[]> parameterMap = request.getParameterMap(); 	// 파라미터를 Map
으로 조회
String[] usernames = request.getParameterValues("username"); 		// 복수 파라미터 조회

 

 

* RequestParamServlet 

 

package hello.servlet.basic.request;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.Enumeration;

/**
 * 1. 파라미터 전송 기능
 * http://localhost:8080/request-param?username=hello&age=20
 * 2. 동일한 파라미터 전송 기능
 * http://localhost:8080/request-param?username=hello&age=20&username=hello2
 */
@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("[단일 파라미터 조회]");
        String username = request.getParameter("username");
        String age = request.getParameter("age");

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

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

 

파라미터 전송

 

http://localhost:8080/request-param?username=hello&age=20&username=hello2

 

 

결과

 

 

 

복수 파라미터에서 단일 파라미터 조회

 

파라미터 이름은 username으로 하나인데, 값이 중복이면? request.getParameterValues() 사용

중복일 때 request.getParameter()를 사용하면 복수 파라미터 값 중 첫번째 값 반환

 

 

 

2. HTTP 요청 데이터 - POST HTML Form

 

  • 메시지 바디쿼리 파라미터 형식으로 데이터 전달 (username=hello&age=20)
  • content-type : application/x-www-form-urlencoded
    • content-type은 HTTP 메시지 바디의 데이터 형식 지정
    • GET URL 쿼리 파라미터 형식으로 클라이언트에서 서버로 데이터 전달할 때는 HTTP 메시지 바디를 사용하지 않기 때문에 content-type 없음 !
    • POST HTML Form 형식으로 데이터 전달하면 HTTP 메시지 바디에 해당 데이터를 포함해서 보내기 때문에 바디에 포함된 데이터가 어떤 형식인지 content-type 꼭 지정!
    • 이렇게 폼으로 데이터를 전송하는 형식application/x-www-form-urlencoded라고 함! 

 

* 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>

 

* action = "/request-param" ---> 전에 만든 서블릿으로 전송

 

 

실행

 

 

값 입력 후 전송

 

웹 브라우저는 다음 형식으로 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에서의 쿼리 파라미터 형식과 같음 (username=kim&age=20)
따라서 쿼리 파라미터 조회 메소드 그대로 사용! (request.getParameter()) 
-> 클라이언트 입장에서는 두 방식에 차이가 있지만, 서버 입장에서는 둘의 형식이 동일하므로,
request.getParameer()로 편리하게 구분없이 조회

request.getParameter() - GET URL 쿼리 파라미터 형식도 지원, POST HTML Form 형식도 지원

 

 

Postman을 사용한 테스트

 

HTML Form 만들지 않고 간단한 테스트

 

 

Body -> x-www-form-urlencoded 선택

 

 

Headers -> Content-Type : application/x-www-form-urlencoded 확인

 

 

Send 후 결과 확인

 

 

 

 

3. HTTP 요청 데이터 - API 메시지 바디 

  • HTML 을 통한 데이터 전송이 아닌, HTTP message body에 데이터 직접 담아서 요청
  • HTTP API (REST API)에서 주로 사용, JSON, XML, TEXT
  • 데이터 형식은 주로 JSON 사용 
  • POST, PUT, PATCH 방식에서 주로 쓰임

 

1) 단순 텍스트

  • 단순한 텍스트 메시지를 HTTP 메시지 바디에 담아서 전송하고 읽어보기
  • HTTP 메시지 바디의 데이터를 InputStream을 사용해서 직접 읽을 수 있음

 

package hello.servlet.basic.request;

import jakarta.servlet.ServletException;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.util.StreamUtils;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

@WebServlet(name = "requestBodyStringServlet", urlPatterns = "/request-body-string")
public class RequestBodyStringServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // inputStream : byte 코드 반환
        ServletInputStream inputStream = request.getInputStream();
        // byte를 문자로 변환
        String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);

        System.out.println("messageBody = " + messageBody);

        // 응답화면에 띄울 내용
        response.getWriter().write("ok");
    }
}

 

 

Postman 으로 테스트

 

 

텍스트 : raw -> Text -> 텍스트 입력 (-> Body에 직접 입력)

 

 

 

Content-Type : text/plain 확인

 

 

 

Send 후 Body 응답 ok 확인 -> 콘솔창 text 확인 

 

 

 

결과 

POST http://localhost:8080/request-body-string 

content-type : text/plain

message body : hello!

출력결과 : messageBody = hello!

 

 

 

2) JSON

 

JSON 형식으로 전송

POST http://localhost:8080/request-body-json

content-type : application/json

message body : {"username""hello""age"20}

결과 : messageBody = {"username""hello""age"20}

 

 

JSON 형식으로 파싱할 수 있도록 객체 생성 

 

package hello.servlet.basic;

import lombok.Getter;
import lombok.Setter;

@Getter @Setter
public class HelloData {
    
    private String username;
    private int age;

}

 

 

package hello.servlet.basic.request;

import com.fasterxml.jackson.databind.ObjectMapper;
import hello.servlet.basic.HelloData;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.util.StreamUtils;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

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

    // JSON <-> 자바 객체로 변환해주는 스프링부트의 Jackson 라이브러리
    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);

        // JSON -> Java (messageBody로 받은 JSON 데이터를 HelloDao 객체에 파싱해서 넣음)
        HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);

        System.out.println("helloData.username = " + helloData.getUsername());
        System.out.println("helloData.age = " + helloData.getAge());

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

 

 

Postman으로 실행

 

 

메시지 바디에 직접 JSON 데이터 입력

 

 

 

 

출력결과 

 

 

 


 

HttpServletResponse 기본 사용법

 

  • HTTP 응답 메시지 생성
    • HTTP 응답코드 지정
    • 헤더 생성
    • 바디 생성
  • 편의 기능 제공
    • Content-Type, 쿠키, Redirect

 

 

package hello.servlet.basic.response;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

@WebServlet(name = "responseHeaderServlet", urlPatterns = "/response-header")
public class ResponseHeaderServlet extends HttpServlet {

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // [status-line] : HTTP 응답코드 지정 (SC_OK : 200)
        response.setStatus(HttpServletResponse.SC_OK);

        // [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");   // 사용자정의 헤더

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

        // [message body]
        PrintWriter writer = response.getWriter();
        writer.println("ok");
    }
    
    // Content 편의 메소드
    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");
        cookie.setMaxAge(600); //600초
        response.addCookie(cookie);
    }
    
    // redirect 편의 메소드
    private void redirect(HttpServletResponse response) throws IOException {
        //Status Code 302
        //Location: /basic/hello-form.html

        //response.setStatus(HttpServletResponse.SC_FOUND); //302
        //response.setHeader("Location", "/basic/hello-form.html");

        response.sendRedirect("/basic/hello-form.html");    // 위 두줄을 이 한줄로 가능
    }
}

 

 

- 응답 Content 생성 (응답코드, 헤더 등) / 편의 메소드 적용

 

 

 

- 쿠키 편의 메소드 적용

 

 

 

- redirect 편의 메소드 적용

 

 

 


 

HTTP 응답 데이터

  • 단순 텍스트 응답
    • 앞에서 살펴봄 ( writer.println("ok");
  • HTML 응답
  • HTTP API - MessageBody JSON 응답

 

1) HttpServletResponse - HTML 응답

  • HTTP 응답으로 HTML을 반환할 때는 content-type을 "text/html"로 지정해야 함 !

 

package hello.servlet.basic.response;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

@WebServlet(name = "responseHtmlServlet", urlPatterns = "/response-html")
public class ResponseHtmlServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // HTTP 응답으로 HTML을 반환할 때는 Content-Type을 text/html로 지정해야 함
        response.setContentType("text/html");
        // 인코딩을 utf-8로 해줘야 한글 안깨짐
        response.setCharacterEncoding("utf-8");

        PrintWriter writer = response.getWriter();
        writer.println("<html>");
        writer.println("<body>");
        writer.println("    <div>안녕?</div>");
        writer.println("</body>");
        writer.println("</html>");
    }
}

 

 

 

 

-> HTML 응답

 

 

 

2) HTTPServletResponse - API JSON 응답

  • HTTP 응답으로 JSON을 반환할 때는 Content-Type을 application/json으로 지정해야 함 

 

package hello.servlet.basic.response;

import com.fasterxml.jackson.databind.ObjectMapper;
import hello.servlet.basic.HelloData;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

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

    // JSON <-> 자바 객체 변환하는 Jackson 라이브러리
    private ObjectMapper objectMapper = new ObjectMapper();

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // HTTP 응답으로 JSON을 반환할 때는 Content-Type을 application/json으로 지정해야 함
        response.setContentType("application/json");
        response.setCharacterEncoding("utf-8");

        // 자바 객체 -> JSON 데이터
        HelloData helloData = new HelloData();
        helloData.setUsername("kim");
        helloData.setAge(20);

        // 자바 객체에 담긴 내용을 JSON 데이터로 파싱 -> {"username":"kim", "age":20}
        String result = objectMapper.writeValueAsString(helloData);
        response.getWriter().write(result);
    }
}

 

 

-> JSON 응답