framework/spring

스프링 MVC 1 - 서블릿

wooweee 2023. 11. 4. 11:29
728x90

프로젝트 생성 tip

  1. Dependencies: Spring web, Lombok
  2. War
    • 외장 톰컷 서버 별도 설치 및 jsp 사용시 선택
  3. Gradle : Gradle -> IntelliJ IDEA
  4. Lombok
    • plugin -> lombok 설치 -> 재시작
    • Annotation Processors -> Enable annotation processing 체크 후 재시작

 

1. 서블릿 기본 사용법

  • 레거시 : tomcat(=sevlet was) 직접 설치 -> 서블릿 코드를 class file로 빌드
  • 스프링부트: 톰캣 서버 내장(embedded) -> 서블릿 코드만 실행
  • 서블릿은 스프링관 관련이 없다. 스프링은 서블릿을 더 편하게 사용할 수 있도록 스프링 web mvc를 가지고 있을 뿐이다.

 

1.1. @ServletComponentScan 등록

  • @ServletCompenentScan 등록: 스프링 부트에서 서블릿 사용할 수 있도록 함
  • servlet은 인터페이스로 spring뿐만 아니라 다른 web 관련된 것들이 해당 인터페이스를 구현 해놓았다.
  • 그래서 servlet은 공통으로 사용이 가능하다.
@ServletComponentScan
@SpringBootApplication
public class ServletApplication{
	public static void main(String[] args){
    	SpringApplication.run(ServletApplication.class, args);
    }
}

 

1.2. 서블릿 등록하기

# application.properties
// 실제 서버가 받는 http 요청 메시지를 출력해준다.
// 추가설정의 debug는 개발단계에서만 적용 - 성능저하 발생 때문
logging.level.org.apache.coyote.http11=debug
// servlet 등록 : @WebServlet
// name과 urlPatterns는 이름이 동일하면 안된다.
// name: servlet container에 bean으로 등록시 bean명
// urlPatterns: 해당 url로 들어올 시, 해당 servlet container에 등록된 bean 동작
@WebServlet(name = "helloServlet", urlPatterns = "/hello") 
public class HelloServlet extends HttpServlet {
    @Override
    // protected service method를 오버라이딩 해야함
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        System.out.println("request = " + request); // 요청 data 객체 주소
        System.out.println("response = " + response); // 미리 만든 응답 data 객체 주소

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

        // http 응답에 보낼 header 정보
        response.setContentType("text/plain");
        response.setCharacterEncoding("utf-8"); // 현대는 utf-8을 꼭 넣어주기
        // http 응답에 보낼 message = body 정보
        response.getWriter().write("hello " + username);
    }
}

 

출력 결과

 

1.3. 정리

  • @WebServlet: http 파싱을 대신해줌
    • (name과 urlPatterns 값이 겹치면 error 발생)
    • name = "helloServlet" : 싱글톤 서블릿 객체 이름
    • urlPatterns = "/hello" : hello url 요청이 올때 해당 서블릿객체 작동

  • extends HttpServlet: 서블릿은 이 인터페이스를 상속받아야 사용 가능

  • request: client webBrowser가 만든 http 요청을 기반으로 생성한 객체
  • response: 일단 미리 만든 http 응답 객체, 개발자가 추가할 정보를 추가할 수 있다.

  • response.getWriter().write("hello")가 실제 web browser UI에 나타나는 이유
더보기

    browser에서 http 응답을 해석할 때 렌더링 엔진 등의 자체 내장하고 있는 기능을 통해서 html, text, json은 그냥 출력해준다.

      * 웹 렌더링: 실시간으로 웹사이트가 그려지는 과정

      * 웹 2가지 엔진(크게 2가지로 구성): 1) 렌더링 엔진 2) JS 엔진 

 

1.4. 서블릿 컨테이너 동작 방식

  1. spring boot가 내장 톰켓 서버 실행
    • @ServletComponentScan를 이용해서 servlet class(@WebServlet class)를 서블릿 컨테이너에 bean으로 등록
  2. http 요청 메시지를 기반으로 request 객체 생성, 이때 response 객체도 생성 = prototype bean

  3. 서블릿 객체 실행하고 실행이 종료되면 실행과정에서 생성한 response data를 response 객체에 반환
    • 해당 servlet Bean code에는 request, response 파싱을 지원해주는 기능들이 존재 extends HttpServlet
  4. client web browser에게 http 응답 메시지 보냄

 

  • 서버에서 만드는 html page
    • webapp 디렉토리 생성 후 안에 index.html 생성하면 localhost:8080(최상단 root) 호출시 해당 html 페이지 열림
    • spring 레거시 일 때 webapp directory 사용
  •  webapp 디렉토리 구성
    1. static html : index.html(/) 
    2. jsp : 동적 html
    3. WEB-INF - views : controller를 통해서만 접근이 가능한 jsp
  • 참고
    • html form - input의 name property :  queryParameter의 key의 역할을 수행

 

2. HttpServletRequest

2.1. httpServletRequest 역할

  • 개요
    • http 요청 메시지를 개발자가 직접 파싱하지 않고 서블릿이 대신 파싱해서 개발자가 http 요청 메시지를 편리하게 사용
    • 파싱한 결과를 HttpServletRequest request 객체에 담아서 제공

  • http request
    • start line : (http method, url, query,schema, protocol)
    • header : 헤더 정보들
    • body
      1. form 형식 조회 - queryParameter
      2. message body 데이터 직접 조회 -json

 

  • 부가기능
    1. 임시저장소
      • request.setAttribute(name, value)
      • request.getAttribute(name)
    2. 세션관리
      • request.getSession(create: true)

 

2.2. 기본 사용법 - start-line, header 정보 조회 방법

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 {
        printStartLine(request);
        printHeaders(request);
        printHeaderUtils(request);
        printEtc(request);
    }

    // start-line 정보
    private void printStartLine(HttpServletRequest request) {
        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.getQueryString() = " + request.getQueryString());
        System.out.println("request.isSecure() = " + request.isSecure());
    }

    // Header 모든 정보
    private void printHeaders(HttpServletRequest request) {
    /* 과거 방법
        Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String headerName = headerNames.nextElement();
            System.out.println("headerName = " + headerName);
            System.out.println("request.getHeader(headerName) = " + request.getHeader(headerName));
        }
     */
        request.getHeaderNames().asIterator()
        		.forEachRemaining(headerName -> System.out.println(headerName + ": " + request.getHeader(headerName)));
    }

    // Header 편리한 조회
    private void printHeaderUtils(HttpServletRequest request) {
        System.out.println("[Host 편의 조회]");
        System.out.println("request.getServerName() = " + request.getServerName());
        System.out.println("request.getServerPort() = " + request.getServerPort());

        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("[cookie 편의 조회]");
        if (request.getCookies() != null) {
            for (Cookie cookie : request.getCookies()) {
                System.out.println(cookie.getName() + ": " + cookie.getValue());
            }
        }

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

    }

    // 기타 조회
    private void printEtc(HttpServletRequest request) {
        System.out.println("[Remote 정보]"); // client 요청이 온것에 관한 정보
        System.out.println("request.getRemoteHost() = " + request.getRemoteHost());
        System.out.println("request.getRemoteAddr() = " + request.getRemoteAddr());
        System.out.println("request.getRemotePort() = " + request.getRemotePort());

        System.out.println("[Local 정보]"); // 나의 server에 관한 정보
        System.out.println("request.getLocalName() = " + request.getLocalName());
        System.out.println("request.getLocalAddr() = " + request.getLocalAddr());
        System.out.println("request.getLocalPort() = " + request.getLocalPort());
    }
}

    

 

3. http 요청 데이터 - 이젠 껍데기 정보가 아니라 client가 직접 보낸 요청 data 추출 방법

요청 방식 요청 header 전송 형태 data 위치 형태
get query / get form params http start-line query
post form application/x-www-form-urlencoded  http body query
api text text/plain http body string
api json application/json http body json

3.1. summary - 3가지

  • 1, 2 :  queary params 형식이여서 둘 다 queary params 조회 메서드를 동일하게 사용 - request.getParameter();
  1. GET - queary params
    • http body 없이 url 쿼리 파라미터에 데이터를 포함해서 전달

  2. POST - HTML Form 
    • http body에 쿼리 파라미터 형식으로 전달
    • html form으로 전달하는 방식은 get, post만 지원한다.
  3. HTTP message body
    • http api에 주로 사용 - json 형식으로 데이터 전달  *text,xml,json 방식이 존재함
    • post,put,patch 다 사용가능

 

3.2. GET - queary params 조회 메서드

  • username=hello&username=kim
    • request.getParameter() : 하나의 파라미터 이름에 대해서 단 하나의 값만 있을 때 사용
      - hello만 출력
    • request.getParameterValues() : 하나의 파라미터 이름에 대해서 값이 중복일 경우 사용
      - hello, kim 출력

package hello.servlet.basic.request;
// public class RequestParamServlet extends HttpServlet{} 내용 요약

// http://localhost:8080/request-param?username=hello&age=20&username=bye
// key-value 형식

// 1. 단일 파라미터 조회 
String username = request.getParameter("username"); // 중복된 key있을 시 1st 꺼냄
String username = request.getParameter("age");

// 2. 파라미터 이름들 모두 조회(중복 key는 첫번째 entity만 꺼냄)
Enumeration<String> parameterNames = request.getParameterNames(); //username,age 다꺼냄
request.getParameterNames()
	.asIterator().forEachRemaining(System.out::println) // 전체 params 각각 꺼냄

// 3. 파라미터를 Map 으로 조회
Map<String, String[]> parameterMap = request.getParameterMap();

// 4. 복수 파라미터 조회
String[] usernames = request.getParameterValues("username"); // 중복된 key가 있을때만 사용가능

 

package hello.servlet.basic.request;

/**
 * 1. 파라미터 전송 기능
 * http://localhost:8080/request-param?username=hello&age=20
 * <p>
 * 2. 동일한 파라미터 전송 기능
 * http://localhost:8080/request-param?username=hello&username=kim&age=20
 */
@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");

        Enumeration<String> parameterNames = request.getParameterNames();
        System.out.println("parameterNames1 = " + parameterNames); // 주소 나옴

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

        System.out.println("[map 형식 조회]");
        Map<String, String[]> parameterMap = request.getParameterMap();
        System.out.println("parameterMap = " + parameterMap); // 주소로 나옴

        System.out.println("[단일 파라미터 조회]");
        String username = request.getParameter("username");
        System.out.println("username = " + username);
        String age = request.getParameter("age");
        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);
        }
        response.getWriter().write("ok");
    }
}

 

3.3. POST - HTML Form 조회 메서드

  • http request header 형식
    • 요청 URL: http://localhost:8080/request-param
    • content-type:  application/x-www-form-urlencoded 
    • message body:  username=hello&age=20
더보기

GET-queary params와 달리 http body를 통해서 쿼리문을 보내는 것이므로 http body가 어떤 형식으로 된 data인지 알려주는 content-type이 필요

요청 메시지에서 나타날 때 :   1) html data: application/x-www-form-urlencoded 

응답 메시지에서 나타날 때 :   1) text data : text/plain     2) html data: text/html   3)  json data : application/json

 

  • <input의 name="key"> :  queryParameter의 key의 역할을 수행
  • 해당 Post form 요청은 http 메시지 바디에 username=""&age="" 형식으로 보내진다.
  • 조회 메서드는 3.2.GET-quary parameter와 동일 
  • http body에 있어도 queary문이기 때문에 동일 취급
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form action="/request-param" method="post"> // post로 보냄
// 요청 url: http://localhost:8080/basic/hello-form.html
    username: <input type="text" name="username" />
    age:      <input type="text" name="age" />
    <button type="submit">전송</button>
</form>
</body>
 </html>

 

 

3.4.1. HTTP message body  -  API 메시지 바디_단순 텍스트

  • http message body에 데이터를 직접 담아서 요청
           
  • InputStream을 사용해서 직접 읽음
  • 사용 method()
    1. request.getInputStream()  :  http message body의 내용을 byte type code로 읽어온다.
      • form data도 위와 같이 가져올 수 있지만, getParameter()가 훨씬 유리하다.
    2. StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8) : byte type code를 string으로 변환
      • StreanUtils : spring이 제공하는 class
      • StandardCharsets.UTF_8: byte를 string으로 변환시 encoding 정보를 알려줘야 함
package hello.servlet.basic.request;

@WebServlet(name = "requestBodyStringServlet", urlPatterns = "/request-body-string")
public class RequestBodyStringServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        ServletInputStream inputStream = request.getInputStream();
        // http message body의 내용을 bytetypecode로 읽어온다.
        String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
        // bytecode를 string으로 변환, 변환시 항상 encoding 정보를 알려줘야함
        
        System.out.println("messageBody = " + messageBody);
    }
}

 

3.4.2. HTTP message body  -  API 메시지 바디_JSON

  • 방식
    1. request -> response  = JSON -> Object
    2. response -> request  = Object -> JSON
  • json 형식으로 파싱할 수 있는 class 생성
import lombok.Getter;
import lombok.Setter;
@Getter @Setter
public class HelloData {

    private String username;
    private int age;
}

 

  • json -> object
  • 사용 method() 
    1. new ObjectMapper(): json data를 파싱해서 사용할 수 있는 java object로 변환하기 위한 Jackson library
      • json  <-> JAVA obejct (양방향 가능)
    2. request.getInputStream()  :  http message body의 내용을 byte type code로 읽어온다.

    3. StreamUtils.copyToString() :  byte type code를 string으로 변환

    4. objectMapper.readValue(messageBody, HelloData.class) : json 형식을 java형식으로 담는다
@WebServlet(name = "requestBodyJsonServlet", urlPatterns = "/request-body-json")
public class RequestBodyJsonServlet extends HttpServlet {

    // 1. json data를 파싱해서 사용할 수 있는 java object로 변환하기 위한 Jackson library, ObjectMapper 제공
    // json 변환 library: Jackson, Gson
    private ObjectMapper objectMapper = new ObjectMapper();
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 2. http meassage body에서 data 꺼내는 작업
        ServletInputStream inputStream = request.getInputStream();
        String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
        System.out.println("messageBody = " + messageBody);

        // 3. 꺼낸 data(=json)을 java 객체에 담아서 파싱하기
        HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
        
        // object로 변환된 data 출력
        System.out.println("helloData.getUsername() = " + helloData.getUsername());
        System.out.println("helloData.getAge() = " + helloData.getAge());

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

}

 

4. HttpServletResponse

  • 개요
    • 개발자가 직접 모든 http 응답 메시지 만들기 번거롭다. 이를 해결해주기 위해 제공해주는 method
  • HTTP 응답 메시지 생성
    1. HTTP 응답코드 지정
    2. 헤더 생성
    3. 바디 생성
  • 응답 message 생성 편의기능 제공
    1. Content-Type
    2. Cookie
    3. Redirect

 

4.1. 기본 사용법(응답코드, 헤더, 편의기능)

@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); //http 응답코드

        // 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", "woowee"); // 내가 만든 임의의 header

        // header 편의 method
        content(response);
        cookie(response);
        redirect(response);

        PrintWriter writer = response.getWriter();
        writer.println("hello");
    }
    // Content 편의 method
    private void content(HttpServletResponse response) {
        response.setContentType("text/plain");
        response.setCharacterEncoding("utf-8");
        // response.setContentLength(2); // 생략시 자동 생성
    }
    // Cookie 편의 method
    private void cookie(HttpServletResponse response) {
        Cookie cookie = new Cookie("myCookie", "good");
        cookie.setMaxAge(600);
        response.addCookie(cookie);
    }
    // redirect 편의 method
    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");
    }
    // forwar method
}

 

5. http 응답 데이터 - http 응답 body에 들어가는 real data 집어넣기

응답 방식 응답 header 전송 형태 (ContentType) data 위치 형태
text text/plain body text
html text/html body html 형태의 text
json application/json body json 형태의 text

5.1. 단순 텍스트 응답

package hello.servlet.basic;

@WebServlet(name = "helloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.setContentType("text/plain");
        response.setCharacterEncoding("utf-8");
        response.getWriter().write("hello " + username);
    }
}

 

5.2. HTML 응답

  • 헤더에 setContentType(text/html) 추가
    • 'http 응답 body의 data'가  'client의 webBrowser'로 가서 렌더링 과정에서 html type인 것을 인지 시킴
    • UI에 html 형식으로 나타날 수 있게 된다.
@WebServlet(name = "responseHtmlServlet",urlPatterns = "/response-html")
public class ResponseHtmlServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response ) throws ServletException, IOException {

        response.setContentType("text/html");
        response.setCharacterEncoding("utf-8");

        PrintWriter writer = response.getWriter();
        writer.println("<html>");
        writer.println("<body>");
        writer.println("안녕");
        writer.println("</body>");
        writer.println("</html>");
        
        // 그냥 writer.println("안녕")만 해도 html로 나타남. ContentType에서 html이라고 알려주기 때문
    }
}

 

5.3. HTTP API -json 응답

  • json -> object
  • 사용 method() 
    1. new ObjectMapper(): json data를 파싱해서 사용할 수 있는 java object로 변환하기 위한 Jackson library
      • json  <-> JAVA obejct (양방향 가능)
    2. objectMapper.writeValueAsString(data) : java 객체를 JSON 문자로 변경한다.
package hello.servlet.basic.response;

@WebServlet(name = "responseJsonServlet", urlPatterns = "/response-json")
public class ResponseJsonServlet extends HttpServlet {
    // 1. objectMapper 생성
    private ObjectMapper objectMapper = new ObjectMapper();
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.setContentType("application/json");
        response.setCharacterEncoding("utf-8");

        // java class 객체에 http 응답 메시지 body에 넣을 data 담기 
        HelloData data = new HelloData();
        data.setUsername("woowee");
        data.setAge(26);

        // 2. java 객체를 json 형태의 string으로 담기
        String result = objectMapper.writeValueAsString(data); // <-> objectMapper.readValue(messageBody, HelloData.class);
        
        // http message body에 넣기
        response.getWriter().write(result);
    }
}

// request 할 때 code

// http meassage body에서 data 꺼내는 작업
ServletInputStream inputStream = request.getInputStream();
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
System.out.println("messageBody = " + messageBody);

// 꺼낸 data(=json)을 java 객체에 담아서 파싱하기
HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
System.out.println("helloData.getUsername() = " + helloData.getUsername());
System.out.println("helloData.getAge() = " + helloData.getAge());

 

 

 

이전 발행글 : 스프링 MVC 0 - 웹애플리케이션 이해

 

다음 발행글 : 스프링 MVC 2 - 서블릿, jsp, MVC 패턴

 

 


출처 인프런 김영한의 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술