복잡한뇌구조마냥

[JAVA] 서블릿(Servlet) 본문

BE/JAVA

[JAVA] 서블릿(Servlet)

지금해냥 2025. 8. 8. 09:06

1) 서블릿이 뭐고, 왜 쓰나

서블릿은 Java로 작성된 서버 사이드 컴포넌트로, 브라우저 요청(HTTP)을 받고 응답(HTML/JSON 등)을 만들어 반환합니다.
JSP가 “화면 템플릿”에 가깝다면, 서블릿은 컨트롤러(요청 분기/비즈니스 로직 호출) 역할을 주로 담당합니다.

⚠️ Tomcat 10+에서는 패키지가 jakarta.servlet.* 입니다. (Tomcat 9 이하는 javax.servlet.*)
환경에 맞춰 import 경로만 바꾸면 됩니다.


2) 최소 예제: HelloServlet

// Tomcat 10+ (Jakarta) 기준
package com.example;

import jakarta.servlet.*;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.*;
import java.io.IOException;
import java.io.PrintWriter;

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        resp.setCharacterEncoding("UTF-8");
        resp.setContentType("text/html; charset=UTF-8");

        PrintWriter out = resp.getWriter();
        out.println("<!doctype html>");
        out.println("<html><head><meta charset='UTF-8'><title>Hello</title></head>");
        out.println("<body><h1>안녕하세요, Servlet!</h1></body></html>");
    }
}
  • @WebServlet("/hello") 로 라우팅
  • doGet, doPost 등 HTTP 메서드별 처리 ( REST API )

3) JSP로 포워딩(Controller → View)

컨트롤러 서블릿에서 데이터를 request에 담아 JSP로 넘깁니다.

- java

@WebServlet("/articles")
public class ArticleListServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {

        // 예시 데이터
        var articles = java.util.List.of(
            new Article(1, "첫 글"), 
            new Article(2, "둘째 글")
        );

        req.setAttribute("articles", articles);
        req.getRequestDispatcher("/WEB-INF/views/article/list.jsp")
           .forward(req, resp); // 서버 내부 포워드 (URL 안 바뀜)
    }

    static class Article {
        public int id; public String title;
        public Article(int id, String title) { this.id = id; this.title = title; }
        public int getId(){ return id; } public String getTitle(){ return title; }
    }
}
- jsp
 
<!-- /WEB-INF/views/article/list.jsp -->
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="http://jakarta.ee/jstl/core" %>
<!doctype html>
<html>
<head><meta charset="UTF-8"><title>게시글 목록</title></head>
<body>
  <h1>게시글 목록</h1>
  <table border="1" cellspacing="0" cellpadding="6">
    <tbody>
    <c:forEach var="article" items="${articles}">
      <tr>
        <td>${article.id}</td>
        <td>
          <a href="${pageContext.request.contextPath}/article/detail?id=${article.id}">
            ${article.title}
          </a>
        </td>
      </tr>
    </c:forEach>
    </tbody>
  </table>
</body>
</html>
  • 포워드는 서버 내부 이동이라 요청/응답이 유지됩니다 (속성도 유지).
  • 보안상 JSP는 /WEB-INF 아래 두는 게 좋습니다(직접 접근 차단).

4) 상세 보기 & 파라미터 처리

- java
 
@WebServlet("/article/detail")
public class ArticleDetailServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {

        String idParam = req.getParameter("id"); // e.g. ?id=2
        if (idParam == null) {
            resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "id가 없습니다.");
            return;
        }

        int id;
        try { id = Integer.parseInt(idParam); }
        catch (NumberFormatException e) {
            resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "id가 숫자가 아닙니다.");
            return;
        }

        // 실제로는 Service/Repository에서 조회
        var article = new Article(id, "상세 글 " + id);
        req.setAttribute("article", article);
        req.getRequestDispatcher("/WEB-INF/views/article/detail.jsp").forward(req, resp);
    }

    static class Article {
        private final int id; private final String title;
        Article(int id, String title){ this.id = id; this.title = title; }
        public int getId(){ return id; } public String getTitle(){ return title; }
    }
}
- jsp
 
<!-- /WEB-INF/views/article/detail.jsp -->
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!doctype html>
<html>
<head><meta charset="UTF-8"><title>상세</title></head>
<body>
  <h1>게시글 상세</h1>
  <div>번호: ${article.id}</div>
  <div>제목: ${article.title}</div>
  <p><a href="${pageContext.request.contextPath}/articles">목록으로</a></p>
</body>
</html>

5) 폼 처리(POST) + 리다이렉트

- java
 
@WebServlet("/article/create")
public class ArticleCreateServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        req.getRequestDispatcher("/WEB-INF/views/article/create.jsp").forward(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        req.setCharacterEncoding("UTF-8");

        String title = req.getParameter("title");
        if (title == null || title.isBlank()) {
            req.setAttribute("errorMessage", "제목을 입력하세요.");
            req.getRequestDispatcher("/WEB-INF/views/article/create.jsp").forward(req, resp);
            return;
        }

        // TODO: DB 저장 (Service 호출)
        // 저장 후 PRG(Post/Redirect/Get) 패턴
        resp.sendRedirect(req.getContextPath() + "/articles");
    }
}
- jsp
 
<!-- /WEB-INF/views/article/create.jsp -->
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!doctype html>
<html>
<head><meta charset="UTF-8"><title>작성</title></head>
<body>
  <h1>게시글 작성</h1>
  <c:if test="${not empty errorMessage}">
    <p style="color:red">${errorMessage}</p>
  </c:if>
  <form method="post" action="${pageContext.request.contextPath}/article/create">
    <input type="text" name="title" placeholder="제목" />
    <button type="submit">등록</button>
  </form>
</body>
</html>
  • 리다이렉트는 URL이 변경되고 새 요청으로 처리됩니다(새로고침 중복 방지).

6) 공통 인코딩/로그 처리: Filter

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

@WebFilter("/*")
public class CharacterEncodingFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        request.setCharacterEncoding("UTF-8");
        response.setCharacterEncoding("UTF-8");
        chain.doFilter(request, response);
    }
}
  • 모든 요청에 UTF-8 강제 → 폼 한글 깨짐 방지
  • 로깅/인증 체크도 필터에서 처리 가능

7) web.xml vs 애너테이션

(1) 애너테이션(@WebServlet/@WebFilter) — 요즘 기본

  • 클래스 위에 바로 매핑. 간단하고 빠름.

(2) web.xml — 레거시/세밀한 설정에 유용

<!-- WEB-INF/web.xml -->
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee" version="5.0">
  <servlet>
    <servlet-name>hello</servlet-name>
    <servlet-class>com.example.HelloServlet</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>hello</servlet-name>
    <url-pattern>/hello</url-pattern>
  </servlet-mapping>
</web-app>

8) 추천 폴더 구조(예시)

src/main/java
 └─ com.example
     ├─ filter
     │   └─ CharacterEncodingFilter.java
     ├─ controller
     │   ├─ ArticleListServlet.java
     │   ├─ ArticleDetailServlet.java
     │   └─ ArticleCreateServlet.java
     ├─ service
     │   └─ ArticleService.java
     └─ repository
         └─ ArticleRepository.java

src/main/webapp
 ├─ WEB-INF
 │   └─ views
 │       └─ article
 │           ├─ list.jsp
 │           ├─ detail.jsp
 │           └─ create.jsp
 └─ index.html
  • 컨트롤러-서비스-리포지토리 레이어 분리(MVC)
  • JSP는 /WEB-INF/views 밑으로

9) 실무 팁 요약

  • JSP에는 비즈니스 로직 넣지 않기: JSTL/EL로 화면만
  • PRG 패턴으로 중복 제출 방지
  • 필터로 UTF-8 고정
  • Tomcat 10+면 jakarta.* 패키지 사용
  • 공통 레이아웃은 include/커스텀 태그로 재사용
LIST