05. JSP
이번 포스트에서는 JSP와 Servlet의 차이를 살펴보고 JSP의 구성요소를 학습해보자.
JSP
기존 Servlet의 문제점과 JSP
Servlet은 Java에서 동적 애플리케이션을 만들기 위해서 꼭 필요한 녀석인데 치명적인 단점이 존재한다.
private void handleRequest(HttpServletRequest request, HttpServletResponse response)
throws IOException {
String danStr = request.getParameter("dan"); // 1. 파라미터 확인 및 검증
StringBuilder data = new StringBuilder();
if (danStr == null || danStr.isEmpty()) {
data.append("dan을 입력하세요.");
} else {
int dan = Integer.parseInt(danStr); // 2. 비지니스 로직 처리
for (int i = 1; i < 10; i++) {
data.append(dan).append("*").append(i).append("=").append(i * dan).append("<br>\n");
}
}
response.setContentType("text/html;charset=UTF-8"); // 3. 결과 출력
PrintWriter writer = response.getWriter();
writer.print("""
<html>
<head>
<title>구구단출력</title>
</head>
<body>%s</body>
</html>""".formatted(data));
}
바로 출력을 위해 HTML 코드를 만드는 부분이다. 이 부분을 문자열로 한땀한땀 정성들여 만들다 보면 들여쓰기, 속성 설정 등 해야 할 일들이 너무 많아진다. 만약 CSS, JavaScript까지 작성해야 한다면 엄청나게 복잡해질 것이다.
즉 Servlet은 자바 코드를 이용해서 작업을 처리하기에는 아주 훌륭하지만 HTML, CSS, JavaScript 코드를 작성하기에는 너무 불편하다. 만약 퍼블리셔와 협업을 한다고 했을 때 퍼블리셔가 디버깅을 위해 Servlet을 만져야 한다고 생각하면 가슴이 먹먹하다.
이런 문제를 해결하기 위해 JSP(Java Server Page)라는 것을 사용할 수 있다.
JSP
JSP는 태그 기반으로 서버 프로그램을 만들기 위한 기술이다. 일단 앞서 작성했던 GuguServlet을 JSP로 변경해보자. src/main/webapp/gugu.jsp을 다음과 같이 만들어보자.
<%@ page contentType="text/html;charset=UTF-8" %>
<%@ page import="java.util.*" %>
<%@ page import="java.io.*" %>
<html>
<head>
<title>구구단 출력</title>
</head>
<body>
<%
String danStr = request.getParameter("dan"); // 1. 파라미터 확인 및 검증
StringBuilder data = new StringBuilder();
if (danStr == null || danStr.isEmpty()) {
data.append("dan을 입력하세요.");
} else {
int dan = Integer.parseInt(danStr); // 2. 비지니스 로직 처리
for (int i = 1; i < 10; i++) {
data.append(dan).append(" * ").append(i).append(" = ").append(i * dan).append("<br>\n");
}
}
%>
<%=data %>
</body>
</html>
한눈에 보더라도 HTML 태그 기반의 코드에 Java 코드가 약간은 어색하게 삽입된 형태인 것을 알 수 있다. 정확히 Servlet의 반대이다. 이처럼 Servlet의 경우 Java기반으로 HTML 작성에 약간의 번거로움이 있었고 JSP의 경우는 HTML 기반으로 Java 코드 작성에 번거로움이 존재한다.
JSP의 정체
JSP는 Servlet을 designer 및 publisher 직군이 좀 더 쉽게 접근할 수 있게 만든 기술이다. Servlet의 코드를 보면서 designer가 html을 디버깅하는 모습은 상상하기 힘들다. 그런데 사실 JSP는 Servlet을 작성하는 또다른 방식이고 나중에 Servlet으로 변환되서 동작한다.
그럼 jsp는 어떻게 Servlet으로 변환되는 것일까? Server 콘솔에 출력된 CATALINA_BASE 경로에 가보면(사실 꼭 가볼 필요는 없다.) Servlet으로 변환된 JSP를 확인할 수 있다.
[CATALINA_BASE]\work\Catalina\localhost\simpleweb\org\apache\jsp\gugu_jsp.java
필요한 부분만 코드를 발췌해서 살펴보자.
public final class gugu_jsp extends HttpJspBase...{ // 1. HttpJspBase extends HttpServlet
public void _jspInit() { } // 2. life cycle 메서드
public void _jspDestroy() { }
public void _jspService(HttpServletRequest request, HttpServletResponse response)
throws java.io.IOException, jakarta.servlet.ServletException {
jakarta.servlet.http.HttpSession session = null; // 3. request, response등 내장 객체
final jakarta.servlet.ServletContext application;
jakarta.servlet.jsp.JspWriter out = null;
try {
response.setContentType("text/html;charset=UTF-8"); // 4. 응답 준비
application = pageContext.getServletContext();
session = pageContext.getSession();
out = pageContext.getOut();
out.write("\r\n"); // 5. PrintWriter에 해당하는 out를 이용한 출력
out.write("<html>\r\n");
out.write("<head>\r\n");
out.write(" <title>구구단 출력</title>\r\n");
out.write("</head>\r\n");
out.write("<body>\r\n");
String danStr = request.getParameter("dan"); // 6. <% .. %> 내부에 있던 자바 코드
StringBuilder data = new StringBuilder();
if (danStr == null || danStr.isEmpty()) {
data.append("dan을 입력하세요.");
} else {
int dan = Integer.parseInt(danStr);
for (int i = 1; i < 10; i++) {
data.append(dan).append("*").append(i).append("=").append(i*dan).append("<br>\n");
}
}
out.write('\r');
out.write('\n');
out.print(data ); // 7. <%=data%>
out.write("\r\n");
out.write("</body>\r\n");
out.write("</html>\r\n");
} catch (java.lang.Throwable t) {}
}
}
자세히 살펴보면 JSP에서 태그 기반으로 작성했던 내용이 Servlet으로 변환되서 동작하고 있음을 알 수 있다. WAS는 'xxx.jsp'에 해당하는 요청을 받으면 이 jsp에 해당하는 Servlet이 로딩되있는지 살펴본다. 만약 처음 요청이어서 아직 로드된 Servlet이 없다면 jsp를 Servlet으로 변경 후 객체 생성 -> _jspInit() -> _jspService()까지 호출한다. 물론 다시 해당 jsp에 대한 요청이 들어오면 _jspService() 만 호출하게 된다.
그럼 JSP의 구성 요소로는 어떤 것들이 있을까?
JSP의 구성 요소
page directive
page directive는 JSP 페이지가 실행될 때 필요한 정보를 지정하는 역할을 하는데 <%@ page 속성='값' 속성='값'%>의 형태로 작성한다. 지정할 수 있는 속성들이 정말 많지만 대부분 기본 값들이 사용되며 반드시 지정해야 하는 것은 2가지 정도로 생각할 수 있다.
- contentType: 클라이언트로 전송되는 content의 타입으로 거의 text/html;charset=utf-8을 사용한다.
- import: JSP 페이지 내에서 import 해서 사용할 클래스를 선언하는 곳으로 ',' 를 이용해서 여러개 선언할 수 있다.
<%@ page contentType="text/html;charset=UTF-8" language="java"
import="java.util.*, java.io.*" %>
또는 page directive를 여러번 사용할 수도 있다.
<%@ page contentType="text/html;charset=UTF-8"%>
<%@ page import="java.util.*" %>
<%@ page import="java.io.*" %>
built in object
built in object(내장객체)란 JSP 파일을 Servlet 파일로 변경하면서 _jspService 메서드의 파라미터 또는 로컬 변수로 선언되는 객체를 말한다. 뒤에서 설명하겠지만 JSP의 scriptlet이나 expression 태그들은 모두 _jspService의 로컬에서 동작한다. 따라서 같은 로컬 영역이므로 내장 객체를 사용하는데 전혀 문제가 없다.
내장 객체의 종류는 다음과 같다.
변수명 | 변수 타입 | 용도 |
response | jakarta.servlet.http.httpServletResponse | 메서드 파라미터로 서버의 응답 정보 저장 |
request | jakarta.servlet.http.HttpServletRequest | 메서드 파라미터로 클라이언트 요청 정보 저장 |
session | jakarta.servlet.http.HttpSession | HTTP 세션에 대한 정보 저장 |
application | jakarta.servlet.ServletContext | 웹 애플리케이션에 대한 정보 저장 |
out | jakarta.servlet.jsp.JspWriter | 클라이언트로의 정보 출력 스트림 |
pageContext | jakarta.servlet.jsp.PageContext | 현재 JSP 페이지에 대한 정보 저장 |
config | jakarta.servlet.ServletConfig | JSP 페이지에 대한 설정 정보 저장 |
exception | java.lang.Throwable | error페이지에서 사용되는 예외 객체 |
page | java.lang.Object | JSP 페이지를 구현한 자바 클래스 인스턴스 |
이중 특히 request, session은 정말 자주 사용되는 객체이므로 잘 기억해두자. 또한 내장 객체들은 이미 선언이 완료되었기 때문에 동일한 이름으로 다른 로컬 변수를 선언할 수 없다.
script let
scriptlet은 <%와 %> 사이에 java 코드를 사용하기 위한 태그이다. scriptlet에 작성한 코드는 _jspService 메서드의 내부 즉 local 영역에서 동작한다는 점은 매우 중요하다.
local 영역에서 동작하기 때문에 local에 선언된 파라미터(request, response)와 내장 객체를 자유로이 사용할 수 있다. 즉 JSP 상에서는 request가 선언되지 않았지만 이 코드가 _jspService(request, response)에서 동작하므로 request라는 것을 사용할 수 있는 것이다.
<%
String danStr = request.getParameter("dan"); // 1. 파라미터 확인 및 검증
StringBuilder data = new StringBuilder();
if (danStr == null || danStr.isEmpty()) {
data.append("dan을 입력하세요.");
} else {
int dan = Integer.parseInt(danStr); // 2. 비지니스 로직 처리
for (int i = 1; i < 10; i++) {
data.append(dan).append("*").append(i).append("=").append(i*dan).append("<br>\n");
}
}
%>
expression
expression은 <%=와 %> 사이에 출력할 내용(변수, 수식, 리턴이 있는 메서드 등)을 작성하면 된다. 이 코드는 나중에 Servlet에서 _jspService() 내부에서 out.print(내용)의 형태로 변형된다. 여기서 out은 JspWriter 타입의 built in object이다.
<%=data %>
out.print(data );
declaration
declaration은 <%!와 %> 사이에 멤버 변수 및 메서드를 선언하는데 Servlet으로 변환되면 선언 위치와 상관 없이 Servlet의 멤버 변수 또는 멤버 메서드로 등록된다.
<%!
private String sayHello(String name){
return "Hello "+name;
}
%>
페이지 모듈화
페이지 모듈화의 필요성
대부분의 사이트들은 일정한 틀을 가지고 페이지 내에 고정된 부분과 변하는 부분을 가진다. 이중 고정된 부분(<header>, <footer> 등)은 여러 페이지에 등장하며 동일한 내용이 사용된다. <header>에서 크게 달라지는 부분은 로그인 전/후의 화면 구성 정도일 것이다.
만약 이런 부분에서 내용에 대한 수정이 필요하다면 어떻게 될까?
이를 대비하여 페이지를 모듈화하고 재사용할 필요가 있다.
include directive
페이지를 모듈화 하기 위해 include directive를 사용할 수 있다. include directive는 file 속성을 통해 삽입할 페이지를 지정한다.
<%@ include file="source_file" %>
include directive를 사용하면 현재의 JSP에 대상 JSP 파일을 끼워 넣어 하나의 소스 파일을 생성해서 servlet으로 변경된다. 결국 source_file의 html 내용 뿐 아니라 member 및 local 변수까지 include한 곳에서 사용할 수 있게 된다. 대표적으로 절대경로를 구성하기 위해 컨텍스트 이름을 사용해야 하는데 이 경로를 문자기반으로 사용하면 변경시 문제가 된다. 이런경우 컨텍스트 이름을 변수화 해두면 재사용이 용이하고 변경에 유연하게 대처할 수 있게 된다.
<%-- header.jsp --%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
String root = request.getContextPath();
%>
<h1>Simple Web App</h1>
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
<title>JSP - Hello World</title>
</head>
<body>
<%@include file="/include/header.jsp"%>
<h1><%= "Hello World!" %>
</h1>
<br/>
<a href="hello-servlet">Hello Servlet</a> <br>
<!--a href="/simpleweb/static/gugu.html">구구단요청</a><br-->
<a href="<%=root%>/static/gugu.html">구구단요청</a><br>
</body>
</html>