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) 로 프록시하는 구성이 일반적.