RE-Heat 개발자 일지

스프링 MVC 1편 - [2] 서블릿 본문

백엔드/스프링

스프링 MVC 1편 - [2] 서블릿

RE-Heat 2023. 6. 21. 21:25

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

 

스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 - 인프런 | 강의

웹 애플리케이션을 개발할 때 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 MVC의 핵심 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., -

www.inflearn.com

아래 내용은 인프런 김영한님의 스프링 MVC 1편 강의를 듣고 정리한 내용입니다.

 

[1] 프로젝트 생성


https://start.spring.io에서 스프링프로젝트 생성
② 스프링부트 버전 및 JAVA 버전 선택: JAVA 11버전을 사용하기 위해 2.7.12 버전 선택
    참고 : 스프링부트 3.0버전 이상은 JAVA 17을 사용해야 함.
③ Packaging
    JSP 사용 위해 WAR 선택. JSP는 무조건 WAR을 써야 함.
④ Dependencies
    Spring Web : 스프링 기반 웹 애플리케이션 개발하는 데 필요한 라이브러리 자동 설정
    Lombok : 반복되는 메소드를 어노테이션을 통해 자동 생성
ex) @Getter @Setter @ToString
@NoArgsConstructor(매개변수 X 기본 생성자)
@RequiredArgsConstructor(초기화되지 않는 모든 final필드 생성자 생성 의존성 주입)
@AllArgsConstructor(모든 필드에 대한 생성자 생성)
@EqualsAndHashCode 클래스에 대한 equals(Object other)와 hashCode()를 만든다.
@Data(끝판왕. @ToString @EqualsAndHashCode, @Getter @Setter @RequriedArgsConstructor 설정)

 


프로젝트 생성 도중 발생한 오류

Execution failed for task ':ServletApplication.main()'.

Process 'command 'C:/Program Files/Java/jdk-11.0.17/bin/java.exe'' finished with non-zero exit value 1

오류코드가 나와 File-Setting Gradle->IntelliJ로 바꾸는 방법을 써보는 등 고생했으나 알고 보니 port 8080을 같이 써서 생긴 오류였다. 

해결책 : application.properties에서 server.port = 8081을 추가


[2] Hello Servlet

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

@ServeltComponentScan 
스프링부트는 서블릿을 직접 등록해 사용할 수 있도록 위 어노테이션을 지원
=>@WebServlet이 달린 객체들을 싱글톤 객체로 등록시킴

 

@WebServlet(name="helloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("HelloServlet.service");
        System.out.println("request = " + request);
        System.out.println("response = " + response);

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

        response.setContentType("text/plain");
        response.setCharacterEncoding("utf-8");
        response.getWriter().write("hello " + username);
    }
}

① @WebServlet

    name으로 싱글톤 객체 이름 등록.

    urlPatterns을 통해 어떤 url이 들어오면 실행할지 지정 ex) localhost:8080/hello

 

② protected void service(HttpServletRequest request, HttpServletResponse response)

HTTP 요청을 통해 매핑된 URL이 호출되면 서블릿 컨테이너는 service() 메서드를 실행

요청이 들어올 때마다 WAS가 request, response 객체 새롭게 만들어서 전송

 

③ getParameter()메소드로 파라미터 꺼내기

[GET방식] http://localhost:8081/hello?username=kim으로 보내면

response.getWriter().write("hello " + username);에 의해 브라우저에 hello kim이 출력

참고] POST 방식도 파라미터가 보내지는 형식은 같기 때문에  똑같이 getParameter()로 꺼낼 수 있음

 

참고 : HttpServlet을 상속하고 ctrl+o를 누르면 메소드를 쉽게 가져올 수 있음 [인텔리J 윈도우 기준]

 

서버가 받은 HTTP 요청메시지 출력하는 기능

application.properties에

logging.level.org.apache.coyote.http11=debug 추가하면 HTTP 요청메시지를 콘솔에 출력해 줌.

영한님 : 이 설정을 해두면 성능저하가 생길 수 있으므로 개발 단계에서만 사용

서블릿 컨테이너 동작 방식 설명

스프링부트 실행 -> JAR or WAR 파일 생성 -> 내장 톰캣에 파일 올림 -> 서블릿 컨테이너가 HelloServlet을 싱글톤 서블릿 객체로 등록
클라이언트가 localhost:8080/hello?username=world에 접속 -> WAS에 HTTP 요청 메시지 전달
WAS(톰캣) HTTP 요청 메시지 바탕으로 request, response 객체 생성, service()실행->HelloServlet안 로직 실행 -> response객체를 활용해 HTTP 응답 자동생성

 

[3] HttpServletRequest 개요

서블릿이 HTTP 요청메시지를 개발자 대신 파싱하고 그 결과를 HttpServletRequest 객체 담아 제공

 

HttpServletRequest의 역할

① 요청 메시지 조회

StartLine : HTTP 메소드(POST), URL(/save), 쿼리 스트링, 스키마, 프로토콜 

Header Line : 헤더 조회

Message Body : form 파라미터 형식 조회, message body 데이터 조회

 

② 임시 저장소 기능

해당 HTTP 요청이 들어온 시점부터, 끝나기 전까지 유지됨

request.setAttribute(name, value)으로 값을 저장

request.getAttribute(name)으로 조회

 

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

    //스타트 라인 정보
    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 모든 정보
    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 +  ": " + headerName);
//
//        }

        //요즘 쓰는 간결한 문법
        request.getHeaderNames().asIterator()
                .forEachRemaining(headerName -> System.out.println(headerName + ":" + request.getHeader(headerName)));

        //값 하나만 꺼낼 수 있음
        request.getHeader("host");

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

① 스타트 라인 정보

Http Start line의 메서드, 프로토콜, 스키마(http, https), URL, URL, 쿼리 파라미터, https사용 여부 등이 출력

 

② 모든 헤더 정보

Enumeration(자바초기버전) -> Iterator(확장버전)

차이점

    Enumeration은 Hashtable, Vector에서 사용가능

    Iterator는 Collection인터페이스를 구현상속한 모든 컬렉션 클래스에서 사용가능

헤더 관련 모든 정보 출력.

 

③ 헤더 편의 조회

 

서버이름, 포트 번호, 선호 언어, 쿠키, 콘텐츠 등 조회

참고] 선호 문자는 Qualtiy Vlaues(q) 값을 활용해 우선순위를 매길 수 있음

ex) Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7 < 클수록 우선수위 높음

 

④ 기타 정보

remote는 클라이언트 정보, Local은 요청을 받은 서버에 관한 정보 

클라이언트 이름, 클라이언트 IP, 클라이언트 포트 번호 조회

 


[4] HTTP 요청 데이터 - 개요

① GET - 쿼리 파라미터

   형식 : /url?key1=value1&key2=value2

    메시지 바디 없이, URL 쿼리 파라미터에 데이터를 포함해 전달

② POST - HTML Form

  content-type: application/x-www-form-urlencoded html폼으로 데이터 전송

  html <form></form> 안 내용을 메시지 바디에 쿼리 파라미터 형식으로 전달

  참고 : HTML Form 데이터를 보낼 땐 POST만 사용 가능

③ HTTP message body 

   http message body에 데이터를 직접 담아서 요청하는 방식(HTTP API에서 주로 사용)

  데이터 형식은 JSON, XML, TEXT 중 JSON을 주로 사용함.

  POST, PUT, PATCH 등을 사용

 

[5] HTTP 요청 데이터 - GET 쿼리 파라미터

    검색, 필터, 페이징에 많이 사용되는 방식

    HttpServletRequest가 제공하는 getParameter메서드를 통해 파라미터 편리하게 조회 가능   

/*
1. 파라미터 전송기능
http://localhost:8080/request-param?username=hello&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");

        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("username = " + name);
        }

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

 

단일 vs 복수 파라미터 조회
단일 파라미터 : request.getParameter()
복수 파라미터 : request.getParameterValues() 사용
=>복수 파라미터일 때 getParamter()를 사용하면 values()의 첫 번째 값만 반환함

 

[6] HTTP 요청 데이터 - POST HTML Form

application/x-www-form-urlencoded 형식은 GET에서 살펴본 쿼리 파라미터와 형식이 같아  request.getParameter()로 구분 없이 조회 가능. 서버 입장에선 둘의 형식이 동일하다는 뜻.

 

[7] HTTP 요청 데이터 - API 메시지 바디 - 단순 텍스트

@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(); //메시지 바디 내용을 바이트 코드로 가져옴
        String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8); // 바이트 코드를 String으로 바꿈. 스프링 내장 StreamUtils로
        System.out.println("messageBody = " + messageBody);
        response.getWriter().write("ok");
    }
}

  [4]③ 내용과 동일, 주로 앱 to 서버, 웹 클라이언트 to 서버, 서버 to 서버에서 사용

 

[8] HTTP 요청 데이터 - API 메시지 바디 - JSON

@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, Gson 같은 라이브러리를 사용해야 함.

스프링부트는 Jackson 라이브러리의 ObjectMapper를 사용.

 

[9] HttpServletResponse 기본 사용법

- HTTP 응답 메시지 생성
    start line 세팅(HTTP 응답코드 지정)
    헤더 생성
    바디 생성
- 편의 기능 제공
    Content-Type
    쿠키
    Redirect

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

        //[response-header]
        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 편의 메서드] - 컨텐츠 타입 Header에 넣지 말고 이렇게 넣는 방법도 있군.
        content(response);
        cookie(response);
        redirect(response);

        //[메시지 바디]
        PrintWriter writer = response.getWriter();
        writer.print("ok");
    }

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

 

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

단순 텍스트는 앞서 writer.println("ok")로 확인

HTML 응답

@WebServlet(name="responseHtmlServlet", urlPatterns = "/response-html")
public class ResponseHtmlServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // Cotent-Type : text/html;charset=utf-8
        response.setContentType("text/html");
        response.setCharacterEncoding("utf-8");

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

HTTP응답을 HTML으로 반환하려면 Content-type을 text/html로 지정해야 함.

utf-8로 인코딩하지 않으면 한글이 깨질 위험이 있음.

서블릿으로 HTML을 렌더링을 하려면 직접 작성해야 하는 번거로움이 있음.

 

[11] HTTP 응답 데이터 - API JSON

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

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //Content Type을 헤더에 넣기
        response.setContentType("application/json");
        response.setCharacterEncoding("utf-8");

        //객체에 담기
        HelloData helloData = new HelloData();
        helloData.setUsername("kim");
        helloData.setAge(20);

        //객체 JSON으로 보내기
        String result = objectMapper.writeValueAsString(helloData);

        //화면에 찍기
        response.getWriter().print(result);
    }
}

JSON으로 반환 시 Content-type은 application/json으로 지정

objectMapper.writeValuesAsString()을 쓰면 객체를 JSON 문자로 변경가능

 

참고] application/json은 기본값이 utf-8이므로 application/json;charset=utf-8이라고 전달할 필요 X