react나 anguler, vue 같은 프론트엔드 프레임워크가 발달해 현재는 많이 사용되지는 않지만, 자바 서블릿 기반의 동적 웹페이지 개발을 위한 기술로 JSP가 있다. JSP는 HTML에 Java를 사용할 수 있게 해준다. 내부적으로는 서블릿으로 변환된 후 실행된다.
이전 포스팅 서블릿 사용하기에서 입력받은 도서 정보를 HTML을 보내줄 때, 다음과 같이 html 코드를 out.print로 보내주었다.
out.print("<!DOCTYPE html><html lang='en'><head><meta charset='UTF-8' /><meta http-equiv='X-UA-Compatible' content='IE=edge' /><meta name='viewport' content='width=device-width, initial-scale=1.0' /><title>도서 입력</title></head><body><h2>입력 내용</h2><div>");
out.print(book.toString());
out.print("</div></body></html>");
html을 out.print로 보내주다보니, 복잡해지면 작성하기도 불편하고 수정하기도 어려워진다. 그래서 JSP가 나왔다.
JSP파일에서는 HTML과 자바 코드를 같이 사용할 수 있다.
다음 3가지 태그가 자바 코드를 사용하기 위한 태그이다.
멤버 변수 선언이나 메서드를 선언하는 영역
String name;
public void init() {
name = "abc";
}
데이터를 출력할 때 사용한다 - out.print() 의 인자로 적합한 모든 자바 코드 가능
안녕하세요, <%= name %>님!
안녕하세요, <% out.print(name); %>님!
위 두개는 같은 표현으로 결과값이 같다.
모든 자바 코드 사용이 가능하다
<table>
<% for (String name : namelist) { %>
<tr>
<td><%=name %></td>
</tr>
<% } %>
</table>
주석은 <%-- --%> 로 쓸 수 있다.
page 지시자는 소스코드 맨 앞에 위치한다.
JSP 페이지를 컨테이너에서 처리하는데 필요한 각종 속성을 기술하는 부분으로 language(스크립트의 언어 지정), contentType(현재 페이지 파일 형식 지정), pageEncoding(character encoding 지정), import(자바 코드를 직접 사용하는 경우 필요한 패키지 import), errorPage(처리 도중 에러 발생 시, 에러 페이지 설정), session(기본값 true, 세션 사용 유무 설정) 등을 설정할 수 있다.
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8" import="java.util.*" errorPage="error.jsp"%>
include 지시자는 다른 jsp file을 해당 페이지에 포함하고 싶을 때 사용한다. header나 footer 등 여러 jsp 페이지에서 반복적으로 사용되는 부분을 jsp file로 따로 만든 후, 반복 영역에 include 시켜서 반복되는 코드를 줄일 수 있다.
<%@ include file="header.jsp" %>
taglib 지시자는 커스텀 태그를 사용하기 위한 지시자이다. prefix는 해당 태그를 구분하기 위한 접두어이고, uri는 태그 라이브러리 uri로 태그를 정의하고 있는 파일의 위치를 의미한다.
뒤에서 볼 JSTL을 사용할 때 주로 쓰인다.
<%@ taglib prefix=“c” uri=“http://java.sun.com/jsp/jstl/core” %>
JSP는 내부적으로 자바 서블릿 파일로 변환된 다음 실행된다.
이렇게 변환될 때, 컨테이너가 자동으로 만들어주는 객체들이 있는데 이 객체들을 기본 객체(내장 객체)라고 한다. 이미 만들어져 있기 때문에 jsp에서 사용할 수 있다. 위의 예제 중 <% out.print(name) %> 도 기본 객체 out을 사용한 코드이다.
기본 객체의 종류는 다음과 같다.
클라이언트의 요청 정보 저장
HTML form 요소 선택 값 등 사용자 입력 정보를 읽어올 때 사용
요청 하나당 하나의 request 객체
클라이언트 응답 정보 저장
JSP 페이지 정보 저장
각종 기본 객체를 얻거나 forward 및 include 기능을 활용할 때 사용
하나의 요청에 대해 하나의 JSP 페이지 호출 - 하나의 page 객체
클라이언트에 대한 세션 정보를 처리하기 위해 사용
page 지시자에 session="false"로 지정하면 session 객체가 만들어지지 않는다.
같은 웹 브라우저 내에서 요청시 같은 session 공유
웹 어플리케이션 정보 저장
웹 어플리케이션당 1개의 application 객체 - 같은 웹 어플리케이션에 요청되는 모든 페이지는 같은 application 객체 공유
page < request < session < application
jsp를 이용해 구성할 수 있는 웹 어플리케이션 구조 Web Application Architecture는 크게 model1과 model2로 나뉜다. 두 방식의 가장 큰 차이점은 model1은 jsp에서 logic 처리와 response page(view)에 대한 처리를 모두 하고. model2는 response page(view)에 대한 처리만 하고, 로직 부분은 servlet에서 따로 한다.
화면 구성(View)뿐만 아니라, 로직 부분도 JSP에서 모두 같이 한다. 예를 들어, JSP에서 DB에 데이터를 가져오고 넣는 작업도 같이 하는 건데, 이렇게 하면 구조가 단순하기 때문에 코딩하기는 쉽지만, 코드 자체가 복잡해져 유지보수가 어렵고 한 파일에 Backend와 Frontend가 섞여 있어 협업하기도 어렵다.
model2는 MVC 패턴은 도입한 구조이다.
model1의 유지보수가 힘들다는 단점을 보완하기 위해 model과 view, controller를 나누어 model에서는 로직 처리(일반 java 파일), view(jsp)에서는 화면 처리, controller(servlet)는 요청을 받아 분석해 로직 처리를 위해 model을 호출하고 데이터를 적절히 처리해 jsp 페이지를 출력한다.
model2는 구조가 복잡해 초기 진입이 어렵고, 개발 시간이 증가한다는 단점이 있다. 하지만 코드가 덜 복잡하고 분업이 용이해졌고, 확장성도 뛰어나며 유지 보수가 쉬워진다는 장점이 있어 JSP를 이용한 개발에서는 주로 model2, MVC 패턴이 많이 사용된다.
Model2를 이용한 예제를 살펴보자.
이전 포스팅 서블릿 사용하기에서 작성했던 코드를 Model2 방식에 맞게, 서블릿에서 바로 html을 출력하는 것이 아니라 view 부분을 jsp로 만들어 고쳐볼 것이다.
model2에서는 로직 처리를 한 다음에 forward나 redirect를 이용해 결과 페이지(jsp)로 이동한다.
redirect와 forward 차이
response.sendRedirect(location)
redirect는 웹 서버가 웹 브라우저에게 300번대 응답을 보내 웹 브라우저에게 다른 페이지로 요청하라고 응답한다. 웹 브라우저는 응답을 받고 새로운 페이지의 URL로 요청을 새로 다시 보낸다.
request.getRequestDispatcher(path).forward(request, response)
forward는 웹 컨테이너내에서만 페이지 이동이 진행된다. 웹 브라우저에는 최초에 호출한 URL만 남아있고, 실제 이동되는 주소는 확인할 수 없다. 기존의 request, response 객체가 그대로 전달된다.
만약 서버에서 데이터를 처리해 화면에 보내주어야한다면 forward로 해야 request 객체에 데이터를 담아 넘겨 줄 수 있기 때문에 forward로 한다. redirect로 data를 넘겨주려면 session이나 cookie를 이용해야 한다.
먼저, 프로젝트 구조는 다음과 같다.
이전 포스팅에서 봤던 구조와 거의 비슷한데, service를 추가했고, controller(servlet)의 내용을 수정해주었다.
service layer는 비즈니스 로직을 처리하는 부분으로, 데이터가 필요하면 dao를 통해 데이터를 받아와 로직을 처리하고 이걸 controller에서 사용한다.
이 예제에서는 DB에서 데이터만 가져오면 되서 아래와 같이 간단하게 구현했다.
// BookService
package com.practice.backend.service;
import java.sql.SQLException;
import java.util.List;
import com.practice.backend.dto.Book;
public interface BookService {
int insert(Book book) throws SQLException;
List<Book> select() throws SQLException;
}
요청마다 새로운 객체를 계속 생성하는 것은 비효율적이므로, dao와 마찬가지로 싱글톤으로 구현한다.
// BookServieImpl
package com.practice.backend.service;
import java.sql.SQLException;
import java.util.List;
import com.practice.backend.dao.BookDao;
import com.practice.backend.dao.BookDaoImpl;
import com.practice.backend.dto.Book;
public class BookServiceImpl implements BookService {
private BookDao bookDao = BookDaoImpl.getBookDao();
private static BookService bookService = new BookServiceImpl();
private BookServiceImpl() {}
public BookService getBookService() {
return bookService;
}
@Override
public int insert(Book book) throws SQLException {
return bookDao.insert(book);
}
@Override
public List<Book> select() throws SQLException {
return bookDao.select();
}
}
controller에 해당하는 MainServlet 부분은 out.print 대신 request 객체에 book 객체를 setAttribute 해준 다음, forward로 register_result.jsp로 페이지를 이동했다.
//MainServlet
package com.practice.backend.controller;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.SQLException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.practice.backend.dao.BookDao;
import com.practice.backend.dao.BookDaoImpl;
import com.practice.backend.dto.Book;
@WebServlet("/main")
public class MainServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private static final BookDao bookDao = BookDaoImpl.getBookDao();
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("utf-8");
doGet(request, response);
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String act = request.getParameter("action");
if ("regist".equals(act)) {
doRegist(request, response);
}
}
private void doRegist(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
String path = "";
Book book = new Book();
book.setIsbn(request.getParameter("isbn"));
book.setTitle(request.getParameter("title"));
book.setAuthor(request.getParameter("author"));
book.setPrice(Integer.parseInt(request.getParameter("price")));
book.setDesc(request.getParameter("desc"));
book.setImg(request.getParameter("img"));
try {
bookDao.insert(book);
request.setAttribute("book", book);
path = "register_result.jsp";
} catch (SQLException e) {
e.printStackTrace();
path = "error.jsp";
}
request.getRequestDispatcher(path).forward(request, response);
}
}
<!--register_result.jsp-->
<%@page import="com.practice.backend.dto.Book"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8' />
<meta http-equiv='X-UA-Compatible' content='IE=edge' />
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
<title>도서 입력</title>
</head>
<body>
<h2>입력 내용</h2>
<% Book book = (Book)request.getAttribute("book");
if (book != null) {
out.print(book);
} else {
%>
<div>입력 내용이 없습니다.</div>
<% } %>
</body>
</html>
JSP 스크립트 표현식(<%= =>) 대신 쉽게 출력할 수 있도록 고안된 언어
java.util.Map 객체의 키에 대한 value이거나 java bean 객체의 bean property 값을 출력할 수 있다. 예를 들어, (Book)request.getAttribute("book").getIsbn() 경우에는 아래와 같이 간단하게 작성할 수 있다.
${book.isbn}
위에서 처럼 request.book 이 아니라 book으로 이름만 사용할 경우, pageScope > requestScope > sessionScope > applicationScope 순으로 객체를 찾는다.
데이터 처리, 조건문, 반복문 등의 일을 처리하기 위한 JSP 태그 라이브러리
여러가지 라이브러리가 있는데, 그 중 많이 쓰는 core를 살펴보자
<%@ taglib prefix=“c” uri=“http://java.sun.com/jsp/jstl/core“ %>
위와 같이 선언을 해놓고 쓸 수 있다.
특정 변수나 객체의 프로퍼티에 값을 할당하고 싶을 때는 <c:set> 태그를 쓴다.
<c:set value="value" var="varName" [scope="{page|request|session|application}"]/>
if문을 쓰고 싶을 때는 <c:if>를 쓸 수 있다.
<c:if test="${!empty book}">
</c:if>
for문을 쓰고 싶을 때는 <c:forEach>를 쓸 수 있다.
<b>1) book 리스트(books)를 반복하면서 책 제목 출력하기</b><br/>
<c:forEach var="book" items="${books}">
${book.title}<br/>
</c:forEach>
<c:if>와 EL을 사용해서 위의 register_result.jsp를 간단하게 바꾸어 보았다.
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8' />
<meta http-equiv='X-UA-Compatible' content='IE=edge' />
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
<title>도서 입력</title>
</head>
<body>
<h2>입력 내용</h2>
<c:if test="${!empty book}">${book}</c:if>
<c:if test="${empty book}">입력 내용이 없습니다.</c:if>
</body>
</html>