Spring MVC/01.WebProgramming

03. Servlet 분석

  • -

이번 포스트에서는 Servlet의 주요 특징에 대해 살펴보자.

더보기
@WebServlet(name = "helloServlet", value = "/hello-servlet")
public class HelloServlet extends HttpServlet {
    private String message;

    public void init() {
        message = "Hello World!";
    }

    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
        response.setContentType("text/html");

        // Hello
        PrintWriter out = response.getWriter();
        out.println("<html><body>");
        out.println("<h1>" + message + "</h1>");
        out.println("</body></html>");
    }

    public void destroy() {
    }
}

Servlet 작성

 

extends HttpServlet

servlet을 작성하기 위해서는 jakarta.servlet.http.HttpServlet을 상속받는 것이 일반적이다. jakarta는 Java EE의 패키지로 tomcat등 웹 컨테이너들은 이 패키지의 interface들을 구현한다.

 

Servlet을 상속받은 후 필요에 따라서 init(), doGet()/doPost(), destroy() 등 메서드를 재정의하여 필요한 동작을 구현한다. 이런 메서드들을 Servlet의 Life Cycle을 구성하는 메서드라고 한다.

 

@WebServlet

@WebServlet은 Servlet에 대한 설정을 위한 애너테이션이다. 과거에는 web.xml에 관련 설정을 했었는데 최근에는 @WebServlet을 이용하는 방식이 주로 사용된다.

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WebServlet {
    String name() default "";

    String[] value() default {};         // url pattern에 대한 alias
    String[] urlPatterns() default {};   // servlet을 호출할 url로 반드시 '/'로 시작
    . . .
}

여러 가지 속성이 있겠지만 가장 중요한 속성은 urlPatterns로 Servlet을 호출할 때 사용되는 url을 지정하는 부분이다. 이 속성은 value로 alias 되어있으며 URL 기반으로 작성할 때는 반드시 '/'로 시작해야 한다.

 

Servlet Life Cycle

 

Servlet Life Cycle을 구성하는 메서드들

일반적으로 Java application을 분석할 때는 main 메서드에서 부터 시작해서 코드를 따라가면 된다. 그런데 Servlet에는 main 메서드가 없었으며 실제 Servlet을 호출하는 코드도 찾아 볼 수 없다. 어떻게 동작하는 것일까?

일단 main 메서드를 가지고 있는 것은 WAS(tomcat)이다. 즉 WAS를 실행시키면 WAS가 내부의 context들을 실행해준다. 만약 코드 중간에 System.out.println()을 이용해서 출력한다면 그것은 WAS의 콘솔에 출력하는 것이다.

WAS는 Servlet Container로써 Servlet 객체의 라이프 사이클을 관리한다. 따라서 프로그래머의 코드에서는 라이프 사이클 관련 메서드를 호출하지는 않는다.

Servlet Container로써 Servlet의 Life Cycle을 관리하는 WAS

  1. WAS는 Servlet에 선언된 @WebServle을 분석해서 어떤 Servlet이 어떤 url에 매핑되었는지 정보를 유지한다.
  2. 사용자의 요청이 url로 들어오면 매핑된 Servlet 객체를 찾는다.
  3. 만약 해당 Servlet을 처음 요청하는 경우라면 객체를 생성하고 초기화 메서드인 init() 메서드를 호출한다.
  4. init() 이후 service()가 호출되는데 사용자의 요청 방식이 get인지, post인지에 따라 doGet / doPost 메서드를 호출한다.
  5. 이후 동일한 servlet을 다시 요청받는다면 init까지는 수행하지 않고 service(doGet/doPost)만 반복해서 호출한다.
  6. WAS를 종료 등의 상황으로 servlet이 제거될 때 소멸 메서드인 destroy() 가 호출된다.

init과 destroy는 각각 servlet에서 사용하는 자원을 초기화하거나 정리하는 용도로 사용된다.

  • init(): servlet 객체가 생성된 후 웹 컨테이너에 의해 호출되는 메서드로 init이 종료되기 전에는 어떠한 servlet 요청도 처리되지 않는다. init()의 주요 용도는 사용자 요청 처리에 필요한 servlet의 리소스를 초기화 하는 것이다.
  • destroy(): servlet 객체가 제거되기 전에 웹 컨테이너에 의해 호출되는 메서드로 모든 클라이언트의 요청이 종료된 후에 호출된다. 주요 용도는 init()에서 생성한 자원을 정리하는 것이다.

 

Servlet과 multi thread

Servlet은 사용자의 요청이 있을 때마다 새로운 Servlet을 만들지 않고 하나의 Servlet만 생성한 상태에서 사용자의 요청을 Thread로 만들어서 Servlet을 공유한다. 따라서 프로그램의 효율이 높다.

동일하 Servlet에 대한 클라이언트의 요청은 하나의 Servlet을 공유한다.

여러 스레드가 동시에 하나의 Servlet 인스턴스를 공유하므로 상태정보를 가진 멤버 변수가 있을 경우 동시성 문제가 발생할 수 있다. 따라서 " Servlet은 Thread에 Safe 하지 않다".

이런 공유 자원 문제를 안전하게 처리하기 위해 synchronized를 사용할 수 있다. synchronized는  한번에 하나의 요청만 순차적으로 처리하게 할 수 있는데 이는 서버의 성능을 심각하게 저하시킬 수 있으므로 바람직하지 않다. 

따라서 Servlet을 개발 할 때 가장 안전한 방법은 상태를 관리하는 쓰기 가능한 멤버 변수를 사용하지 않는 것이다. 대신 각 요청에 대해 로컬 변수를 사용하거나 필요한 경우 스레드에 안전한 외부 리소스를 활용하는 것이 좋다. (HelloServlet의 message는 init에서 수정하며 service(doGet)에서는 단지 사용만 하고 있다.)

Life Cycle 동작 테스트

HelloServlet의 init(), doGet(), destroy() 메서드에 적절히 로그를 출력하도록 하고 여러번 호출해보자.

@WebServlet(name = "helloServlet", value = "/hello-servlet")
public class HelloServlet extends HttpServlet {

    public void init() {
        System.out.println("init 호출됨 - 리소스 초기화");
    }

    public void doGet(HttpServletRequest request, HttpServletResponse response) 
                                                                        throws IOException {
        System.out.println("doGet 호출됨 - 요청별 서비스");
        . . .
    }

    public void destroy() {
        System.out.println("destroy 호출됨 - 리소스 정리");
    }
}

이제 http://localhost:8080/simpleweb/hello-servlet를 요청하면 init()은 처음에 1회만 호출되고 이후에는 doGet()만 동작하는 것을 확인할 수 있다. 그리고 WAS를 종료하면 destroy()가 호출된다.

init 호출됨 - 리소스 초기화  // 최소 Servlet 생성 후 초기화 진행
doGet 호출됨 - 요청별 서비스 
doGet 호출됨 - 요청별 서비스 // 요청 시마다 반복
destroy 호출됨 - 리소스 정리 // Servlet 제거 이전에 리로스 정리

 

HttpServletRequest와 HttpServletResponse

WAS는 사용자의 요청을 받으면 HttpServletRequest, HttpServletResponse를 만들어서 doGet / doPost를 호출한다. 이 두 녀석의 기본적인 사용법에 대해 살펴보자.

HttpServletRequest

HttpServletRequest는 HTTP를 통해 서버로 전달된 사용자의 요청을 분석하고 처리하기 위한 객체로 요청과 관련된 정보를 추출할 수 있는 다양한 메서드들이 존재한다. 

요청에는 어떤 정보들이 있을까? 가장 중요한 것은 사용자가 "무엇을 가지고 어떤 작업을 하고 싶은가?"일 것이다.  예를 들어 "100원을 입금해주세요"라는 요청이 있다고 생각해보자. 여기서 어떤 작업은 url에 표현되고 '무엇을'에 해당하는 100원은 파라미터로 전달된다.

HttpServletRequest에는 원격지에서 전달된 파라미터를 확인할 수 있는 다양한 메서드가 제공된다. 네트워크를 통해 전달되는 값은 문자열로만 처리된다.

HttpServletRequest에서 파라미터 처리를 위해 제공되는 메서드들

이 외에도 header 정보 조회, 클라이언트 정보 조회, 쿠키 조회 등의 기능을 제공한다. 이들은 필요할 때 설명하기로 하자.

HttpServletResponse

HttpServletResponse는 HTTP를 통해 응답을 클라이언트에게 전달하기 위해 사용되는 객체이다. 응답에서 가장 중요한 것은 "어떤 데이터를 보내줄 것인가?" 이다. 어떤 데이터에는 '어떤 형식의'와 '어떤 내용의'가 포함된 내용이다.

전송하는 데이터의 형식을 지정하기 위해서는 setContentType()이라는 메서드를 이용해서  MIME TYPE 을 지정해주면 된다. HttpServletRequest의 기본 MIME TYPE은 text/html이어서 문제는 없지만 encoding설정이 추가로 필요하다. 한글이 문제 없이 처리되기 위해서는 UTF-8로 처리해주는 것이 좋다.(기본은 ISO8859-1로 한글이 깨진다.)

response.setContentType("text/html;charset=UTF-8");

MIME TYPE을 설정했다면 클라이언트에게 전송하기 위한 Stream을 구성해야 하는데 이를 위해 getWriter()라는 메서드를 이용해서 PrintWriter를 얻어온다. 다음으로는 PrintWriter를 통해 전송할 데이터를 쓰면 된다.

PrintWriter out = response.getWriter();
out.println("<html><body>");               // html 코드를 이렇게 작성해야 하다니. ㅜㅜ
out.println("<h1>" + message + "</h1>");
out.println("</body></html>");


이 외에도 header를 설정하거나 cookie 설정, error 처리 등 다양한 형태로 사용이 가능하다.

 

GET vs POST

 

HTTP 메서드

클라이언트가 서버로 요청하는 방식을 HTTP 메서드라고 하는데 다음과 같이 매우 다양하다.

출처: https://ko.wikipedia.org/wiki/HTTP

이중 일반적인 페이지 요청(전체적으로 페이지를 로딩하는 방식)에는 GET/POST가 사용된다.

GET / POST

두 방식의 차이점을 비교해보자.

방식 GET POST
상황 • post를 지정하지 않은 모든 경우
•  특별한 지정이 없는 경우 default
• <form> 태그에서 post 방식으로 전달하는 경우
파라미터
전달 방식
•  request body가 없으므로 URL을 통해 전달
 - 데이터가 노출되며 짧은 데이터만 전달 가능
 - www.google.co.kr?page=1&q=today
•  URL이 아닌 request body를 통해 전달
 - 데이터가 노출되지 않고 전송 길이에 제한이 없다.
데이터 양 •  255자 이하의 적은 데이터만 전송 가능 •  용량에 제한이 없음
보안성 •  데이터가 노출되므로 보안에 취약 •  데이터가 노출되지는 않아 GET 보다는 안전
 - 암호화와는 무관('옆사람한테 안보인다.' 정도)
멱등성 •  동일한 요청에 대해 언제나 같은 결과 - 멱등 •  동일한 요청에 다른 결과 - 멱등하지 앟음
활용 예
request의 처리가 서버에 아무런 영향을 주지 않을 경우 - 단순 조회
데이터가 포함된 request 자체를 bookmark로 사용하고 싶을 때
•  데이터 생성, 수정, 삭제 등 C, U, D 작업
비밀번호와 같이 URL상에 보이지 말아야 할 자료를 전송할 경우:  로그인의 경우 R이지만 보안을 위해 POST 처리


간단히 구구단을 출력하는 Servlet을 만들어서 이제까지의 내용을 점검해보자.

 

GuguDanServlet

 

gugu.html

먼저 사용자로부터 알고 싶은 단을 입력 받을 수 있는 html 페이지를 작성해보자. 다른 동적 요소들과 구분하기 위해서 정적임을 나타내는 static 폴더를 만들고 안에 저장하자. (src/main/webapp/static/gugu.html)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>구구단출력</title>
</head>
<body>
<form action="/simpleweb/gugu">
    <label for="dan">단: </label><input type="text" name="dan" id="dan">
    <button type="submit" formmethod="get">전송:get</button>
    <button type="submit" formmethod="post">전송:post</button>
</form>
</body>
</html>

<form>의 action은 서버에서 요청을 처리할 Servlet의 url-mapping이다. 현재는 절대 경로로 작성되었으며 /simpleweb은 context root이며 /gugu는 path 정보이다. 일반적으로 <form>의 method 속성을 통해 전송 방식을 결정하는데 여기서는 get/post를 테스트 해보기 위해 <button>의 formmethod 속성을 이용해보자. 

<form>내의 <input> 요소들의 입력 값은 서버로 전송되는데 이때 name은 서버로 전송되는 파라미터 이름이 된다.

index.jsp에 <a href="/simpleweb/static/gugu.html">구구단 요청</a> 를 추가해두면 편하다.

 

GuguServlet

다음은 위 요청을 처리할 GuguServlet을 작성해보자.(src/main/java/com.doding.simpleweb.GuguServlet.java) 

Servlet을 작성하기 위해서는 클라이언트의 요청에 따라 다음 요소가 결정지어진다.

  1. @WebServlet의 urlPatterns : 클라이언트에서 호출한 path - gugu
  2. doGet 또는 doPost 작성: <form>의 메서드에 따라서 해당 method override
  3. 파라미터 확인: <form> 내부 <input> 요소의 name 속성이며 문자열로 전달 - dan

위 내용을 염두에 두고 Servlet을 작성해보자.

더보기
package com.doding.simpleweb.controller;

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("/gugu")
public class GuguServlet extends HttpServlet {
  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
                                                    throws ServletException, IOException {
    System.out.println("doGet 호출됨");
    handleRequest(req, resp);
  }

  @Override
  protected void doPost(HttpServletRequest req, HttpServletResponse resp) 
                                                    throws ServletException, IOException {
    System.out.println("doPost 호출됨");
    handleRequest(req, resp);
  }

  private void handleRequest(HttpServletRequest request, HttpServletResponse response)
            throws IOException {
        String danStr = request.getParameter("dan");  // 1. 파라미터 확인 및 검증
        StringBuilder data = new StringBuilder();
        if (danStr == null || danStr.isEmpty()) {
            data.append("dan을 입력하세요.");
        } else {
            int dan = Integer.parseInt(danStr);  // 2. 비지니스 로직 처리
            for (int i = 1; i < 10; i++) {
                data.append(dan).append("*").append(i).append("=").append(i * dan).append("<br>\n");
            }
        }
        response.setContentType("text/html;charset=UTF-8"); // 3. 결과 출력
        PrintWriter writer = response.getWriter();
        writer.print("""
                <html>
                    <head>
                        <title>구구단출력</title>
                    </head>
                    <body>%s</body>
                </html>""".formatted(data));
    }
}

 

동작 확인

[http://localhost:8080/simpleweb/gugu.html]를 요청하면 다음과 같은 화면을 볼 수 있다.

여기서 단에 4입력하고 get 방식으로 전송해보면 dan=4가 url의 파라미터로 전송되는 것을 확인할 수 있다. 하지만 post로 전송해보면 url에 파라미터가 전송되지 않는다.

전송되는 파라미터를 확인하기 위해서는 [F12]를 이용해서 개발자 도구를 활성시킨 후 Network 탭을 활성화 시키고 다시 한번 요청해보면 요청 정보의 Payload 부분에서 전달되는 정보를 확인할 수 있다.

post 방식으로 전달된 파라미터의 확인

'Spring MVC > 01.WebProgramming' 카테고리의 다른 글

06. MVC와 Model2  (0) 2024.08.20
05. JSP  (0) 2024.08.12
04. Front Controller Pattern  (0) 2024.08.08
02. Hello Servlet  (0) 2024.03.05
01. 웹 프로그래밍 개요  (0) 2023.12.09
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.