TIL

오늘 배운 것

Servlet이란?

한 줄 요약: “브라우저 ↔ 웹서버 ↔ WAS(서블릿 컨테이너) ↔ 애플리케이션” 에서, Servlet 은 서버 사이드 동적 처리에서 사용된다.


1. Java 웹 개발의 시작: 정적에서 동적으로

초기 웹은 HTML/CSS 중심의 정적 콘텐츠였다. 곧 사용자·시간·DB 상태에 따라 달라지는 동적 페이지가 필요해졌고, Java는 한때 브라우저 플러그인(Applet) 으로 이를 시도했다.
그러나 플러그인 의존(보안 경고/설치 번거로움), 호환성/성능 이슈, 모바일 부재 등의 이유로 서버에서 HTML을 생성해 내려주는 서버 사이드 렌더링(SSR) 이 주류가 되었고, 그 핵심이 Servlet/JSP였다.

그림(정적/동적 요청 흐름)

[Browser]
    |
    v
[Web Server: Nginx/Apache]
    |  \
    |   \-- (static) --> [/assets, CDN]
    |
   (proxy)
    v
[WAS(=Servlet Container: Tomcat/Jetty)] --> [DB]

2. Servlet이란?

Servlet은 “HTTP 요청을 받아 HttpServletRequest/Response로 처리하고, 로직을 수행해 응답을 생성하는 서버 측 자바 컴포넌트”다.
컨테이너가 생명주기, 스레드, 리소스를 관리해 주므로 개발자는 비즈니스 로직에 집중할 수 있다.

패키지 주의

  • Tomcat 10+ : jakarta.servlet.*
  • Tomcat 9- : javax.servlet.*

2.1 Tomcat, WAS, 웹서버, 서블릿 컨테이너

  • 웹 서버(Web Server): 정적 파일 서빙 + 리버스 프록시(예: Nginx, Apache httpd)
  • WAS(Web Application Server): 서블릿/JSP 실행환경, 스레드/리소스 관리(예: Tomcat, Jetty)
  • 서블릿 컨테이너(Servlet Container): 서블릿 등록/매핑/생명주기/스레드/디스패치를 담당하는 엔진. Tomcat의 핵심 역할.

그림(요청 파이프라인 개념)

Client --> WebServer(Proxy) --> ServletContainer(Tomcat)
           -> Filter(s) -> (Front Controller / Servlet) -> Controller -> View(JSP/Thymeleaf)

2.2 Servlet 라이프사이클

1) 로드 & 인스턴스화: 최초 요청 시(또는 load-on-startup) 클래스 로드/인스턴스 생성
2) init(): 1회 초기화(캐시/풀 준비 등)
3) service()doGet()/doPost(): 매 요청마다 호출(멀티스레드)
4) destroy(): 종료 시 자원 정리

스레드 안전 핵심

  • 서블릿 인스턴스는 보통 1개이며, 요청마다 서로 다른 스레드가 같은 인스턴스를 호출한다.
  • 인스턴스 필드에 요청별 상태 저장 금지. 지역 변수/요청 속성 사용.

2.3 Servlet 예제코드

(A) 애너테이션 매핑 (jakarta.servlet.*, Tomcat 10+)

package com.example;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.*;

import java.io.IOException;
import java.io.PrintWriter;

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

    @Override
    public void init() throws ServletException {
        // 1회 초기화 로직 (예: 캐시 준비)
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        resp.setContentType("text/plain;charset=UTF-8");
        try (PrintWriter out = resp.getWriter()) {
            String name = req.getParameter("name");
            out.println("Hello " + (name != null ? name : "Servlet"));
        }
    }

    @Override
    public void destroy() {
        // 자원 정리
    }
}

(B) web.xml 매핑

<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee" version="5.0">
  <servlet>
    <servlet-name>helloServlet</servlet-name>
    <servlet-class>com.example.HelloServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>

  <servlet-mapping>
    <servlet-name>helloServlet</servlet-name>
    <url-pattern>/hello</url-pattern>
  </servlet-mapping>
</web-app>

TIP

  • 문자 인코딩: POST 폼 파라미터 깨짐 방지 → UTF-8 인코딩 필터로 공통 적용
  • I/O: getReader()getInputStream()둘 중 하나만 사용
  • 에러 처리: 공통 에러 페이지/로깅을 필터 또는 프론트컨트롤러 레벨에서

3. 프론트 컨트롤러(Front Controller) 패턴

URL마다 서블릿을 늘리면 공통 로직(로깅/인증/인코딩) 중복, 뷰 이동 반복이 발생한다..
Front Controller진입점 1개(/app/*) 에서 모든 요청을 수신하고, 공통 처리 → 라우팅 → 컨트롤러 실행 → 뷰 렌더링을 표준화한다. Spring MVC의 DispatcherServlet 의 동작 원리가 이와 같다.

3.1 미니 MVC(핵심) 예제

FrontController + Controller/View 계약

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.*;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@WebServlet(urlPatterns = "/app/*")
public class FrontControllerServlet extends HttpServlet {

    private final Map<String, Controller> handlerMap = new HashMap<>();

    @Override
    public void init() {
        handlerMap.put("/members/new-form", new MemberFormController());
        handlerMap.put("/members/save", new MemberSaveController());
        handlerMap.put("/members", new MemberListController());
    }

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp)
            throws IOException, ServletException {
        String path = req.getPathInfo(); // "/members", "/members/save" ...
        Controller handler = handlerMap.get(path);
        if (handler == null) {
            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        View view;
        try {
            view = handler.handle(req, resp);
        } catch (Exception e) {
            throw new ServletException(e);
        }
        view.render(req, resp);
    }
}

interface Controller {
    View handle(HttpServletRequest req, HttpServletResponse resp) throws Exception;
}

interface View {
    void render(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException;
}

class JspView implements View {
    private final String jsp; // 예: "members/list"
    JspView(String jsp) { this.jsp = jsp; }
    @Override
    public void render(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        req.getRequestDispatcher("/WEB-INF/views/" + jsp + ".jsp").forward(req, resp);
    }
}

샘플 컨트롤러

class MemberFormController implements Controller {
    @Override
    public View handle(HttpServletRequest req, HttpServletResponse resp) {
        return new JspView("members/new-form");
    }
}

class MemberSaveController implements Controller {
    @Override
    public View handle(HttpServletRequest req, HttpServletResponse resp) {
        String name = req.getParameter("name");
        // TODO: 저장 로직
        req.setAttribute("name", name);
        return new JspView("members/result");
    }
}

class MemberListController implements Controller {
    @Override
    public View handle(HttpServletRequest req, HttpServletResponse resp) {
        // TODO: 목록 조회
        req.setAttribute("members", java.util.List.of("A","B","C"));
        return new JspView("members/list");
    }
}

그림(요청 흐름)

[Client] -> [Filter(s)] -> [FrontController /app/*]
                       -> [HandlerLookup] -> [Controller]
                       -> [View(JSP)] -> [Response]

3.2 필터(Filter): 공통 전/후처리

  • 위치: 서블릿 앞단
  • 용도: 인코딩, 로깅, 인증/인가, CORS 등
  • 구조: 체인으로 순서를 제어하여 조합

로깅 필터 예시

import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import java.io.IOException;

@WebFilter(urlPatterns = "/*")
public class LoggingFilter implements Filter {
    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
            throws IOException, ServletException {
        long start = System.currentTimeMillis();
        try {
            chain.doFilter(req, resp); // 다음 필터/서블릿 호출
        } finally {
            System.out.println("RT(ms)=" + (System.currentTimeMillis() - start));
        }
    }
}

UTF-8 인코딩 필터(예시)

import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import java.io.IOException;

@WebFilter(urlPatterns = "/*")
public class CharacterEncodingFilter implements Filter {
    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
            throws IOException, ServletException {
        req.setCharacterEncoding("UTF-8");
        resp.setCharacterEncoding("UTF-8");
        chain.doFilter(req, resp);
    }
}

3.3 리스너(Listener): 라이프사이클 훅

  • ServletContextListener: 앱 시작/종료
  • HttpSessionListener: 세션 생성/소멸
  • 용도: 캐시 워밍, 스케줄러 시작/정지, 리소스 정리

리스너 예시

import jakarta.servlet.ServletContextEvent;
import jakarta.servlet.ServletContextListener;
import jakarta.servlet.annotation.WebListener;

@WebListener
public class AppStartupListener implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        // ex) 캐시 로딩, 스케줄러 시작
    }
    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        // ex) 자원 정리
    }
}

4. 주요 체크리스트

  • 스레드 안전: 서블릿 필드에 요청 상태 저장 금지. 공유 변경 상태는 동기화/락/불변객체 고려.
  • 인코딩: UTF-8 필터 공통 적용(특히 POST 폼).
  • 예외/에러 페이지: 404/500 공통 처리 + 구조적 로깅(MDC로 트레이싱 아이디 남기기 권장).
  • 보안: 세션에 민감정보 저장 자제, 쿠키 HttpOnly/SameSite, CSRF/클릭재킹 대비 헤더.
  • 리소스 관리: DB는 커넥션 풀(DataSource), try-with-resources로 누수 방지.
  • 정적 리소스: 이미지/JS/CSS는 웹서버/CDN으로 서빙해 WAS 부하 절감.
  • 배포: 전통적 war(Tomcat 배치) 또는 Spring Boot(임베디드 Tomcat) 단일 jar.
  • 테스트: 서블릿 API 의존부를 어댑터로 격리하여 단위 테스트 가능하게.

5. Spring Web MVC로 가는 다리

프론트컨트롤러를 직접 만들다 보면 매핑, 바인딩, 검증, 뷰 리졸빙, 예외 처리, 인터셉터 등 공통 요소가 점점 커진다.
Spring MVC는 이를 DispatcherServlet(프론트컨트롤러) 중심으로 표준화했다.

  • 핸들러 매핑: @RequestMapping/@GetMapping
  • 데이터 바인딩/검증: @RequestParam, @ModelAttribute, @Valid
  • : ViewResolver(JSP, Thymeleaf 등)
  • 인터셉터/예외 처리: HandlerInterceptor, @ControllerAdvice

대응표(요약)

[직접 구현 미니 MVC]     <->     [Spring MVC]
FrontController           <->     DispatcherServlet
HandlerMap                <->     HandlerMapping
View(JSP path 직접)       <->     ViewResolver
필터/예외 공통처리        <->     Interceptor/ControllerAdvice
수작업 바인딩/검증        <->     바인딩/검증(Bean Validation)

참고: 실제 배포는 웹서버(Nginx) 로 정적/SSL 처리, 동적은 WAS(Tomcat) 로 프록시하는 구성이 일반적.

results matching ""

    No results matching ""