JNDI와 Connection Pooling 내용 정리 + 웹 기반의 도서 정보 관리 시스템인 Library CRUD 프로그램을 만들어보겠다.
이 프로그램은 자바 서블릿을 활용하여 도서 정보를 생성(Create), 조회(Read), 수정(Update), 삭제(Delete) 할 수 있게 한다.
화면은 크게 세 페이지로 분류될 것이다. 도서 등록을 위한 book_regist.jsp 페이지와 도서 목록을 보여주며 삭제 기능을 포함하는 book_list.jsp 페이지, 도서 내용 수정을 위한 book_detail.jsp 페이지로 구성된다.
도서 등록을 위한 ISBN(International Standard Book Number, 국제 표준 도서번호)과 도서명, 저자, 출판사, 출판일을 입력하면 책 정보가 있는 DB에 데이터를 등록시킨다. 도서번호는 자동으로 1씩 증가시키며 추가되고, 도서위치와 상태는 등록시에 기본값이 삽입된다.
DB에 저장된 책의 목록을 불러와 테이블에 띄워주는 페이지이다. 타이틀을 누르면 도서 수정을 위한 상세 페이지로 이동할 수 있게 하고, 삭제를 누르면 도서를 지우도록 한다.
도서 상세화면은 리스트에서 선택한 도서의 내용을 서블릿을 통해 읽어와서 화면에 띄워준다. 수정 버튼을 누르면 내용을 수정할 수 있고, 원래대로 버튼을 누르면 수정 전의 원래 내용으로 돌아간다.
- 프로그램을 만들어 보기 전에 몇 가지 용어 및 사용 기술 정리를 하고 가겠다 -
▶ taglib 디렉티브
- JSP에서 사용되는 태그 라이브러리를 관리하는 기능을 제공함.
- JSP는 HTML과 Java코드를 혼합하여 동적인 웹 페이지를 생성하는 기술이지만, 많은 양의 Java 코드를 작성하는 것은 가독성과 유지보수 측면에서 좋지 않을 수 있음.
↳ 이러한 문제를 해결하기 위해 JSP에서는 태그 라이브러리를 사용하여 미리 정의된 태그 활용 → 코드의 가독성과 재사용성을 높이는 방법 제공.
- taglib를 사용하려면 먼저 JSP 페이지에서 해당 태그 라이브러리를 import 한다. (<%@ taglib %> 지시문 추가)
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>
- uri는 Uniform Resource Identifier의 약자로써, 태그 라이브러리의 식별자인 URI를 지정하고, prefix는 태그 라이브러리의 태그를 사용하기 위해 정의된 접두사(prefix)를 지정함
- 태그 라이브러리는 표현 언어(EL, Expression Language), JSTL(JSP Standard Tag Library), 커스텀 태그(Custom tag)가 존재함.
- JSP 커스텀 태그는 사용자가 직접 정의한 태그이며, JSTL은 JSP에서 자주 사용되는 기능을 제공하는 태그 라이브러리이다. (JSTL = 자주 사용될 수 있는 커스텀 태그들을 모아서 표준으로 모아놓은 태그 라이브러리)
<!-- expression language 사용 -->
<c:forEach var="copy" items="${list}"> <!-- 앞의 c는 prefix. 위에서 정의한 uri와 같음 -->
<tr>
<td>${copy.bookSeq}</td>
<td><a href='./BlmController?cmd=detail&bookSeq=${copy.bookSeq}'>${copy.title}</a></td>
<td>${copy.author}</td>
<td><fmt:formatDate value="${copy.publishDate}" pattern="yyyy-MM-dd"/> </td>
<td><a href="./BlmController?cmd=remove&bookSeq=${copy.bookSeq}">삭제</a></td>
</tr>
</c:forEach>
- <c:forEach> 태그로 반복문을 간편하게 처리한다.
> JSTL 사용 방법
- JSTL을 사용하기 위해서는 아래 작업이 필요하다.
Maven jstl 검색 후 사이트에 들어가서 jstl.jar 파일을 받아준다.
다운받은 jar 파일을 프로젝트에 넣어주면 사용할 준비가 된다.
▶ Servlet Context
- 서블릿 Container에 의해 생성되는 객체로, 웹 애플리케이션 전체에서 공유되는 정보를 저장하고 관리하는데 사용됨.
- 서블릿 컨텍스트는 웹 애플리케이션 내의 모든 서블릿들이 접근할 수 있는 공통된 환경을 제공함.
- javax.servlet.ServletContext 인터페이스를 구현한 객체로서, 서블릿 클래스 내에서 getServletContext() 메서드를 호출하여 현재 서블릿 컨텍스트를 얻을 수 있음.
▷ Servlet Context의 기능과 특징
- 웹 애플리케이션 범위 : 서블릿 컨텍스트는 웹 애플리케이션 전체에서 유효한 범위를 가지며, 웹 애플리케이션 내의 모든 서블릿들이 접근 가능함. → 서블릿 간에 데이터를 공유하고 상태를 유지하는데 사용됨
- 애플리케이션 설정 정보 : 서블릿 컨텍스트는 웹 애플리케이션의 설정 정보를 저장하고 관리함. 웹 애플리케이션의 초기화 매개변수, 환경 변수, DB 연결 정보 등과 같은 설정 정보를 Servlet Context를 통해 가져올 수 있음.
- 리소스 접근 : Servlet Context는 웹 애플리케이션 내의 리소스에 접근하는데 사용됨. 웹 애플리케이션의 classpath 내의 리소스 파일, JSP 파일, 서블릿 파일 등에 접근할 수 있는 메서드를 제공함.
- 세션 관리 : Servlet Context는 세션 객체를 생성하고 관리함. 세션을 생성하거나 세션 ID를 가져오는 등의 작업 수행가능
- 로깅 : Servlet Context는 로깅 기능을 제공하여 웹 애플리케이션의 로그를 관리함. 로깅 레벨 설정, 로그 파일 경로 지정 등의 작업을 할 수 있음.
▶ JNDI (Java Naming and Directory Interface)
- Java 애플리케이션에서 네임 서비스 및 디렉터리 서비스에 접근하기 위한 표준 인터페이스
- Java EE(Enterprise Edition) 환경에서 주로 사용되며, Network Resource, DB 연결, 메시지 큐 등과 같은 리소스를 애플리케이션에서 참조하고 검색하는데 사용됨.
- JNDI를 사용하여 네임 서비스에 접근하려면 해당 서비스의 이름을 사용하여 검색해야 함.
↳ 이를 통해 애플리케이션은 Network Resource나 DataBase와 같은 리소스에 투명하게 접근할 수 있음. (JNDI는 일종의 중간 계층으로 작동하여 애플리케이션 코드와 실제 리소스 사이의 인터페이스 역할을 함)
- JNDI를 사용하는 가장 일반적인 사례는 데이터베이스 연결이다.
↳ 애플리케이션은 JNDI를 통해 데이터베이스 서버에 연결 정보를 등록하고, 필요할 때마다 JNDI를 통해 데이터베이스 연결을 검색하여 사용할 수 있음. → 이렇게 하면 애플리케이션 코드에서 직접 데이터베이스 연결 정보를 관리하거나 하드코딩하지 않아도 됨.
- JNDI는 일반적으로 서버 환경에서 사용됨. Java EE 서버는 JNDI를 지원하며, 서버에 등록된 리소스를 애플리케이션에서 사용할 수 있도록 제공함.
- JNDI는 Java EE 환경에서 주로 사용되지만, 일반적인 Java 애플리케이션에서도 사용 가능 (Local에서 실행되는 Java 애플리케이션이 특정 디렉터리에 접근하거나, LDAP(디렉터리 서비스를 제공하기위한 프로토콜) 서버에서 사용자 정보를 가져오는 등의 작업을 수행할 때 JNDI 사용가능)
- 애플리케이션은 JNDI를 통해 리소스를 중앙 집중화하고, 필요한 경우 리소스를 변경하거나 교체 가능 → 애플리케이션 확장성과 유지보수성 향상
> JNDI 사용 단계
① 서버 환경에서 JNDI 리소스를 구성하고 등록
② 애플리케이션에서 JNDI를 사용하여 필요한 리소스를 검색
③ 검색된 JNDI 리소스를 사용하여 해당 리소스와 상호작용
자바에 서버를 연결했다면 Servers 폴더가 있을 것인데, server.xml을 보겠다.
Server에 Configured 되어 있는 프로젝트는 아래와 같이 server.xml의 Context 태그 docBase에 자동으로 들어간다.
context.xml을 보겠다.
DB에 연결하기 위한 정보를 ConnectionManager에 하드코딩하는 것이 아닌 Server의 context에 Resource를 집어 넣을 것이다. (환경 설정)
<Resource
name="jdbc/book"
auth="Container"
type="javax.sql.DataSource"
driverClassName="com.mysql.cj.jdbc.Driver"
url="jdbc:mysql://localhost:3306/library_schema"
username="root"
password="1234"
maxTotal="100"
maxIdle="30"
maxWaitMillis="10000"
/>
<Context> 태그 내부에 위와 같은 코드를 삽입한다.
Resource 태그 내의 각 요소에 대해 알아보면
1. name : 리소스의 이름 지정. 이 이름은 JNDI를 통해 리소스에 접근할 때 사용됨.
2. auth : 리소스의 인증 방식을 지정함. "Container"를 설정하면 컨테이너가 리소스에 대한 인증을 처리하고, "Application"을 설정하면 애플리케이션이 직접 인증을 처리함.
3. type : 리소스의 유형을 지정함. 데이터베이스와의 연결을 관리하는 데이터 소스를 지정했음.
4. driverClassName : 사용할 JDBC 드라이버의 클래스 이름 지정
5. url : DB 접속을 위한 url 지정. 로컬호스트의 3306 포트에 위치한 library_schema DB에 접속
6. username, password : DB 접속을 위한 name, password
7. maxTotal : 커넥션 풀에서 생성할 수 있는 최대 커넥션의 개수를 지정. 이 값은 동시에 사용할 수 있는 최대 커넥션 수를 나타냄. 최대 100개의 커넥션을 생성할 수 있도록 설정함.
8. maxIdle : 커넥션 풀에서 유지할 수 있는 최대 유휴 상태의 커넥션 개수를 지정함. 유휴 상태의 커넥션은 현재 사용중이 아니지만 풀에 반환되어 대기하는 커넥션을 말함. maxIdle="30"은 최대 30개의 유휴 커넥션을 유지하는 것. 이 값을 너무 낮게 설정하면 매번 새로운 커넥션을 생성해야 하므로 성능에 영향을 줄 수 있음. 그러나 너무 높게 설정하면 메모리 리소스를 낭비할 수 있음.
9. maxWaitMillis : 커넥션 풀에서 Connection을 얻기위해 대기할 수 있는 최대 시간을 지정함. 이 값을 밀리초 단위로 설정되며, 커넥션 풀이 모든 커넥션을 사용 중일때, 새로운 커넥션을 얻기 위해 대기하는 시간임. maxWaitMillis="10000"으로 설정하면 최대 10초까지 대기할 수 있음. 이 값을 너무 낮게 설정하면 대기중인 요청이 커넥션을 얻지 못하고 예외가 발생할 수 있음. 그러나 너무 높게 설정하면 응답 시간이 길어질 수 있음.
- 위의 설정은 Tomcat 서버에서 사용되는 커넥션 풀 관련 리소스 설정을 나타내며, 데이터베이스와의 연결 관리를 위해 필요한 정보들을 지정함
- maxTotal, maxIdle, maxWaitMillis와 같은 설정값들은 애플리케이션의 트래픽이나 데이터베이스 서버의 용량 등을 고려하여 조정해야 함. (최적의 값 설정으로 성능과 안정성 유지)
▶ Connection Pooling
- 커넥션 풀링은 DataBase 연결을 관리하기 위한 기술로, DB 연결을 미리 생성하여 풀에 저장해두고 필요할 때마다 가져와서 사용하는 방식.
- DB 연결의 생성과 소멸을 반복하는 비용을 줄이고, 효율적으로 DB 연결을 관리
- 일반적으로 웹 애플리케이션에서는 동시에 여러 사용자가 DB에 접근하고, 매번 DB 연결을 생성하고 종료하는 것은 부하가 큰 작업이다. → 이 문제를 해결하기 위해 커넥션 풀링을 사용하여 미리 여러개의 DB 연결을 생성하고 관리함. 사용자가 DB 연결을 필요로 할 때마다 커넥션 풀에서 사용 가능한 연결을 가져와 사용하고, 사용이 끝나면 풀에 반환한다.
- 커넥션 풀링은 다양한 프로그래밍 언어와 데이터베이스 관리 시스템에서 지원되며, 대부분의 웹 애플리케이션 서버와 DB 연동 라이브러리에서 기본적으로 제공됨
> 커넥션 풀링의 기능
- 연결 재사용 : 커넥션 풀에서 가져온 연결을 사용한 후에는 풀에 반환하여 재사용함. 연결을 반복적으로 생성하는 비용을 줄이고, 재사용 가능한 연결을 최대한 활용 가능
- 연결 생성 및 소멸 제어 : 커넥션 풀은 초기에 설정한 개수만큼 연결을 생성하여 풀에 보관함. → 연결 생성에 필요한 비용을 분산시키고, 연결이 더 이상 필요하지 않을 때 풀에서 연결을 제거하여 자원을 해제함.
- 연결 유휴 상태 관리 : 커넥션 풀은 유휴 상태의 연결을 관리함. 사용자가 연결을 반환하면 풀에 유휴 상태로 보관되고, 다시 필요할 때 사용 가능 상태로 유지됨. 이를 통해 연결의 재사용과 관련된 성능 향상을 이룰 수 있음.
package bitedu.bipa.member.utils;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;
public class ConnectionManager {
private static ConnectionManager manager;
private ConnectionManager() {
}
public static ConnectionManager getInstance() {
if(manager==null) {
manager = new ConnectionManager();
}
return manager;
}
public Connection getConnection() {
Connection con = null;
try { // Apache Tomcat 에서 제공하는 Connection pooling
Context ctx = (Context)new InitialContext();
Context env = (Context)ctx.lookup("java:/comp/env");
DataSource ds = (DataSource)env.lookup("jdbc/book");
con = ds.getConnection();
} catch (NamingException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}
return con;
}
public Connection getConnectionOld() {
Connection con = null;
String jdbcURL = "jdbc:mysql://localhost:3306/library_schema";
String driver = "com.mysql.cj.jdbc.Driver";
String id = "root";
String pwd = "1234";
try {
Class.forName(driver);
con = DriverManager.getConnection(jdbcURL,id,pwd);
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return con;
}
public void closeConnection(ResultSet rs , Statement stmt, Connection con) {
if(rs!=null) {
try {
rs.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if(stmt!=null) {
try {
stmt.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if(con!=null) {
try {
con.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} else {
con = null;
}
}
}
커넥션을 담당하는 클래스인 ConnectionManager를 보면, 기존의 getConnection()은 URL과 Driver 정보들을 클래스 내부에 하드코딩 하였다.
하지만 Tomcat의 커넥션 풀링을 사용한 getConnection()은 그렇지 않다.
- InitialContext()로 Context 객체를 생성 후, 컨텍스트 객체를 이용하여 환경 컨텍스트 객체를 얻는다.
- lookup() 메서드를 사용하여 "java:/comp/env" 경로에 위치한 컨텍스트를 얻음. (java:/comp/env = 웹 어플의 구성된 엔트리와 리소스들이 배치되어있는 부분. 이곳에 접근하여 매핑된 리소스를 가져온다.)
- 환경 컨텍스트에서는 Resource를 검색할 수 있음. "jdbc/book" 이름으로 등록된 데이터소스(DataSource)를 검색한다.
- DataSource는 미리 설정된 커넥션 풀을 관리하는 객체이다.
- DataSource의 getConnection() 메서드를 호출하여 DB Connection을 얻는다.
+ 위의 ConnectionManager는 Singleton 방식으로 만들어짐
- 외부에서 객체 생성을 막음 (생성자 private 선언)
- 자신의 타입인 정적 필드를 하나 선언하고 private 접근 제한자를 붙여 외부에서 필드 값을 변경하지 못하게 함.
- 외부에서 호출할 수 있는 정적 메서드 getInstance()를 선언하고 자신의 인스턴스를 갖는 정적 필드를 리턴해줌. (private 생성자이고 static 변수이므로 static 메서드를 통해서만 객체를 얻어오는 상황이 됨)
이제 본격적으로 Library CRUD 프로그램을 보겠다.
아래에는 기본적으로 사용할 테이블이다.
book_seq는 1부터 차례대로 자동으로 증가된다.
book_position은 도서 위치를 뜻하며 아래의 의미를 갖는다.
book_status는 도서의 상태를 의미하며 다음과 같다.
book_isbn은 도서의 고유번호와 같으며 다른 테이블과의 join을 위한 외래키로 사용될 것이다.
책 정보가 있는 book_info 테이블이다.
book_isbn 컬럼을 가지며
book_title(= 책 제목), book_author(= 저자), book_published_date(= 출판 일자) 등의 정보를 담고 있다.
> 등록 페이지
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>도서 등록</title>
<style>
table, td, th {
border : 1px solid black;
border-collapse: collapse;
margin: 20px auto;
}
td {
width: 150px;
height: 50px;
padding: 5px;
font-size: 20px;
/* text-align: center; */
}
input , select {
font-size: 20px;
}
button {
font-size: 15px;
margin: 5px;
}
#sending {
text-align: center;
}
#form {
font-size: 30px;
}
</style>
<script type="text/javascript" src="https://code.jquery.com/jquery-latest.min.js"></script>
<script type="text/javascript">
$(document).ready(function(){
$('#go_book_regist').on('click', function(){
let form = $('#frm');
form.attr('action', '/MemberSample0629/BlmController?cmd=regist');
alert("도서가 등록되었습니다.");
form.submit();
});
$('#go_book_list').on('click', function(){
let form = $('#frm');
form.attr('action', '/MemberSample0629/BlmController?cmd=list');
form.submit();
});
});
</script>
</head>
<body>
<%-- <% String cmd = request.getParameter("cmd"); %>
<%=cmd.equals("success")?"<script>alert('hello');</script>":""%> --%>
<form action="" method="post" id="frm"> <!-- action은 비운다 => javascript 문에서 클릭에 따라 form의 attr를 세팅 -->
<table>
<tr><th colspan="4" id="form">도서등록</th></tr>
<tr><th>구분</th><th class="data_ui" colspan="2">데이터입력</th><th>비고</th></tr>
<tr>
<td>도서번호</td>
<td colspan="2">
<input type="text" id="book_seq" name="book_seq" disabled="disabled">
</td>
<td id="message">자동생성</td></tr>
<tr>
<td>ISBN</td>
<td colspan="2">
<input type="text" id="isbn" name="isbn">
</td>
<td>
<input type="hidden" id="flag" value="false">
</td>
</tr>
<tr>
<td>도서명</td>
<td colspan="2">
<input type="text" id="book_title" name="book_title">
</td><td></td>
</tr>
<tr>
<td>저자/역자</td>
<td colspan="2">
<input type="text" id="author" name="author">
</td><td></td>
</tr>
<tr>
<td>출판사</td>
<td colspan="2">
<input type="text" id="publisher" size="35" name="publisher">
</td><td></td>
</tr>
<tr>
<td>출판일</td>
<td colspan="2">
<input type="text" id="publish_date" size="35" name="publish_date">
</td>
<td></td>
<tr>
<tr>
<td>도서위치</td>
<td colspan="2">
<select name="book_position" disabled="disabled">
<option value='BS'>--도서 위치--
<option value='BS-001' selected>일반서가
<option value='BS-002'>예약서가
<option value='BS-'>회원
</select>
</td>
<td>기본값삽입</td>
<tr>
<tr>
<td>도서상태</td>
<td colspan="2">
<select name="book_status" disabled="disabled">
<option value='BM'>--도서 상태--
<option value='BM-001' selected>도서대출서비스
<option value='BM-002'>도서수선
<option value='BM-003'>도서저장고
</select>
</td>
<td>기본값삽입</td>
<tr>
<tr>
<td colspan="4" id="sending">
<input type="submit" value="도서등록" id="go_book_regist">
<input type="reset">
<input type="submit" value="도서리스트" id="go_book_list">
</td>
<!-- <td colspan="4" id="sending" id=frm.=>
<input type="submit" value="도서등록">
<input type="reset">
<a href="/MemberSample0629/BlmController?cmd=list"><button type="button">도서리스트</button></a>
</td> -->
</tr>
</table>
</form>
</body>
</html>
다음과 같이 입력을 주고 도서 등록 버튼을 누르면 서블릿을 통해 등록된 값을 자바에 넘겨주고, 내부 코드에서 쿼리문을 통해 해당 정보들을 DB 테이블에 삽입해 줄 것이다.
중요한 부분만 보자면
먼저 각 버튼들에 id를 준다.
Form을 post 방식으로 넘겨줄 것이고, 자바스크립트로 각 버튼에 대한 클릭 이벤트 처리를 해준다.
form의 action란은 비워두고 버튼에 따라 action 값이 세팅되게 할 것이다.
Controller에서 cmd가 regist냐 list냐 에 따라 일처리를 다르게 해줄 것이다.
> 컨트롤러 생성
프로젝트 우클릭 후 Servlet을 새로 만들어준다.
이렇게 만들어주면 annotation이 달린 컨트롤러를 만든다.
Blm이란 Book Library (Cycle) Manager의 줄임말.
▶ @WebServlet 어노테이션
- Annotation은 사전적 의미로 주석이라는 뜻. 기존의 자바 주석처럼 @ 기호와 함께 사용되지만 개발자가 아닌 Compiler 또는 JVM(Java Virtual Machine)을 위한 주석이다. → 실행에 필요하지는 않지만, 컴파일러에게 해당 클래스에 대한 정보를 알려주거나 자바 프로그램 실행에 관한 내용을 설정하는 용도로 사용됨.
ex) @Override, @SuppressWarnings 어노테이션 등
- 클래스 선언부 앞에 @WebServlet(접근시 사용할 url)을 쓰면 어노테이션을 통해 서블릿에 접근 가능.
- 서블릿 한개에 여러개의 url 주소를 매핑하려면 다음과 같이 배열 형태로 사용해주면 됨.
@WebServlet(urlPatterns = {"/main", "/test", "/suldenlion"})
public class Controller extends HttpServlet { ... }
> 확장자 패턴
- URL 부분을 「*.확장자」 방식으로 적으면 해당 확장자를 가진 URL 패턴은 모두 지정된 서블릿 또는 JSP 파일로 이동됨. 주로 서블릿을 컨트롤러로 사용할 때, 모든 요청을 컨트롤러로 모으기 위해 사용.
@WebServlet("*.xxx")
public class FrontController extends HttpServlet
> Annotation 특징
- 어노테이션도 클래스와 마찬가지로 구현시 패키지를 가질 수 있으니 import가 필요함 (import javax.servlet.annotation.WebServlet)
- 어노테이션은 메서드는 없지만, 속성(Attribute)은 가짐
- 클래스 선언부, 멤버 변수 선언부, 메서드 선언부 위에 선언함
- 속성이 있는 경우 @어노테이션명(속성=값)의 형식으로 사용하면 되며, 속성이 값 하나인 경우에는 이름 없이 값만 적기도 함
- 서블릿 2.5 ver까지는 web.xml 파일의 설정을 통해서만 서블릿에 접근 가능했지만, 톰캣 7이후 서블릿 3.0 ver을 지원하며 @WebServlet 어노테이션을 사용할 수 있게 됨. (web.xml과 어노테이션 둘 중 하나만 사용. 동시에 설정하면 web.xml 설정이 우선시 되어 오류난다함)
- web.xml 파일로 설정하는 방식은 여러개의 서블릿을 태그로 등록하기에 전체적인 관리가 쉽고 URL 값이 변경되어야 할 때는 소스를 수정하지 않고서도 web.xml에서 쉽게 변경 가능함. 반면 @WebServlet 설정 방식은 설정 파일 없이 자바 소스에서 annotation으로 쉽게 URL 패턴을 지정
> BlmController.java
package bitedu.bipa.member.controller;
import java.io.IOException;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import javax.servlet.RequestDispatcher;
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 bitedu.bipa.member.service.BlmService;
import bitedu.bipa.member.vo.BookCopy;
/**
* Servlet implementation class BlmController
*/
@WebServlet("/BlmController") // anotation 덕분에 spring 쉬워짐 (환경설정 없어져서)
public class BlmController extends HttpServlet {
private static final long serialVersionUID = 1L;
/**
* @see HttpServlet#HttpServlet()
*/
public BlmController() {
super();
}
/**
* @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
*/
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String cmd = request.getParameter("cmd");
cmd = cmd == null?"list":cmd;
String url = "./manager/book_list.jsp";
BlmService blm = new BlmService();
boolean isRedirect = false;
if(cmd.equals("list")) {
ArrayList<BookCopy> list = blm.searchBookAll();
request.setAttribute("list", list);
} else if(cmd.equals("regist")) {
ArrayList<BookCopy> list = new ArrayList<BookCopy>();
request.setAttribute("list", list);
String isbn = request.getParameter("isbn");
String title = request.getParameter("book_title");
String author = request.getParameter("author");
String publisher = request.getParameter("publisher");
String dateTime = request.getParameter("publish_date");
//System.out.println(dateTime);
/*DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd");
LocalDateTime localDateTime = LocalDateTime.from(df.parse(dateTime));
System.out.println(localDateTime);*/
//Timestamp publishDate = Timestamp.valueOf(localDateTime);
Timestamp publishDate = Timestamp.valueOf(dateTime + " 00:00:00"); // 망할 시간 단위 때문에 계속 오류
//System.out.println("+++++" + publishDate);
String bookPosition = request.getParameter("book_position");
String bookStatus = request.getParameter("book_status");
BookCopy copy = new BookCopy();
copy.setIsbn(isbn);
copy.setTitle(title);
copy.setAuthor(author);
copy.setPublisher(publisher);
copy.setPublishDate(publishDate);
copy.setBookPosition(bookPosition);
copy.setBookStatus(bookStatus);
blm.registBook(copy);
url = "./manager/book_regist.jsp";
isRedirect = true;
} else if(cmd.equals("edit")) {
String isbn = request.getParameter("isbn");
String title = request.getParameter("book_title");
String author = request.getParameter("author");
String dateTime = request.getParameter("publish_date");
Timestamp publishDate = Timestamp.valueOf(dateTime);
String bookPosition = request.getParameter("book_position");
String bookStatus = request.getParameter("book_status");
BookCopy copy = new BookCopy();
copy.setIsbn(isbn);
copy.setTitle(title);
copy.setAuthor(author);
copy.setPublishDate(publishDate);
copy.setBookPosition(bookPosition);
copy.setBookStatus(bookStatus);
blm.editBookInfo(copy);
url = "./BlmController?flag=false";
isRedirect = true;
//request.setAttribute("list", list); // 수정된거 세팅 ?
} else if(cmd.equals("remove")) {
String bookSeq = request.getParameter("bookSeq");
boolean flag = blm.removeBook(bookSeq);
url = "./BlmController?flag=true";
isRedirect = true;
} else if (cmd.equals("view_regist")) {
url = "./manager/book_regist.jsp";
} else if (cmd.equals("detail")) {
String bookSeq = request.getParameter("bookSeq");
BookCopy bookCopy = blm.getBookByBookSeq(bookSeq);
// 해당하는 book_seq 없으면 null, 처리해줘야함
request.setAttribute("bookCopy", bookCopy);
url = "./manager/book_detail.jsp";
}
if (!isRedirect) {
RequestDispatcher rd = request.getRequestDispatcher(url);
rd.forward(request, response);
} else {
response.sendRedirect(url);
}
}
/**
* @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
*/
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
Post로 받았지만 doPost(request, response) 내부에서 this.doGet(request, response)를 하여 parameter를 그대로 갖다 사용할 수 있다.
doGet()을 보면 우선
cmd 로 넘어오는 값을 getParameter로 받아 String변수 cmd에 저장해준다.
cmd가 null인 경우에는 list를 기본으로 세팅해준다.
url도 기본적으로 book_list.jsp로 세팅해준다.
서비스 객체 blm을 생성해주고 redirect 여부를 판별할 isRedirect를 false로 선언해준다.
book_regist로 부터는 "regist"나 "list"만 넘어올 것이기 때문에 이 두개에 대한 조건만 확인하겠다.
먼저 regist에 대한 부분부터 보겠다.
request로 입력받았던 8개의 데이터를 변수로 만들어준다. Timestamp 객체는 시간 단위도 설정이 필요하므로 뒤에 " 00:00:00"과 같은 식으로 붙여준다. (DateTimeFormatter 같은 걸로 할 수 있는 듯한데 계속 쫑나서 임시방편으로 시간 붙임)
Data 정보 담을 BookCopy 객체를 하나 만들고 받아온 값들을 setting 해준다.
그 다음 서비스로 가서 registBook() 작업을 실행한다.
BlmService의 registBook() 메서드이다.
BookCopy를 넘겨받고 로직 처리를 담당할 BlmDAO의 insertBook(copy)를 실행한다.
public boolean insertBook(BookCopy copy){
boolean flag = false;
String sql1 = "insert into book_info values (?,?,?,?)";
String sql2 = "insert into book_copy(book_isbn) values (?)";
Connection con = manager.getConnection();
try {
con.setAutoCommit(false);
PreparedStatement pstmt = con.prepareStatement(sql1);
pstmt.setString(1, copy.getIsbn());
pstmt.setString(2, copy.getTitle());
pstmt.setString(3, copy.getAuthor());
pstmt.setTimestamp(4, copy.getPublishDate());
int affectedCount = pstmt.executeUpdate();
if(affectedCount>0) {
pstmt = con.prepareStatement(sql2);
pstmt.setString(1, copy.getIsbn());
affectedCount = pstmt.executeUpdate();
if(affectedCount>0) {
flag = true;
con.commit();
System.out.println("commit");
}
} else {
con.rollback();
}
} catch (SQLException e) {
e.printStackTrace();
try {
con.rollback();
System.out.println("rollback");
} catch (SQLException e1) {
e1.printStackTrace();
}
} finally {
try {
con.setAutoCommit(true);
manager.closeConnection(null, null, con);
} catch (SQLException e) {
e.printStackTrace();
}
}
return flag;
}
book_info 테이블과 book_copy 테이블에 값을 삽입할 sql 쿼리문 두개를 사용할 것이다.
Connection을 생성해주고 setAutoCommit()을 false로 해준다.
기본적으로 새 연결은 autoCommit이 true로 설정되는데, autoCommit 모드이면 해당 연결의 모든 SQL 문이 개별 트랜잭션으로 실행되고 커밋됨.
autoCommit 모드가 아니면 SQL문은 commit() 메서드 또는 rollback() 메서드 호출로 종료되는 트랜잭션으로 그룹화 됨.
↳ 두 쿼리가 모두 성공적으로 동작할 때, commit() 해줘야하고 하나라도 안되면 rollback() 해줘야 하는듯.
finally 문에는 다시 autoCommit을 true로 돌려놔주고 close()를 해준다.
return type이 boolean이라 flag를 반환해주는데 내부에서 commit / rollback 여부를 출력해주므로 굳이 필요 없는것 같다. (원래는 컨트롤러까지 가져가는게 맞는듯)
dao 처리가 끝나면 controller로 돌아와 url을 book_regist로 세팅해주고 isRedirect를 true가 되게 한다.
isRedirect가 true인 경우 response의 sendRedirect를 실행시켜 book_regist를 다시 띄워준다.
도서등록 버튼 누르면 alert 창을 띄워서 알려주고
입력 화면을 다시 비춰준다.
다음은 도서리스트 버튼을 눌렀을 때인 cmd가 "list"인 경우이다.
DB에 저장된 책에 대한 목록을 페이지에 띄워줄 것이다.
BookCopy에 대한 ArrayList 객체를 하나 만들어주고 Service에서 searchBookAll()을 실행시킨다.
리턴해줄 list를 만든 후 dao의 selectBookAll() 메서드를 실행한다.
public ArrayList<BookCopy> selectBookAll(){
ArrayList<BookCopy> list = null;
list = new ArrayList<BookCopy>();
BookCopy copy = null;
StringBuilder sb = new StringBuilder("select a.*, b.* from book_info a ");
sb.append("inner join book_copy b on a.book_isbn=b.book_isbn");
String sql = sb.toString();
Connection con = manager.getConnection();
PreparedStatement pstmt;
try {
pstmt = con.prepareStatement(sql);
ResultSet rs = pstmt.executeQuery();
while(rs.next()) {
copy = new BookCopy();
copy.setIsbn(rs.getString(1));
copy.setTitle(rs.getString(2));
copy.setAuthor(rs.getString(3));
copy.setPublishDate(rs.getTimestamp(4));
copy.setBookSeq(rs.getInt(5));
copy.setBookPosition(rs.getString(6));
copy.setBookStatus(rs.getString(7));
list.add(copy);
}
manager.closeConnection(rs, pstmt, con);
} catch (SQLException e) {
e.printStackTrace();
}
return list;
}
select 쿼리문을 사용하여 선택한 책의 모든 정보를 받아올 것이다.
book_info 테이블과 book_copy 테이블을 inner join 하여 외래키 book_isbn이 서로 같은 경우 정보들을 select 한다.
ResultSet을 준비해주고, BookCopy 객체를 만든뒤 select 된 값들을 세팅해준다. 그리고 ArrayList에 하나씩 추가해준다.
다시 컨트롤러로 돌아와서 list에 대한 값들을 페이지에서 갖다 쓰도록 setAttribute 해주면 된다.
url은 기본적으로 book_list.jsp로 세팅이 되어있으니 바로 다음 코드를 실행한다.
서블릿 내에서 다른 리소스(서블릿, jsp 등)로의 요청 전달을 위해 RequestDispatcher를 사용할 것이다.
전달하고자 하는 리소스의 경로를 getRequestDispatcher의 parameter로 주어 생성한다.
RequestDispatcher의 forward() 메서드로 현재 request와 response 객체를 전달한다. 이를 통해 현재 서블릿의 실행을 중단하고 전달된 리소스로 제어를 넘긴다. 전달된 리소스는 새로운 요청으로 처리되며, 클라이언트에게는 원본 요청 서블릿의 실행 결과가 반환된다.
쉽게 말해, RequestDispatcher는 새 페이지로 갈 때, 정보 연장시키는 역할을 한다.
forward() 메서드를 호출하면 새로운 요청으로 인식되기 때문에 웹 브라우저의 주소창에는 전달된 리소스의 URL이 표시되지 않는다. → 클라이언트는 리소스 간의 전환 과정을 인지하지 못하며, 브라우저에서는 새로운 요청이 발생한 것으로 인식한다.
▷ RequestDispatcher 활용 상황
- 웹 애플리케이션 내에서 다른 다른 서블릿, JSP 등으로 제어 흐름을 전달하고자 할 때
- 공통적인 로직이나 뷰를 처리하는 서블릿이나 JSP로 제어를 전달하고자 할 때
- 데이터를 공유하거나 필터링 등의 전처리를 수행하는 작업 등을 위해 다른 리소스로 전달하고자 할 때
도서 리스트 페이지를 띄워보겠다.
마지막 row엔 등록시켰던 데이터도 잘 나옴을 확인할 수 있다.
리스트 페이지에서는 도서 제목을 누르면 책의 세부정보 확인 및 수정을 할 수 있는 기능과 테이블 우측의 삭제를 누르면 해당 row를 삭제하는 기능을 가지게 할 것이다.
<%@page import="bitedu.bipa.member.vo.BookCopy"%>
<%@page import="java.util.ArrayList"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Book List</title>
<style>
table, td, th {
border : 1px solid black;
border-collapse: collapse;
margin: 20px auto;
}
td {
width: 150px;
height: 50px;
padding: 5px;
font-size: 20px;
/* text-align: center; */
}
input , select {
font-size: 20px;
}
.data_ui {
/* width: 250px; */
height: 50px;
}
button {
font-size: 15px;
margin: 5px;
}
#sending {
text-align: center;
}
input.poster :disabled {
background: gray;
}
#form {
font-size: 30px;
}
#title {
height: 80px;
font-size: 50px;
}
</style>
</head>
<body>
<!-- 아래 두 줄이 param... 줄과 같음 -->
<!-- < %
// 코드가 들어가면 안좋다
String cmd = request.getParameter("flag");
cmd = cmd == null ? "" : cmd;
%> -->
<!-- < %=cmd.equals("true")? "<script>alert('삭제성공');</script>":""%> -->
${param.flag=='true'?"<script>alert('삭제성공');</script>":""}
<%
ArrayList<BookCopy> list = (ArrayList<BookCopy>)request.getAttribute("list");
%>
<table>
<tr><th colspan="5" id="title">도서리스트</th></tr>
<tr><td>순번</td><td>타이틀</td><td>저자</td><td>출판일</td><td></td></tr>
<!-- expression language 사용 -->
<c:forEach var="copy" items="${list}"> <!-- 앞의 c는 prefix. 위에서 정의한 uri와 같음 -->
<!-- < % for(BookCopy copy : list) { % > 이거 대신 위의 c:forEach 사용 -->
<tr>
<td>${copy.bookSeq}</td>
<td><a href='./BlmController?cmd=detail&bookSeq=${copy.bookSeq}'>${copy.title}</a></td>
<td>${copy.author}</td>
<td><fmt:formatDate value="${copy.publishDate}" pattern="yyyy-MM-dd"/> </td>
<td><a href="./BlmController?cmd=remove&bookSeq=${copy.bookSeq}">삭제</a></td>
</tr>
<!-- < % } % > -->
</c:forEach>
<tr><td colspan="5"><a href="/MemberSample0629/BlmController?cmd=view_regist"><button>도서등록</button></a></td></tr>
</table>
</body>
</html>
book_list.jsp 이다.
여기서 앞서 말한 jstl을 써볼 것이다.
body 태그 아래를 보면, scriptlet으로 다음과 같이 구성했다.
삭제 처리 여부를 alert로 띄워주기 위한 코드인데, 서블릿에서 boolean 값으로 flag가 넘어오고 그 값이 true인 경우 alert를 띄운다.
하지만 Front 쪽에서 이러한 코드가 들어가면 좋지 않다고 하며, 이를 대체할 코드가 다음과 같다.
${param.flag=='true'?"<script>alert('삭제성공');</script>":""}
컨트롤러 상에서 URL을 지정할 때 get 값으로 "./BlmController?flag=true"와 같이 넘겨줄 것인데, 이 경우 JSTL에서 ${param.flag}로 받을 수 있다. (request.getParameter("flag")와 동일하다)
도서들의 리스트도 받아 온 후 화면에 뿌려줄 것이다.
테이블 각 셀에 보여질 내용이다.
책 순서, 책 번호로 알아올 책 제목, 저자, 출판일, 삭제 텍스트 링크를 for-each 문으로 구현했다.
하지만, 이것도 expression language로 바꾸는 것이 좋은 듯하다. (객체화에 시간 걸리는게 이유인 듯하다. (Servlet 동작에 텀이 있는 이유))
<!-- expression language 사용 -->
<c:forEach var="copy" items="${list}"> <!-- 앞의 c는 prefix. 위에서 정의한 uri와 같음 -->
<tr>
<td>${copy.bookSeq}</td>
<td><a href='./BlmController?cmd=detail&bookSeq=${copy.bookSeq}'>${copy.title}</a></td>
<td>${copy.author}</td>
<td><fmt:formatDate value="${copy.publishDate}" pattern="yyyy-MM-dd"/> </td>
<td><a href="./BlmController?cmd=remove&bookSeq=${copy.bookSeq}">삭제</a></td>
</tr>
</c:forEach>
c:foreach의 c는 prefix로써, 코드 맨 위에 선언해준 taglib의 약어이다.
list를 traverse 할 때, 요소를 copy라는 이름으로 두고 ${copy.bookseq}와 같은 방식으로 쓴다.
fmt prefix는 날짜 형식 포맷을 위해 사용한다. publishDate는 Timestamp 형식이므로 시간 단위까지 출력되어 나오는데, pattern을 "yyyy-MM-dd"로 주면 날짜까지만 나오게 할 수 있다.
책 제목과 삭제는 각각 hypertext에 cmd=detail, cmd=remove를 주어 컨트롤러에서 처리하도록 한다.
컨트롤러를 보겠다.
책 번호를 받아와서 삭제를 실행할것인데, 삭제 여부를 flag로 받아 url의 끝과 isRedirect의 값으로 넣어주면 될 듯하다. (삭제가 안 될 경우가 없을 것이므로 url과 isRedirect에 무조건 true 인 것도 맞는듯)
서비스의 removeBook() 메서드이다. 반환시킬 boolean 변수를 두고 dao의 deleteBook() 메서드를 실행한다. String으로 넘어온 책 순서는 정수로 parsing하여 실행해준다.
book_info와 book_copy의 행을 같이 지워주기 위해서 inner join 문을 사용한다. 각 테이블의 book_isbn이 같으면서 book_copy의 책 순서가 넘어온 bookSeq와 같은 경우 delete 시켜준다.
executeUpdate()가 성공적인 경우라면 정상 작동 후 true를 리턴해줄 것이다.
다음은 책 제목 클릭 시, 해당 책의 정보를 자세히 띄워주고 수정 가능하도록 하는 페이지를 만들 것이다.
목록의 첫번째에 위치한 책을 선택해 보겠다.
컨트롤러에서 cmd가 detail로 들어온 경우이며, 책 번호를 얻어와서 서비스의 getBookByBookSeq() 메서드를 실행한다.
dao의 getBookByBookSeq()로 bookCopy를 받아오고 리턴해준다.
public BookCopy getBookByBookSeq(int bookSeq) {
BookCopy bookCopy = new BookCopy();
String sql = "select c.*, i.* from book_copy c "
+ "inner join book_info i "
+ "on c.book_isbn = i.book_isbn where c.book_seq = ?";
Connection con = manager.getConnection();
PreparedStatement pstmt = null;
try {
pstmt = con.prepareStatement(sql);
pstmt.setInt(1, bookSeq);
ResultSet rs = pstmt.executeQuery();
if (rs.next()) {
bookCopy.setBookSeq(rs.getInt(1));
bookCopy.setBookPosition(rs.getString(2));
bookCopy.setBookStatus(rs.getString(3));
bookCopy.setIsbn(rs.getString(5));
bookCopy.setTitle(rs.getString(6));
bookCopy.setAuthor(rs.getString(7));
bookCopy.setPublishDate(rs.getTimestamp(8));
bookCopy.setPublisher(""); // publish는 테이블에 열 추가 안돼있기 때문에 일단 버림
} else {
return null;
}
System.out.println(bookCopy);
manager.closeConnection(rs, pstmt, con);
} catch (SQLException e) {
e.printStackTrace();
}
return bookCopy;
}
book_copy의 모든 행과 book_info의 모든 행을 isbn이 일치하고 book_seq가 들어온 값에 해당할 때 inner join하는 메서드이다.
출판사(publisher)에 대한 정보는 양쪽 테이블에 존재하지 않으므로 제외시켰음.
선택한 책의 정보를 가져와 상세 화면에 set 해준다.
도서번호는 자동생성, ISBN은 외래키이므로 수정 불가능하게 하고 나머지는 수정 가능하다.
다음과 같이 도서명을 수정하고 리스트를 확인해 보겠다.
수정되었다는 알림과 함께 내용이 수정됨을 확인할 수 있다.
수정화면에 해당하는 book_detail.jsp부터 보겠다.
<%@page import="bitedu.bipa.member.vo.BookCopy"%>
<%@page import="java.util.ArrayList"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>도서 등록</title>
<style>
table, td, th {
border : 1px solid black;
border-collapse: collapse;
margin: 20px auto;
}
td {
width: 150px;
height: 50px;
padding: 5px;
font-size: 20px;
/* text-align: center; */
}
input , select {
font-size: 20px;
}
.data_ui {
/* width: 250px; */
height: 50px;
}
button {
font-size: 15px;
margin: 5px;
}
#sending {
text-align: center;
}
input.poster :disabled {
background: gray;
}
#form {
font-size: 30px;
}
#message {
color: red;
}
</style>
<script type="text/javascript" src="https://code.jquery.com/jquery-latest.min.js"></script>
<script type="text/javascript">
$(document).ready(function() {
$('#book_edit').on('click', function() {
let form = $('#frm');
form.attr('action', '/MemberSample0629/BlmController?cmd=edit');
alert("도서정보가 수정되었습니다.");
form.submit();
});
$('#book_list').on('click', function() {
let form = $('#frm');
form.attr('action', '/MemberSample0629/BlmController?cmd=list');
form.submit();
});
});
</script>
</head>
<body>
<%
BookCopy book_copy = (BookCopy)request.getAttribute("bookCopy");
%>
<form action="" method="post" id="frm">
<table>
<tr><th colspan="4" id="form">도서 상세</th></tr>
<tr><th>구분</th><th class="data_ui" colspan="2">데이터입력</th><th>비고</th></tr>
<tr>
<td>도서번호</td>
<td colspan="2">
<input type="text" id="book_seq" name="book_seq" value="<%=book_copy.getBookSeq()%>" readonly>
</td>
<td id="message"></td></tr>
<tr>
<td>ISBN</td>
<td colspan="2">
<input type="text" id="isbn" name="isbn" value="<%=book_copy.getIsbn()%>" readonly>
</td>
<td>
<input type="hidden" id="flag" value="false">
</td>
</tr>
<tr>
<td>도서명</td>
<td colspan="2">
<input type="text" id="book_title" name="book_title" value="<%=book_copy.getTitle()%>">
</td><td></td>
</tr>
<tr>
<td>저자/역자</td>
<td colspan="2">
<input type="text" id="author" name="author" value="<%=book_copy.getAuthor()%>">
</td><td></td>
</tr>
<!-- <tr>
<td>출판사</td>
<td colspan="2">
<input type="text" id="publisher" size="35" name="publisher" value="<%=book_copy.getPublisher()%>">
</td><td></td>
</tr> -->
<tr>
<td>출판일</td>
<td colspan="2">
<input type="text" id="publish_date" size="35" name="publish_date" value="<%=book_copy.getPublishDate()%>">
</td>
<td></td>
<tr>
<tr>
<td>도서위치</td>
<td colspan="2">
<select name="book_position" value="<%=book_copy.getBookPosition()%>">
<option value='BS'>--도서 위치--
<option value='BS-001' selected>일반서가
<option value='BS-002'>예약서가
<option value='BS-'>회원
</select>
</td>
<td></td>
<tr>
<tr>
<td>도서상태</td>
<td colspan="2">
<select name="book_status" value="<%=book_copy.getBookStatus()%>">
<option value='BM'>--도서 상태--
<option value='BM-001' selected>도서대출서비스
<option value='BM-002'>도서수선
<option value='BM-003'>도서저장고
</select>
</td>
<td></td>
<tr>
<tr>
<td colspan="4" id="sending">
<input type="submit" value="도서수정" id="book_edit">
<input type="reset" value="원래대로">
<input type="submit" value="도서리스트" id="book_list">
</td>
</tr>
</table>
</form>
</body>
</html>
book_regist.jsp와 거의 유사하다.
도서 리스트에서 선택했던 책 정보 BookCopy를 가져오고, 각 텍스트 박스 및 셀렉트 박스의 value에 값을 세팅해 놔야 한다.
book_seq와 isbn은 readonly로 두어 값 수정할수 없게 만들어준다.
버튼 이벤트는 도서수정과 도서리스트로 가는 것만 해주면 되는데, 도서리스트는 book_regist.jsp의 내용과 동일하다.
수정 버튼은 cmd에 edit를 넣어서 보낸다.
바로 서블릿 컨트롤러를 보겠다.
페이지 내의 7가지 정보를 받아와서 BookCopy 객체에 담아주고 서비스의 editBookInfo() 메서드를 실행시켜준다.
성공 실패 여부를 판별한 flag를 두고 update 결과에 따라 결과를 알려준다. (서비스에서 알려주는게 맞는가? 테이블 수정이 중점이니 중요하지 않을듯)
public boolean updateBookInfo(BookCopy copy) {
boolean flag = false;
// query문 작성시 ?를 ''로 감싸면
// java.sql.SQLException: Parameter index out of range (1 > number of parameters, which is 0) 이런거 뜬다.
String sql = "update book_copy c "
+ "inner join book_info i "
+ "on c.book_isbn = i.book_isbn "
+ "set c.book_position=?, "
+ "c.book_status=?, "
+ "i.book_title=?, i.book_author=?, "
+ "i.book_published_date=? "
+ "where i.book_isbn=?";
try {
Connection con = manager.getConnection();
PreparedStatement pstmt = con.prepareStatement(sql);
pstmt.setString(1, copy.getBookPosition());
pstmt.setString(2, copy.getBookStatus());
pstmt.setString(3, copy.getTitle());
pstmt.setString(4, copy.getAuthor());
pstmt.setTimestamp(5, copy.getPublishDate());
pstmt.setString(6, copy.getIsbn());
int affectedCount = pstmt.executeUpdate();
if (affectedCount > 0) {
flag = true;
}
manager.closeConnection(null, pstmt, con);
} catch (SQLException e) {
e.printStackTrace();
}
return flag;
}
book_copy 테이블과 book_info테이블의 isbn이 일치하는 경우 입력받은 데이터들을 각 테이블에 맞게 inner join 후 update 해주는 메서드이다.
(쿼리 문 처음에는 답도 없었는데 하다보니 어느정도 익숙해짐)
어쨌든 프론트엔드와 백엔드를 연결한 동적 Library CRUD 프로그램의 기능들을 구현해봤다.
아래의 주소에는 소개한 프로그램의 war파일이 있으니 필요시 참고바람.
https://github.com/jungwu2503/BitProject/blob/main/DynamicLibraryCRUD.war
'Spring' 카테고리의 다른 글
(스프링 관련 정보) web.xml / servlet-context.xml / root-context.xml 에 대하여 (1) | 2023.07.16 |
---|---|
페이지네이션 (Pagination) - Servlet 프로그래밍 (0) | 2023.07.08 |
Servlet 프로그래밍 (동적 웹 회원가입 폼 만들기) (0) | 2023.07.01 |
Servlet, WAS에 대한 정리 (동적 웹 프로그래밍) (0) | 2023.06.30 |
단축 URL API 설계 (네이버) (0) | 2022.11.09 |
댓글