JSP/Servlet - 상태관리, 포워딩, 재스퍼!
상태관리
리다이렉트의 처리순서는 다음과 같다.
- 클라이언트가 url에 해당하는 html페이지 요청
- 서버는 url에 해당하는 서블릿 혹은 jsp객체를 반환
- 만약 jsp페이지에
response.sendRedirect(url2)
메서드가 있다면 클라이언트에게 url2로 다시 접속하라고 알림 - 클라이언트가
url2
에 해당하는 html페이지 요청 - 서버는
url2
에 해당하는 서블릿 혹은 jsp객체를 반환
위와 같은 처리 순서에서 클라이언트가 get
방식, post
방식으로 넘긴 파라미터를 서버에서 계속 유지하고 싶다면 상태관리가 필요하다.
<%
String name = "admin";
int age = 20;
String params = String.format("name=%s&age=%d",name,age);
%>
...
<body>
<a href="ex01_02.jsp?<%= params %>">ex01_02.jsp</a>
</body>
위와같이 name
과 age
이라는 파라미터를 ex01_02.jsp
로 넘긴다.
ex01_02.jsp
에선 response.sendRedirect
를 통해 바로 ex01_03.jsp
로 다시 접속하라고 클라이언트에게 전달한다.
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%
String location = String.format("ex01_03.jsp", name, age);
response.sendRedirect(location);
%>
결국 클라이언트는 ex01_03.jsp
url을 다시 요청하는 과정에서 파라미터 유지가 되지 않는다.
따라서 유지를 해주고 싶다면 클라이언트에게 전송하는 url뒤에 파라미터 정보를 붙여 보내야한다.
<%
String name = request.getParameter("name");
String age = request.getParameter("age");
String location = String.format("ex01_03.jsp?name=%s&age=%s", name, age);
System.out.println(location);
response.sendRedirect(location);
%>
sendRedirect
를 사용하려면 위와같이 유지해야 하지만 포워딩을 사용하면 서버에서 바로 처리하기 때문에 url뒤에 지저분하게 파리미터를 붙일 필요 없다.
포워딩
response.sendRedirect
를 사용하면 서버가 클라이언트에게 재 연결을 위한 urld을 다시 보내게 되고 클라이언트가 서버에게 다시 요청해야 한다.
반면 포워딩을 사용하면 서버에서 바로 연결을 해주기 때문에 클라이언트가 서버에게 다시 요청할 일이 없다.
String path = "ex01_03.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(path);
dispatcher.forward(request, response);
foward를 위해 RequestDispatcher
객체가 필요하고 이 객체는 request.getRequestDispatcher("이동시킬 url")
을 통해 받을 수 있다.
dispatcher.forward()
메서드의 매개변수로 request
와 response
객체를 넘기기 때문에 path
변수 뒤에 파라미터를 붙일 필요가 없다.
즉 클라이언트가 <a href="ex01_02.jsp?<%= params %>">ex01_02.jsp</a>
를 통해 ex01_02.jsp
파일을 요청하면
서버의 ex01_02.jsp
는 실제로 ex01_03.jsp
으로 포워드를 하고 있고 ex01_03.jsp
을 출력결과를 클라이언트에게 반환한다.
또한 클라이언트의 브라우저 주소창엔 http://localhost/jspPro/days03/ex02_info.jsp
이런식으로 뜨겠지만 실제 출력된 것은 서버의 ex01_03.jsp
파일 결과물이다.
그리고 MVC패턴으로 설계하기 위해 Model
, View
, Controller가
DTO
객체를 서로 전송해야 하는데, 이 DTO
객체를 url뒤에?
를 붙여 전달할 순 없으니 request
객체에 데이터를 포함시켜 통째로 넘겨야 한다.
request
객체를 전송하기 위해선 dispatcher.forward()
메서드를 사용한 포워딩이 필수이다!
request Scope(요청 스코프)
클라이언트가 서버에게 html페이지 요청과 동시에 넘긴 파라미터들을 request
객체로 가져올 수 있었다.
서버에선 이 데이터를 가지고 포워딩을 통해 각 서블릿 객체에서 작업을 해야 하는데
이 작업기간 동안 사용자가 넘긴 파라미터(request
객체)가 유지되어야 한다.
작업기간동안 요청객체를 유지하는 것을 request Scope
(범위)라 한다.
request
가 파라미터만 유지하는것이 아니라 파라미터로 인해 생긴 데이터 혹은 DB에서 가져온 dto등의 데이터도 보관하고 있어야 하는데
request.setAttribute()
메서드를 사용해 requset
객체에 데이터를 포함시킬 수 있다.
예를 들어 클라이언트가 서버에게 10번 부서의 사원을 모두 출력해달라고 다음과 같이 select
태그에서 사원번호를 선택해 서버로 넘기다 할 때
<form action="" method="get">
<select name="deptno" id="deptno">
<option value="10">ACCOUNTING</option>
<option value="20">RESEARCH</option>
<option value="30">SALES</option>
<option value="40">OPERATIONS</option>
</select>
</form>
...
...
<script>
var flag = true;
$("select").on("click", function() {
if(flag)
{
flag = false;
return;
}
var deptno = $(this).val();
$("form")
.attr("action", "ex04_emp")
.submit();
flag = true;
});
</script>
선택한 부서번호를 파라미터로 넘기며 ex04_emp
url을 요청하는데
이는 web.xml
에 정의된 mapping
에 따라 Ex04_emp
서블릿 객체를 호출한다.
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String deptno = request.getParameter("deptno");
String sql = "select empno, ename, job, hiredate, deptno from emp";
if( deptno != null )
sql +=" where deptno = " + deptno;
Connection con = null;
Statement stmtEmp;
ResultSet rsEmp;
ArrayList<EmpDTO> elist = null;
try {
con = DBConn.getConnection();
stmtEmp = con.createStatement();
rsEmp = stmtEmp.executeQuery(sql);
if( rsEmp.next()){
elist = new ArrayList<>();
do{
EmpDTO dto = new EmpDTO();
dto.setEmpno( rsEmp.getInt("empno") );
dto.setDeptno( rsEmp.getInt("deptno") );
dto.setEname( rsEmp.getString("ename") );
dto.setHiredate(rsEmp.getDate("hiredate"));
dto.setJob( rsEmp.getString("job"));
elist.add(dto);
}while(rsEmp.next());
}
rsEmp.close();
stmtEmp.close();
DBConn.close();
} catch (Exception e) {
e.printStackTrace();
}
String path = "ex04_ok.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(path);
request.setAttribute("list", elist);
dispatcher.forward(request, response);
}
Ex04_emp
서블릿 객체는 DB와 연결해서 부서리스트를 request.setAttribute("list", elist);
메서드를 통해 ex04_ok.jsp
로 전달한다.
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%
ArrayList<EmpDTO> elist = (ArrayList<EmpDTO>)request.getAttribute("list");
%>
<!DOCTYPE html>
<html>
...
...
<body>
<%
if(elist == null)
{
out.print("사원이 존재하지 않습니다.");
}
else
{
Iterator<EmpDTO> ir = elist.iterator();
while(ir.hasNext()){
EmpDTO dto = ir.next();
%>
<h2><%= dto.getEname() %>(<%= dto.getEmpno() %>)</h2>
<p class="title"><%= dto.getDeptno() %>-<%= dto.getJob() %></p>
<p><%= dto.getHiredate() %></p>
}
}
%>
</body>
</html>
코드를 보면 request.getAttribute("list");
을 통해 Ex04_emp
서블릿 객체가 보낸 list
를 받는다.
jsp - 재스퍼
지금까지 jsp파일 안에서 html코딩과 java코딩을 같이했는데 상당히 어색하다.
서블릿 객체는 다음과 같이 PrintWriter
객체를 만들고 출력버퍼에 html문자열을 집어넣고 클라이언트에게 전달하였는데.
PrintWriter out = resp.getWriter();
out.print("<!DOCTYPE html>");
사실 jsp파일도 tomcat에 의해 위와같은 서블릿 객체로 변환된다.
스크립트릿 안에서 java문법이 틀리게 되면 다음과 같은 오류가 발생한다.
org.apache.jasper.JasperException: Unable to compile class for JSP:
org.apache.jasper.compiler
재스퍼(Jasper)는 톰켓의 JSP엔진이다. 제스퍼는 JSP파일을 파싱하여 서블릿(JavaEE) 코드로 컴파일 한다.
JSP 파일의 변경을 감지하여 리컴파일 작업도 수행한다.
재스퍼에 의해 .java파일로 변환된 jsp파일은 아래위치에 저장된다.(프로젝트 폴더에서 jsp파일명을 검색해보자)
\workspace위치\.metadata\.plugins\org.eclipse.wst.server.core\tmp0\work\Catalina\localhost\프로젝트명\org\apache\jsp\
다음 jsp파일을 재스퍼가 어떻게 파싱해서 서블릿 객체로 만드는지 확인해보자.
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%!
int age = 20;
public void dispInfo()
{
System.out.println("displayInfo() callled...");
}
%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<%
String name = "홍길동";
%>
<h3>이름은 <%= name %>입니다.</h3>
<%
out.print("out.print() called...");
%>
</body>
</html>
재스퍼가 변환한 ex03_005ftest_jsp.java
라는 파일이 위의 디렉터리 위치에 생성되고 안을 살펴 보면 좀 복잡하긴 해도 우리가 지금까지 써온 서블릿 객체와 비슷한 점이 많다.
public final class ex03_005ftest_jsp
extends org.apache.jasper.runtime.HttpJspBase
implements org.apache.jasper.runtime.JspSourceDependent,
org.apache.jasper.runtime.JspSourceImports {
int age = 20;
public void dispInfo()
{
System.out.println("displayInfo() callled...");
}
// 스크립트릿의 변수와 선언부의 변수선언의 차이는 다음과 같다.
...
...
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 {
...
...
}
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=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("\r\n");
out.write("\r\n");
String name = "홍길동";
// _jspService 메서드의 지역변수로 잡힌다.
out.write('\r');
out.write('\n');
out.write("\r\n");
out.write("<!DOCTYPE html>\r\n");
out.write("<html>\r\n");
out.write("<head>\r\n");
out.write("</head>\r\n");
out.write("<body>\r\n");
out.write("<h3>이름은 ");
out.print( name );
out.write("입니다.</h3>\r\n");
out.print("out.print() called...");
out.write("\r\n");
out.write("</body>\r\n");
out.write("</html>");
}
catch (java.lang.Throwable t) {
...
...
}
finally {
...
...
}
}
}
jsp파일에서 작성한 주석까지 .java
파일 안에 쓰여있다.
또한 jsp파일에서 <%! ... %>
선언부에서 정의했던 변수age
와 메서드dispInfo()
는 서블릿 객체의 전역변수로 초기화 되었다.
int age = 20;
public void dispInfo()
{
System.out.println("displayInfo() callled...");
}
반면 스크립트릿에서 정의한 <% String name = "홍길동"; %>
은 _jspService
메서드 안의 지역변수로 잡힌다.
_jspService
메서드를 보면 서블릿 객체의 오버라딩된 service()
메서드와 형식이 비슷하다.
public void _jspService(final javax.servlet.http.HttpServletRequest request, final javax.servlet.http.HttpServletResponse response)
get방식, post방식 요청 모두 _jspService
메서드에 의해 처리되는 걸 유추할 수 있다….
<%= %>
표현부에서 출력했던 것도 out.print()
메서드로 바뀌어 있다.
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;
...
...
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;
그리고 jsp에서 사용하는 기본객체 application
, config
, session
, out
등이 정의되어 있는데
지금까지 저런 기본객체를 jsp파일에서 사용할 수 있던것도 위와 같이 재스퍼가 기본적으로 파싱하면서 자동 생성해주기 때문이다.
즉 jsp도 tomcat
의 재스퍼에게 컴파일 당할 뿐이지 일종의 서블릿객체이다.