서블릿
날짜: 2021년 7월 11일
학습목표
서블릿... 혼자서 웹프로젝트를 만들고 서블릿을 돌려보려하니 실패했다... 충격적이다. 자바로 웹 개발을 하는데에 있어 가장 기초라고 할수있는 서블릿에 대해서 알아보도록 하자
- 서블릿이란?
- 서블릿 컨테이너? 서블릿 컨텍스트?
- 서블릿의 동작원리
- 서블릿의 동작원리 그림
- 비동기 서블릿
서블릿(Servlet)
JSP이전의 Java의 웹 애플리이케이션을 만드는 표준이었다. 현재 JSP도 컴파일시 서블릿으로 변환된다. 즉 JSP = 서블릿이다. (JSP를 볼때 이 JSP가 서블릿으로 변하면 어떻게 되는걸까 생각해보자!)
서블릿 이전에는 'CGI프로그래밍'을 통해서 웹 애플리케이션을 개발했다. 그러나, 이 CGI프로그래밍은 요청이 들어올때마다 '프로세스'를 생성하는 방식으로, 자원낭비가 심한 단점이 있었다. 이에반해 서블릿을 요청이 들어오면 '쓰레드'를 생성하는 방식으로 자원사용에 있어 훨씬 유리하다.
서블릿 컨테이너? 서블릿 컨텍스트
서블릿 컨테이너, 서블릿 컨텍스트? 도대채 무슨말일까??
먼저 서블리 컨테이너에 대해 알아보자.
서블릿(Servelt)은 위에서 알아본 동적 웹페이지를 만드는 기술중 1개로, JSP이전의 Java 표준 이었다.
컨테이너(Container)는 사전적의미로는 그릇,용기를 뜻하는 말로, 서블릿 컨테이너를 직역하면 서블릿이 담긴 그릇, 용기 정도로 해석할 수 있겠다.
그럼 여기서 생각해보자, 서블릿들이 담겨있는 곳은 어디인가??
바로 ApacheTomcat등의 WAS(WebApplicationServer)이다. 웹 프로젝트를 만들때 Servlet클래스의 위치를 잘 살펴보자. Java에서 기본적으로 제공하는 클래스가 아니다!!...
즉, 서블릿은 WAS에서 생성하고, 관리한다. 즉, WAS가 서블릿 컨테이너가 되는것이다.
그럼, 서블릿 컨텍스트는 무엇일까??
여기서 먼저 컨텍스트라는 단어의 뜻에 대해서 알아보자.
사전적 의미로는 '문맥'이라는 뜻을 가지고있다. 컨텍스트는 프로그래밍에 있어서, 참 많이 쓰이는 단어다. CPU가 하는 컨텍스트 스위칭, 서블릿 컨텍스트, 웹 컨텍스트 등.... 여러곳에서 쓰여, 하나의 단어로 명확히 정의하기 어려움이 있다.
내가 느낀 컨텍스트란, 코드에 따라 실행되는 프로그램의 흐름이다. 웹 컨텍스트는 웹 애플리케이션을 말한다. 즉 웹의 문맥이 곧 웹 애플리케이션이다. 따지고 보면 맞는 말이다. 결국 애플리케이션은 논리적으로 작성된(소스코드) 하나의 글 아닌가? 또한 CPU의 컨텍스트 스위칭도, CPU자원을 할당받는 쓰레드나 프로세스가 바뀌는것을 의미한다. 쓰레드와 프로세스란 결국 메모리에 로드되어 살아움직이는 프로그램들이고, 이 프로그램들은 애플리케이션이라 볼 수있으며, 애플리케이션들은 곧 위에서 말한 소스코드로 이루어진 글이다.
그럼 서블릿 컨텍스트란 무엇일까?? 서블릿의 문맥?..
서블릿 컨텍스트란, WAS의 실행과 동시에, 웹 컨텍스트마다 1개씩 생성되며, 애플리케이션 전체의 공통자원 이나 정보를 바인딩, 생성된 서블릿들끼리 공유하여 사용한다.
서블릿 컨텍스트의 특징은 다음과 같다.
- 서블릿과 컨테이너(WAS)마다 하나의 서블릿 컨텍스트가 생성된다.
- 서블릿끼리 자원(데이터)를 공유하는데 사용된다.
- 컨테이서 실행 시 생성되고, 종료시 소멸한다.
서블릿 컨텍스트의 개념을 위의 그림에 그려보면 다음과 같다.
서블릿의 라이플 사이클과 동작 원리
서블릿은 어떻게 동작할까?? 서블릿의 라이프 사이클과 멀티쓰레드 환경에서 동작하는 원리를 간단하게 웹 애플리케이션을 만든후 디버깅을 통해 알아보자.
서블릿의 라이프 사이클
먼저 서블릿의 라이프 사이클에 대해서 알아보자. 예제에서 사용될 코드이다.
package webServletExam;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet(urlPatterns = "/hello")
public class Exam1 extends HttpServlet{
public Exam1() {
System.out.println("servlet created");
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("service call");
}
@Override
public void destroy() {
System.out.println("destroy call");
}
@Override
public void init(ServletConfig config) throws ServletException {
System.out.println("init call");
}
}
처음 요청시 나타나는 콘솔창을 보자, /hello 요청을 통해 맵핑된 서블릿을 찾아 서블릿을 생성, init, 그리고 service를 진행하는 모습을 볼수있다. 그럼, 계속 요청을 하면 서블릿은 요청마다 계속 생성되는 걸까??
아니다. 서블릿은 한번 만들어지면, 응답했다고 사라지지 않는다는 것을 알수있다. 여러번 새로고침을 통해 요청을 해보았지만, 생성과 초기화는 최초1회 이후 진행되지 않고, service부분만 계속 호출되는 것을 볼수있다.
그럼 destroy는 언제 호출되는걸까?...
클래스의 내용을 수정하자, 서버가 재시작되었다. 그리고 서버가 재시작되는 과정에서, 종료되는 순간 destroy가 호출되었다. 즉, 서블렛은 최초 1회 생성 및 초기화 후 서버가 종료될때까지 소멸되지 않고 존재하는 객체임을 알수있다. (singleton)
서블릿의 동작원리
그럼, 서블릿이 multithread환경에서 어떻게 동작하는지 알아보자. 해당 예제에 사용될 코드이다.
package webServletExam;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet(urlPatterns = "/hello")
public class Exam1 extends HttpServlet{
private String name;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// TODO Auto-generated method stub
super.doGet(req, resp);
req.setCharacterEncoding("utf-8");
resp.setContentType("text/html; charset=urf-8");
name = req.getParameter(name);
PrintWriter out = resp.getWriter();
out.println("<html>");
out.println("<head><title>인사</title></head>");
out.println("<body>");
out.println("안녕하세요");
out.println(name);
out.println("님");
out.println("</body></html>");
}
}
hello servlet에 name = park로 파라미터를 넘겼을 때다. 쓰레드7번에 할당된것을 볼수있다.
다음은 새로운 브라우저인 Microsoft Edge를 통해서 접속해 보았다. 이번에는 파라미터 값을 park2로 넘겨주었다. 이번엔 쓰레드11번에 할당된것을 볼수있다.
그럼, 이 두 요청에 대해서 디버깅으로 인한 일시정지를 풀고 진행해 보겠다.
추가적으로 인터넷 익스플로러까지 진행해보았다...
이상하다. 모든 브라우저에서 park으로 출력되고 있다. 왜?... 분명 파라미터값으로는 park, park2, pakr3를 넘겼으나, 결과값은 park으로 동일하게 출력되었다.
왜 그럴까? 앞서 배운 서블릿의 라이프사이클로 설명이 가능하다. 서블릿은 싱글톤으로, 요청마다 객체가 생성되지 않는다. 즉, 최초 생성 후 이후의 요청부터는 최초 생성되었던 서블릿 객체가 요청을 처리한다. 이때 서블릿의 전역변수로 선언했던 name 변수에 대해서 3개의 요청의 쓰레드가 경쟁상태에 돌입하게 되는것이다. 알기 쉽게 그림을 그려 알아보자.
즉 위 예제코드 부분의 전역변수를 doGet()메소드의 안에서 선언해주도록 해야 동시성 문제가 없다. 혹여나, 전역변수를 선언하는 일은 하지 말도록 하자!
@WebServlet(urlPatterns = "/hello")
public class Exam1 extends HttpServlet{
~~private String name;~~
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doGet(req, resp);
req.setCharacterEncoding("utf-8");
resp.setContentType("text/html; charset=urf-8");
String name = req.getParameter(name);
...
}
}
클라이언트 요청 ~ 서블릿의 응답
비동기 JSP
비동기JSP에 대해 알아보자. 먼저 JSP = 서블릿 이라고했다. 즉, 비동기JSP는 결국 비동기로 동작하는 서블릿이다. 먼저 비동기JSP가 어떻게 동작하는지 알아보자.
위의 그림을 보면, 여러개의 요청에 대해서, 멀티쓰레드를 통해 여러 요청을 처리하고 있다. 즉, 쓰레드 1개당 1개의 요청을 담당하고, 응답까지 처리해주는 것이다. 이렇게 되면 싱크와 블락의 문제로, 쓰레드가 응답이 반환되기까지 연산이 없어도 기다려야 하는 경우가 생긴다.
반면에 비동기JSP는, 쓰레드가 모든 연산을 비동기로 처리하는것이다. 즉, 서블릿의 서비스함수를 호출하고, 쓰레드는 응답까지 대기하지 않는다. 다시 쓰레드풀로 반환된다. 이후, 서블릿의 응답이 반환되면, 쓰레드풀에서 새로운 쓰레드가 할당되어 응답을 클라이언트에게 전달하는 방식이다. (NginX와 비슷한 방식인것 같다.)
JSP에서 서블릿으로!
JSP = 서블릿이다. 그럼, JSP가 어떻게 서블릿으로 바뀌는지 알아보자. 예제 JSP코드이다.
<%@ page language="java" contentType="text/html; charset=EUC-KR"
pageEncoding="EUC-KR"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="EUC-KR">
<title>Insert title here</title>
</head>
<body>
<% System.out.println("hello JSP"); %>
<%= "hello there, it's JSP time" %>
</body>
</html>
JSP는 서버가 구동되면, 톰캣에서 JSP를 서블릿으로 변환해준다.
workspaceRoot.metadata.plugins\org.eclipse.wst.server.core\tmp0\work\Catalina\localhost\webServletExam\org\apache\jsp\jsp 로 jsp가 저장되는 경로를 가보자.
jsp파일이 java파일로 변환되고, 또 그 java파일이 class파일로 컴파일 된것을 알수있다. 즉, jsp파일은 톰캣에 의해 java파일로 변환되고, JVM의 컴파일러에 의해 class파일로 컴파일 되는것이다. 그럼, java파일로 어떻게 변했을까??
/*
* Generated by the Jasper component of Apache Tomcat
* Version: Apache Tomcat/9.0.36
* Generated at: 2021-07-12 05:32:17 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.jsp;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.jsp.*;
public final class helloJsp_jsp extends org.apache.jasper.runtime.HttpJspBase
implements org.apache.jasper.runtime.JspSourceDependent,
org.apache.jasper.runtime.JspSourceImports {
private static final javax.servlet.jsp.JspFactory _jspxFactory =
javax.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.HashSet<>();
_jspx_imports_packages.add("javax.servlet");
_jspx_imports_packages.add("javax.servlet.http");
_jspx_imports_packages.add("javax.servlet.jsp");
_jspx_imports_classes = null;
}
private volatile javax.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 javax.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 javax.servlet.http.HttpServletRequest request, final javax.servlet.http.HttpServletResponse response)
throws java.io.IOException, javax.servlet.ServletException {
if (!javax.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 javax.servlet.jsp.PageContext pageContext;
javax.servlet.http.HttpSession session = null;
final javax.servlet.ServletContext application;
final javax.servlet.ServletConfig config;
javax.servlet.jsp.JspWriter out = null;
final java.lang.Object page = this;
javax.servlet.jsp.JspWriter _jspx_out = null;
javax.servlet.jsp.PageContext _jspx_page_context = null;
try {
response.setContentType("text/html; charset=EUC-KR");
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("\r\n");
out.write("<!DOCTYPE html>\r\n");
out.write("<html>\r\n");
out.write("<head>\r\n");
out.write("<meta charset=\"EUC-KR\">\r\n");
out.write("<title>Insert title here</title>\r\n");
out.write("</head>\r\n");
out.write("<body>\r\n");
System.out.println("hello JSP");
out.write('\r');
out.write('\n');
out.print( "hello there, it's JSP time" );
out.write("\r\n");
out.write("</body>\r\n");
out.write("</html>");
} catch (java.lang.Throwable t) {
if (!(t instanceof javax.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);
}
}
}
굉장히 복잡하다... 여기서 눈여겨 볼 부분은
public void _jspInit() {
}
public void _jspDestroy() {
}
public void _jspService(final javax.servlet.http.HttpServletRequest request, final javax.servlet.http.HttpServletResponse response)
throws java.io.IOException, javax.servlet.ServletException {
...
}
부분이다. 기존 서블릿의 init,destroy,service 메소드가 저렇게 변환이 되는것이다. 또, JSP에서 작성한 코드들은 _jspService() 메소드 안에 모두 구현되는것을 확인할수있다. 왜냐하면, HTML은 결국 응답의 결과인데, 서블릿에서 응답에 관여하는 부분은 service()밖에 없기 때문이다.
out.write("\r\n");
out.write("<!DOCTYPE html>\r\n");
out.write("<html>\r\n");
out.write("<head>\r\n");
out.write("<meta charset=\"EUC-KR\">\r\n");
out.write("<title>Insert title here</title>\r\n");
out.write("</head>\r\n");
out.write("<body>\r\n");
System.out.println("hello JSP");
out.write('\r');
out.write('\n');
out.print( "hello there, it's JSP time" );
out.write("\r\n");
out.write("</body>\r\n");
out.write("</html>");
Feedback
JSP와 서블릿에 대해서 안개낀듯 뿌옇던 시야가 조금은 밝아진것 같다. 이제 스프링MVC와의 관계를 파악, 좀더 명확하게 내가 어떻게 서블릿을 이용하고 있었는지 알아보면 될것같다!!
알게된 점
- 서블릿 이란?
- 서블릿 컨테이너와 서블릿 컨텍스트
- 서블릿의 라이프 사이클과 동작원리
- 비동기JSP
- JSP의 변환과정
알아야 할 점
- 스프링MVC와 연계한 JSP와 서블릿의 사용법
References
[JSP] ServletContext(Application)
multithread에서 Servlet 사용시 고려할 사항
'프로그래밍 > Java' 카테고리의 다른 글
Comparator VS Comparable (0) | 2021.07.26 |
---|---|
정적 언어와 동적언어 그리고 덕 타이핑 (0) | 2021.07.22 |
Spring VS Spring Boot 의 배포차이 (0) | 2021.07.03 |
Non-Blocking,Blocking VS Async,Sync (0) | 2021.06.19 |
네트워크 통신(2) (0) | 2021.06.15 |