Java : Servlet 생명주기

NuyHes·2025년 8월 12일
0

[Study]

목록 보기
64/71
post-thumbnail

🕵️ HttpServlet 제대로 이해하기

📄HttpServlet.class

public abstract class HttpServlet extends GenericServlet {
	
	protected void doGet   ...

	protected void doPost  ...

	protected void service ...
}

서블릿 컨테이너는 이름과 시그니처가 약속된 메서드만 호출한다. 그래서 doGet , doPost , init , destroy , service 같은 훅 메서드를 정확히 override해야 한다.


생명주기

전체 생명 주기
배포/시작
웹앱이 배포되고 컨테이너(Tomcat 등)가 시작
⬇️
클래스 로딩 & 인스턴스 생성
서블릿 클래스 로딩 👉 인스턴스 1개 생성(기본)
⬇️
초기화
init (ServletConfig) 👉 보통 init() ovverride로 리소스 준비
⬇️
요청 처리 루프 (요청마다 반복)
▫️ URL 매핑 결정 👉 필터 체인 전처리 👉 스레드에서 service(req, resp) 호출
▫️ HttpServlet (HttpServletRequest, HttpServletResponse)가
HTTP 메서드에 따라 doGet/doPost/...로 분기
▫️ 응답 커밋 👉 필터 체인 후처리 👉 커넥션 반환
⬇️
종료
앱/컨테이너가 내려갈 때 destroy() 호출 👉 리소스 정리, 스레드풀 종료
⬇️
재로딩/핫리로드
변경/재배포 시 기존 인스턴스 destroy() 후 새 인스턴스 init()

🕵️ 인스턴스는 보통 하나이고 요청마다 다른 스레드가 service()를 호출한다. 따라서 인스턴스 필드에 요청상태를 저장하면 안됨


메서드 요약

메서드누가 호출?언제기본 동작(override ❌)
init()컨테이너서블릿 초기화 1회아무것도 안함 (리소스 준비는 개발자 구현)
service(ServletRequest, ServletResponse)컨테이너모든 요청HTTP가 아니면 예외
HTTP면 아래 service로 위임
service(HttpServletRequest, HttpServletResponse)HttpServletHTTP 요청메서드별로 doXXX 디스패치, GET/HEAD의 캐싱 처리 포함
doGet()HttpServlet#serviceGET405/400 에러 (미구현시)
doPost()HttpServlet#servicePOST405/400 에러 (미구현시)
doPut() / doDelete()HttpServlet#servicePUT / DELETE405/400 에러 (미구현시)
doHead()HttpServlet#serviceHEAD기본은 doGet 재사용 (바디 제거)
doOptions()HttpServlet#serviceOPTIONS서브클래스의 doXXX 존재 여부를 반영해 Allow헤더 자동 구성
doTrace()HttpServlet#serviceTRACE요청 라인 / 헤더 에코 (보안상 비활성 권장)
destroy()컨테이너종료 1회리로스 정리는 개발자 구현

🕵️ Servlet / GenericServlet / HttpServlet 구조

📄Servlet.class

인터페이스

  1. 정의 : 컨테이너 (톰캣 등)와 서블릿이 어떻게 상호작용하는지를 약속
  2. 핵심 메서드
    • init(ServletConfig) : 최초 1회 초기화
    • service(ServletRequest, ServletResponse) : 요청마다 호출
    • destroy() : 종료 시 1회 정리
    • getServletConfig() , getServletInfo()

포인트 : 여기엔 로직이 없고 "생명주기 훅이 이런 이름/시그니처로 제공된다" 수준의 스펙만 있다.

public interface Servlet {  
    void init(ServletConfig var1) throws ServletException;  
  
    ServletConfig getServletConfig();  
  
    void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;  
  
    String getServletInfo();  
  
    void destroy();  
}

📄GenericServlet.class

추상 클래스 - 프로토콜 무관 기본 구현

  1. 상속/구현 : implements Servlet , ServletConfig
    • Servlet의 계약을 기본 구현으로 편하게 제공
  2. 역할
    • init(ServletConfig) 안에서 this.config 보관 후 매개변수 없는 init()을 호출
    • getServletConfig() , getServletContext() , getInitParameter() 등 유틸 제공
    • log() 같은 로깅 편의 메서드 제공
    • service(ServletRequest, ServletResponse)abstract 실제 요청 처리는 하위 클래스가 구현하도록 남겨둔다.
public abstract class GenericServlet implements Servlet, ServletConfig, Serializable {  
    private static final String LSTRING_FILE = "javax.servlet.LocalStrings";  
    private static ResourceBundle lStrings = ResourceBundle.getBundle("javax.servlet.LocalStrings");  
    private transient ServletConfig config;  
  
    public void destroy() {  
    }  
  
    public String getInitParameter(String name) {  
        ServletConfig sc = this.getServletConfig();  
        if (sc == null) {  
            throw new IllegalStateException(lStrings.getString("err.servlet_config_not_initialized"));  
        } else {  
            return sc.getInitParameter(name);  
        }  
    }  
  
    public Enumeration<String> getInitParameterNames() {  
        ServletConfig sc = this.getServletConfig();  
        if (sc == null) {  
            throw new IllegalStateException(lStrings.getString("err.servlet_config_not_initialized"));  
        } else {  
            return sc.getInitParameterNames();  
        }  
    }  
  
    public ServletConfig getServletConfig() {  
        return this.config;  
    }  
  
    public ServletContext getServletContext() {  
        ServletConfig sc = this.getServletConfig();  
        if (sc == null) {  
            throw new IllegalStateException(lStrings.getString("err.servlet_config_not_initialized"));  
        } else {  
            return sc.getServletContext();  
        }  
    }  
  
    public String getServletInfo() {  
        return "";  
    }  
  
    public void init(ServletConfig config) throws ServletException {  
        this.config = config;  
        this.init();  
    }  
  
    public void init() throws ServletException {  
    }  
  
    public void log(String msg) {  
        this.getServletContext().log(this.getServletName() + ": " + msg);  
    }  
  
    public void log(String message, Throwable t) {  
        this.getServletContext().log(this.getServletName() + ": " + message, t);  
    }  
  
    public abstract void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;  
  
    public String getServletName() {  
        ServletConfig sc = this.getServletConfig();  
        if (sc == null) {  
            throw new IllegalStateException(lStrings.getString("err.servlet_config_not_initialized"));  
        } else {  
            return sc.getServletName();  
        }  
    }  
}

📄HttpServlet.class

추상 클래스 - HTTP 전용 구현 + 메서드 분기

  1. 상속 : extends GenericServlet
  2. 핵심 아이디어 : HTTP 요청이라면 GET/POST/PUT/DELETE/ . . . 메서드 별로 자동 분기해주는 틀을 제공
  3. 메서드 구조
    • 오버로드된 service 2종
      - public void service(ServletRequest, ServletResponse) HTTP 전용 객체로 캐스팅 후 아래 HTTP 전용 service(HttpServletRequest, HttpServletResponse) 호출
      - protected void service(HttpServletRequest, HttpServletResponse) req.getMethod()로 분기해서 doGet/doPost/doPut/doDelete . . . 호출
    • doGet/doPost/doPut/doDelete/ . . .
      - 우리가 주로 오버라이드하는 지점
      - 오버라이드하지 않으면 보통 405(Method Not Allowed)가 응답된다.
    • getLastModified(HttpServletRequest)
      - doGet과 연계되는 조건부 GET(if-Modified-Since) 지원 포인트 타임스탬프를 반환하면 Last-Modified 헤더 처리와 304 응답 최적화가 가능 (미구현 시 -1)
public abstract class HttpServlet extends GenericServlet {  
    private static final String METHOD_DELETE = "DELETE";  
    private static final String METHOD_HEAD = "HEAD";  
    private static final String METHOD_GET = "GET";  
    private static final String METHOD_OPTIONS = "OPTIONS";  
    private static final String METHOD_POST = "POST";  
    private static final String METHOD_PUT = "PUT";  
    private static final String METHOD_TRACE = "TRACE";  
    private static final String HEADER_IFMODSINCE = "If-Modified-Since";  
    private static final String HEADER_LASTMOD = "Last-Modified";  
    private static final String LSTRING_FILE = "javax.servlet.http.LocalStrings";  
    private static ResourceBundle lStrings = ResourceBundle.getBundle("javax.servlet.http.LocalStrings");  
  
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {  
        String protocol = req.getProtocol();  
        String msg = lStrings.getString("http.method_get_not_supported");  
        if (protocol.endsWith("1.1")) {  
            resp.sendError(405, msg);  
        } else {  
            resp.sendError(400, msg);  
        }  
  
    }  
  
    protected long getLastModified(HttpServletRequest req) {  
        return -1L;  
    }  
  
    protected void doHead(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {  
        NoBodyResponse response = new NoBodyResponse(resp);  
        this.doGet(req, response);  
        response.setContentLength();  
    }  
  
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {  
        String protocol = req.getProtocol();  
        String msg = lStrings.getString("http.method_post_not_supported");  
        if (protocol.endsWith("1.1")) {  
            resp.sendError(405, msg);  
        } else {  
            resp.sendError(400, msg);  
        }  
  
    }  
  
    protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {  
        String protocol = req.getProtocol();  
        String msg = lStrings.getString("http.method_put_not_supported");  
        if (protocol.endsWith("1.1")) {  
            resp.sendError(405, msg);  
        } else {  
            resp.sendError(400, msg);  
        }  
  
    }  
  
    protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {  
        String protocol = req.getProtocol();  
        String msg = lStrings.getString("http.method_delete_not_supported");  
        if (protocol.endsWith("1.1")) {  
            resp.sendError(405, msg);  
        } else {  
            resp.sendError(400, msg);  
        }  
  
    }  
  
    private Method[] getAllDeclaredMethods(Class<? extends HttpServlet> c) {  
        Class<?> clazz = c;  
  
        Method[] allMethods;  
        for(allMethods = null; !clazz.equals(HttpServlet.class); clazz = clazz.getSuperclass()) {  
            Method[] thisMethods = clazz.getDeclaredMethods();  
            if (allMethods != null && allMethods.length > 0) {  
                Method[] subClassMethods = allMethods;  
                allMethods = new Method[thisMethods.length + allMethods.length];  
                System.arraycopy(thisMethods, 0, allMethods, 0, thisMethods.length);  
                System.arraycopy(subClassMethods, 0, allMethods, thisMethods.length, subClassMethods.length);  
            } else {  
                allMethods = thisMethods;  
            }  
        }  
  
        return allMethods != null ? allMethods : new Method[0];  
    }  
  
    protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {  
        Method[] methods = this.getAllDeclaredMethods(this.getClass());  
        boolean ALLOW_GET = false;  
        boolean ALLOW_HEAD = false;  
        boolean ALLOW_POST = false;  
        boolean ALLOW_PUT = false;  
        boolean ALLOW_DELETE = false;  
        boolean ALLOW_TRACE = true;  
        boolean ALLOW_OPTIONS = true;  
  
        for(int i = 0; i < methods.length; ++i) {  
            String methodName = methods[i].getName();  
            if (methodName.equals("doGet")) {  
                ALLOW_GET = true;  
                ALLOW_HEAD = true;  
            } else if (methodName.equals("doPost")) {  
                ALLOW_POST = true;  
            } else if (methodName.equals("doPut")) {  
                ALLOW_PUT = true;  
            } else if (methodName.equals("doDelete")) {  
                ALLOW_DELETE = true;  
            }  
        }  
  
        StringBuilder allow = new StringBuilder();  
        if (ALLOW_GET) {  
            allow.append("GET");  
        }  
  
        if (ALLOW_HEAD) {  
            if (allow.length() > 0) {  
                allow.append(", ");  
            }  
  
            allow.append("HEAD");  
        }  
  
        if (ALLOW_POST) {  
            if (allow.length() > 0) {  
                allow.append(", ");  
            }  
  
            allow.append("POST");  
        }  
  
        if (ALLOW_PUT) {  
            if (allow.length() > 0) {  
                allow.append(", ");  
            }  
  
            allow.append("PUT");  
        }  
  
        if (ALLOW_DELETE) {  
            if (allow.length() > 0) {  
                allow.append(", ");  
            }  
  
            allow.append("DELETE");  
        }  
  
        if (ALLOW_TRACE) {  
            if (allow.length() > 0) {  
                allow.append(", ");  
            }  
  
            allow.append("TRACE");  
        }  
  
        if (ALLOW_OPTIONS) {  
            if (allow.length() > 0) {  
                allow.append(", ");  
            }  
  
            allow.append("OPTIONS");  
        }  
  
        resp.setHeader("Allow", allow.toString());  
    }  
  
    protected void doTrace(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {  
        String CRLF = "\r\n";  
        StringBuilder buffer = (new StringBuilder("TRACE ")).append(req.getRequestURI()).append(" ").append(req.getProtocol());  
        Enumeration<String> reqHeaderEnum = req.getHeaderNames();  
  
        while(reqHeaderEnum.hasMoreElements()) {  
            String headerName = (String)reqHeaderEnum.nextElement();  
            buffer.append(CRLF).append(headerName).append(": ").append(req.getHeader(headerName));  
        }  
  
        buffer.append(CRLF);  
        int responseLength = buffer.length();  
        resp.setContentType("message/http");  
        resp.setContentLength(responseLength);  
        ServletOutputStream out = resp.getOutputStream();  
        out.print(buffer.toString());  
    }  
  
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {  
        String method = req.getMethod();  
        if (method.equals("GET")) {  
            long lastModified = this.getLastModified(req);  
            if (lastModified == -1L) {  
                this.doGet(req, resp);  
            } else {  
                long ifModifiedSince = req.getDateHeader("If-Modified-Since");  
                if (ifModifiedSince < lastModified) {  
                    this.maybeSetLastModified(resp, lastModified);  
                    this.doGet(req, resp);  
                } else {  
                    resp.setStatus(304);  
                }  
            }  
        } else if (method.equals("HEAD")) {  
            long lastModified = this.getLastModified(req);  
            this.maybeSetLastModified(resp, lastModified);  
            this.doHead(req, resp);  
        } else if (method.equals("POST")) {  
            this.doPost(req, resp);  
        } else if (method.equals("PUT")) {  
            this.doPut(req, resp);  
        } else if (method.equals("DELETE")) {  
            this.doDelete(req, resp);  
        } else if (method.equals("OPTIONS")) {  
            this.doOptions(req, resp);  
        } else if (method.equals("TRACE")) {  
            this.doTrace(req, resp);  
        } else {  
            String errMsg = lStrings.getString("http.method_not_implemented");  
            Object[] errArgs = new Object[1];  
            errArgs[0] = method;  
            errMsg = MessageFormat.format(errMsg, errArgs);  
            resp.sendError(501, errMsg);  
        }  
  
    }  
  
    private void maybeSetLastModified(HttpServletResponse resp, long lastModified) {  
        if (!resp.containsHeader("Last-Modified")) {  
            if (lastModified >= 0L) {  
                resp.setDateHeader("Last-Modified", lastModified);  
            }  
  
        }  
    }  
  
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {  
        if (req instanceof HttpServletRequest && res instanceof HttpServletResponse) {  
            HttpServletRequest request = (HttpServletRequest)req;  
            HttpServletResponse response = (HttpServletResponse)res;  
            this.service(request, response);  
        } else {  
            throw new ServletException("non-HTTP request or response");  
        }  
    }  
}

호출 흐름 요약

호출 흐름
컨테이너
⬇️
service(ServletRequest, ServletResponse)
⬇️
(HttpServletRequest/Response로 캐스팅)
⬇️
service(HttpServletRequest, HttpServletResponse)
⬇️
HTTP 메서드별 doXXX ... 로 분기

🕵️ 언제 무엇을 오버라이드할까

  • 대부분의 경우 : doGet() , doPost()만 구현하면 충분
  • 공통 전/후처리 (로깅, 트랜잭션 등) : service(HttpServletRequest, HttpServletResponse)를 오버라이드하고 반드시 super.service(req, resp)호출해서 기본 분기 유지
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp)
    throws ServletException, IOException {
  long t0 = System.nanoTime();
  try {
    super.service(req, resp); // doGet/doPost 분기 유지!
  } finally {
    log(req.getMethod() + " " + req.getRequestURI() + " took " + (System.nanoTime()-t0));
  }
}

💡정말 필요할 때만 : service(ServletRequest, ServletResponse)를 건드리자 (비-HTTP 요청 차단 등 특수 케이스)


최소 예시

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
      throws IOException {
    resp.setCharacterEncoding("UTF-8");
    resp.setContentType("text/plain;charset=UTF-8");
    resp.getWriter().println("Hello, GET!");
  }

  @Override
  protected void doPost(HttpServletRequest req, HttpServletResponse resp)
      throws IOException {
    // 폼 처리 등
    resp.getWriter().println("Hello, POST!");
  }
}

0개의 댓글