
JSP 동작 원리Backend/Servlet | JSP2025. 1. 22. 22:15
Table of Contents
JSP는 서버에서 웹 페이지를 동적으로 생성하기 위한 기술이며 실행시점에 서블릿으로 변환되어 처리된다.
JSP가 처리되는 방식에 대해서 더 자세히 살펴보도록 하겠다.
1. JSP 동작 방식
JSP 파일은 클라이언트의 최초 요청이 들어올 때 서블릿으로 변환되고, 이후에는 변환된 클래스를 재사용하는 방식으로 동작한다.
즉, 다음과 같은 단계로 처리된다.
- 클라이언트가 JSP 파일 요청
- JSP엔진(Jasper)이 JSP 파일을 서블릿 자바 코드로 변환하고 컴파일하여 클래스 파일을 생성한다.
- 변환된 서블릿 클래스가 메모리에 로드되고, 클라이언트 요청을 처리한다.
- 최초 변환 후에는 JSP파일이 변환되지 않는 한 매번 변환 및 컴파일 과정을 거치지 않고 기존 변환된 서블릿 클래스를 재사용한다.
그리고 jsp에서 변환된 서블릿은 일반 서블릿과 구분을 위해 HttpJspBase라는 추상 클래스를 상속하여 만들어진다.
1.1 코드 예시
globalCounter.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>globalCounter</title>
</head>
<body>
<%!
private long counter=0;
private long increaseCounter(){
return ++counter;
}
public void jspInit(){
counter=100;
}
%>
<h1>counter:<%=increaseCounter()%></h1>
</body>
</html>
이 JSP가 최초 요청이 되면, JSP엔진은 이것을 다음과 같은 서블릿 코드로 변환한다.
globalCounter_jsp.java
/*
* Generated by the Jasper component of Apache Tomcat
* Version: Apache Tomcat/10.1.30
* Generated at: 2025-01-06 11:51:59 UTC
* Note: The last modified time of this file was set to
* the last modified time of the source file after
* generation to assist with modification tracking.
*/
package org.apache.jsp;
import jakarta.servlet.*;
import jakarta.servlet.http.*;
import jakarta.servlet.jsp.*;
public final class globalCounter_jsp extends org.apache.jasper.runtime.HttpJspBase
implements org.apache.jasper.runtime.JspSourceDependent,
org.apache.jasper.runtime.JspSourceImports,
org.apache.jasper.runtime.JspSourceDirectives {
private long counter=0;
private long increaseCounter(){
return ++counter;
}
public void jspInit(){
counter=100;
}
private static final jakarta.servlet.jsp.JspFactory _jspxFactory =
jakarta.servlet.jsp.JspFactory.getDefaultFactory();
private static java.util.Map<java.lang.String,java.lang.Long> _jspx_dependants;
private static final java.util.Set<java.lang.String> _jspx_imports_packages;
private static final java.util.Set<java.lang.String> _jspx_imports_classes;
static {
_jspx_imports_packages = new java.util.LinkedHashSet<>(4);
_jspx_imports_packages.add("jakarta.servlet");
_jspx_imports_packages.add("jakarta.servlet.http");
_jspx_imports_packages.add("jakarta.servlet.jsp");
_jspx_imports_classes = null;
}
private volatile jakarta.el.ExpressionFactory _el_expressionfactory;
private volatile org.apache.tomcat.InstanceManager _jsp_instancemanager;
public java.util.Map<java.lang.String,java.lang.Long> getDependants() {
return _jspx_dependants;
}
public java.util.Set<java.lang.String> getPackageImports() {
return _jspx_imports_packages;
}
public java.util.Set<java.lang.String> getClassImports() {
return _jspx_imports_classes;
}
public boolean getErrorOnELNotFound() {
return false;
}
public jakarta.el.ExpressionFactory _jsp_getExpressionFactory() {
if (_el_expressionfactory == null) {
synchronized (this) {
if (_el_expressionfactory == null) {
_el_expressionfactory = _jspxFactory.getJspApplicationContext(getServletConfig().getServletContext()).getExpressionFactory();
}
}
}
return _el_expressionfactory;
}
public org.apache.tomcat.InstanceManager _jsp_getInstanceManager() {
if (_jsp_instancemanager == null) {
synchronized (this) {
if (_jsp_instancemanager == null) {
_jsp_instancemanager = org.apache.jasper.runtime.InstanceManagerFactory.getInstanceManager(getServletConfig());
}
}
}
return _jsp_instancemanager;
}
public void _jspInit() {
}
public void _jspDestroy() {
}
public void _jspService(final jakarta.servlet.http.HttpServletRequest request, final jakarta.servlet.http.HttpServletResponse response)
throws java.io.IOException, jakarta.servlet.ServletException {
if (!jakarta.servlet.DispatcherType.ERROR.equals(request.getDispatcherType())) {
final java.lang.String _jspx_method = request.getMethod();
if ("OPTIONS".equals(_jspx_method)) {
response.setHeader("Allow","GET, HEAD, POST, OPTIONS");
return;
}
if (!"GET".equals(_jspx_method) && !"POST".equals(_jspx_method) && !"HEAD".equals(_jspx_method)) {
response.setHeader("Allow","GET, HEAD, POST, OPTIONS");
response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "JSP들은 오직 GET, POST 또는 HEAD 메소드만을 허용합니다. Jasper는 OPTIONS 메소드 또한 허용합니다.");
return;
}
}
final jakarta.servlet.jsp.PageContext pageContext;
jakarta.servlet.http.HttpSession session = null;
final jakarta.servlet.ServletContext application;
final jakarta.servlet.ServletConfig config;
jakarta.servlet.jsp.JspWriter out = null;
final java.lang.Object page = this;
jakarta.servlet.jsp.JspWriter _jspx_out = null;
jakarta.servlet.jsp.PageContext _jspx_page_context = null;
try {
response.setContentType("text/html;charset=UTF-8");
pageContext = _jspxFactory.getPageContext(this, request, response,
null, true, 8192, true);
_jspx_page_context = pageContext;
application = pageContext.getServletContext();
config = pageContext.getServletConfig();
session = pageContext.getSession();
out = pageContext.getOut();
_jspx_out = out;
out.write("\n");
out.write("<html>\n");
out.write("<head>\n");
out.write(" <title>globalCounter</title>\n");
out.write("</head>\n");
out.write("<body>\n");
out.write("\n");
out.write("\n");
out.write("<h1>counter:");
out.print(increaseCounter());
out.write("</h1>\n");
out.write("</body>\n");
out.write("</html>");
} catch (java.lang.Throwable t) {
if (!(t instanceof jakarta.servlet.jsp.SkipPageException)){
out = _jspx_out;
if (out != null && out.getBufferSize() != 0)
try {
if (response.isCommitted()) {
out.flush();
} else {
out.clearBuffer();
}
} catch (java.io.IOException e) {}
if (_jspx_page_context != null) _jspx_page_context.handlePageException(t);
else throw new ServletException(t);
}
} finally {
_jspxFactory.releasePageContext(_jspx_page_context);
}
}
}
이 변환과정의 원리를 설명하자면 아래와 같다.
- JSP 파일의 선언부
<%! ... %>
안에 작성된 필드와 메서드는 서블릿 클래스의 필드와 메서드가 된다. 즉 _jspService 메서드 밖에서 전역으로 선언된다. <%= ... %>
표현식은 실행 결과를 값으로 반환한다.<% ... %>
은 _jspService 메서드 내에서 코드를 실행한다.- 변환된 서블릿은
HttpJspBase
추상 클래스를 상속받아 JSP 전용 라이프사이클 메서드인jspInit
,jspDestroy
,_jspService
를 구현한다.
1.2 Jsp에서 변환된 서블릿이 HttpJspBase를 상속하는 이유
Jsp 파일이 서블릿으로 변환될 때 생성되는 클래스는 HttpJspBase
를 상속한다. 이유는 다음과 같다.
- 라이프 사이클 관리:
HttpJspBase
는 JSP의 생명주기인 초기화(jspInit
), 요청 처리(_jspService
), 소멸(jspDestroy
) 메서드를 정의하고 있으며, 이를 통해 JSP의 동작흐름을 제어한다.
생명주기의 로직 구성은 톰캣의 역할이다. - 선언부(
<%! %>
), 스크립틀릿(<% %>
), 표현식(<%= %>
) 등을 처리해준다. - 내장객체: Jsp에서 내장객체를 사용할 수 있게한다. 내장객체의 종류는 아래와 같다.
객체 타입 설명 page jakarta.servlet.jsp.HttpJspPage JSP 페이지의 Servlet 인스턴스 config jakarta.servlet.ServletConfig Servlet 초기화 설정 객체 request HttpServletRequest 요청 객체 response HttpServletResponse 응답 객체 out jakarta.servlet.jsp.JspWriter page content 출력용 스트림 session jakarta.servlet.http.HttpSession 세션 application jakarta.servlet.ServletContext ServletContext pageContext jakarta.servlet.jsp.PageContext JSP page의 실행 context exception java.lang.Throwable 처리되지 않은 에러나 예외
2. 톰캣에서 Jsp 요청을 처리하는 방법
2.1 Tomcat에 구현되어 있는 JspServlet
Tomcat에는 JSP 요청을 처리하기 위한 서블릿인 JspServlet이 존재한다.
Tocmat의 conf/web.xml 파일에는 JspServlet에 관한 매핑이 다음과 같이 정의되어 있다.
<servlet>
<servlet-name>jsp</servlet-name>
<servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
...
</servlet>
<servlet-mapping>
<servlet-name>jsp</servlet-name>
<url-pattern>*.jsp</url-pattern>
<url-pattern>*.jspx</url-pattern>
</servlet-mapping>
- 이 매핑 덕분에
.jsp
확장자를 가진 모든 요청은JspServlet
이 가로채게 된다.
2.2 JSP 동작 과정 정리
- JspServlet은
*.jsp
로 오는 모든 요청을 가로챈다. - 클라이언트가 Jsp파일을 최초 요청 시
JspServlet
이 JSP 파일을HttpJspBase
를 상속한 서블릿 자바 코드로 변환시킨 후 요청을 처리한다. - 변환된 자바 코드는 컴파일되어
.class
파일로 만들어지며, 이 파일은 톰캣의/work/Catalina/localhost/
디렉토리에 저장된다.
(톰캣의 bin 폴더 안에 있는startup.sh
로 톰캣을 실행해야 확인할 수 있다.) - 최초 변환 이후에는 JSP 파일이 변경되지 않는 한 이미 변환된 서블릿 클래스를 재사용한다.
'Backend > Servlet | JSP' 카테고리의 다른 글
서블릿 매핑 우선순위 (0) | 2025.01.31 |
---|---|
Jsp 사용할 때 루트 경로에서 index.jsp가 작동하는 이유 (0) | 2025.01.28 |
WAS(Tomcat)가 필요한 이유 (0) | 2024.12.24 |
Apache Tomcat 버전별 Jakarta EE 의존성 설정 (2) | 2024.12.10 |
@nuheajiohc :: nuheajiohc
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!