트랜잭션을 설명하기 전에 아래의 코드를 살펴보자.
pstmtIns = conn.prepareStatement(
"insert into ARTICLE (ID, TITLE, CONTENT) values (?, ?, ?)";
pstmtIns.setInt(1, id);
...
pstmtIns.executeUpdate();
pstmtUpdate = conn.prepareStatement(
"update BOARD set LASTARTICLELD = ? where BOARDID = ?");
pstmtUpdate.setInt(1, id);
...
pstmtUpdate.executeUpdate();
위 코드는 먼저 INSERT 쿼리를 이용해서 ARTICLE 테이블에 데이터를 삽입하는데 ID 칼럼의 값으로 id 변수를 설정한다. INSERT 쿼리가 정상 실행되면 BOARD 테이블의 LASTARTICLEID 칼럼 값을 id 변수의 값으로 변경한다. 즉, BOARD 테이블의 LASTARTICLEID 칼럼은 마지막에 저장된 ARTICLE의 식별 값을 저장하도록 코드를 작성했다.
두 쿼리가 모두 정상적으로 실행되면 ARTICLE 테이블과 BOARD 테이블에는 올바른 값이 들어갈 것이다. 그런데 만약 첫 번째 INSERT 쿼리를 실행한 뒤 두 번째 UPDATE 쿼리를 실행하는 과정에서 익셉션이 발생하면 어떻게 될까? 이 경우 ARTICLE 테이블에는 새로운 데이터가 삽입되었는데 BOARD 테이블의 LASTARTICLEID 칼럼의 값은 변경되지 않을 것이다. 이는 곧 데이터가 잘못된 상태로 저장되는 것을 의미한다.
이 예에서는 두 쿼리를 모두 정상적으로 실행해야 데이터가 올바르게 유지된다. 이렇게 두 개 이상의 쿼리를 모두 성공적으로 실행해야 데이터가 정상적으로 처리되는 경우 DBMS는 트랜잭션(transaction)
을 이용해서 두 개 잇아의 쿼리를 마치 한 개의 쿼리처럼 처리할 수 있도록 하고 있다.
트랜잭션은 시작과 종료를 갖고 있다. 트랜잭션이 시작되면 이후로 실행되는 쿼리 결과는 DBMS에 곧바로 반영되지 않고 임시로 보관된다. 이후 트랜잭션을 커밋(commit)
하면 임시 보관한 모든 쿼리 결과를 실제 데이터에 반영한다.
트랜잭션을 커밋하기 전에 에러가 발생하면 임시로 보관한 모든 쿼리 결과를 실제 데이터에 반영하지 않고 취소한다. 즉, 트랜잭션이 시작되면 트랜잭션 범위 내에 있는 모든 쿼리 결과가 DB에 반영되거나 또는 반영되지 않게 된다. 이때 트랜잭션을 반영하지 않고 취소하는 것을 롤백(rollback)
이라고 한다.
트랜잭션을 구현하는 방법에는 다음의 두 가지 방식이 있다.
단일 데이터베이스에 접근하는 경우 JDBC 기바의 트랜잭션을 이용하여 구현하고, 두 개 이상의 데이터베이스를 트랜잭션으로 처리하려면 JTA를 이용해야 한다. 여기서는 JDBC API에 대한 내용만 살펴보도록 한다.
JDBC API를 이용해서 두 개 이상의 쿼리를 트랜잭션으로 묶어서 처리하고 싶다면 다음과 같이 쿼리를 실행하기 전에 Connection.setAutoCommit() 메서드에 false를 값으로 전달해서 트랜잭션을 시작하면 된다.
try{
conn = DriverManager.getConnection(...);
// 트랜잭션 시작
conn.setAutoCommit(false);
... // 쿼리 실행
... // 쿼리 실행
// 트랜잭션 커밋
conn.commit();
}catch(SQLException ex){
if(conn != null){
// 트랜잭션 롤백
conn.rollback();
}
}finally{
if(conn != null){
try{
conn.close();
}catch(SQLException ex){}
}
}
트랜잭션이 실제로 어떻게 동작하는지 살펴보기 위해 간단하게 두 개의 테이블에 데이터를 삽입하는 예제를 작성해보도록 하자. 먼저 다음의 쿼리를 이용해서 두 개의 테이블을 생성해보자.
create table ITEM(
ITEM_ID int not null primary key,
NAME varchar(100)
) engine = InnoDB default character set = utf8;
create table ITEM_DETAIL(
ITEM_ID int not null primary key,
DETAIL varchar(200)
) engine = InnoDB default character set = utf8;
이제 위 두 테이블에 데이터를 삽입하는 JSP 코드를 다음과 같이 작성하자.
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.sql.DriverManager" %>
<%@ page import="java.sql.Connection" %>
<%@ page import="java.sql.PreparedStatement" %>
<%@ page import="java.sql.SQLException" %>
<%
String idValue = request.getParameter("id");
Connection conn = null;
PreparedStatement pstmtItem = null;
PreparedStatement pstmtDetail = null;
String jdbcDriver = "jdbc:mysql://localhost:3306/jsptest?"+"useUnicode=true&characterEncoding=utf8";
String dbUser="jspexam";
String dbPass="jsppw";
Throwable occuredException = null;
try{
int id = Integer.parseInt(idValue);
conn = DriverManager.getConnection(jdbcDriver, dbUser, dbPass);
conn.setAutoCommit(false);
pstmtItem = conn.prepareStatement("insert into ITEM values (?, ?)");
pstmtItem.setInt(1, id);
pstmtItem.setString(2, "상품 이름 " + id);
pstmtItem.executeUpdate();
if(request.getParameter("error") != null){
throw new Exception("의도적 익셉션 발생");
}
pstmtDetail = conn.prepareStatement("insert into ITEM_DETAL values (?, ?)");
pstmtDetail.setInt(1, id);
pstmtDetail.setString(2, "상세 설명 " + id);
pstmtDetail.executeUpdate();
conn.commit();
}catch (Throwable e){
if(conn != null){
try{
conn.rollback();
}catch (SQLException ex){}
}
occuredException = e;
}finally {
if(pstmtItem != null) try{ pstmtItem.close();} catch (SQLException ex) {}
if(pstmtDetail != null) try{ pstmtDetail.close();} catch (SQLException ex){}
if(conn != null) try{ conn.close();} catch (SQLException ex){}
}
%>
<html>
<head>
<title>ITEM 값 입력</title>
</head>
<body>
<% if(occuredException != null) {%>
에러가 발생하였음: <%=occuredException.getMessage()%>
<% } else { %>
데이터가 성공적으로 들어감
<% } %>
</body>
</html>
insertItem.jsp는 id 파라미터로 전달받은 값을 이용해서 ITEM 테이블과 ITEM_DETAIL 테이블에 삽입할 데이터를 생성한다. 첫 번째 쿼리 실행후 error 파라미터 값이 null이 아니면 익셉션을 발생시켜서 두 번째 쿼리를 실행하지 않는다. insertItem.jsp는 첫 번째 쿼리를 실행하기 전에트랜잭션을 시작하기 때문에 error 파라미터가 존재할 경우 첫 번째 쿼리 실행 결과가 DB에 반영되지 않고 롤백된다.
실제로 다음의 두 가지 URL을 요청해보자.
http://localhost:8080/chap14/insertItem.jsp?id=1 URL을 실행하면 아래와 같은 결과 화면이 출력될 것이다. (이 URL을 두 번 실행하면 이미 동일한 값의 주요키가 존재하기 때문에 주요키 중복 익셉션이 나타난다.)
DB에서 ITEM 테이블과 ITEM_DETAIL 테이블을 조회해 보면 주요키가 1인 데이터가 올바르게 삽입된 것을 확인할 수 있다.
http://localhost:8080/chap14/insertItem.jsp?id=2&error=4 URL을 실행하면 error 파라미터가 존재하므로 첫 번째 쿼리 실행 후 익셉션을 발생시키고 트랜잭션이 롤백된다. 실행 결과는 아래와 같다.
실행 후 ITEM 테이블과 ITEM_DETAIL 테이블을 조회해보면 주요키가 2인 데이터가 ITEM 테이블과 ITEM_DETAIL 테이블에 모두 삽입되지 않은 것을 확인할 수 있을 것이다. 즉, ITEM 테이블에 데이터를 삽입해주는 첫 번째 쿼리 실행 후 트랜잭션이 롤백된 경우, 쿼리 실행 결과가 DB에 반영되지 않는다는 것을 확인할 수 있다.
참고