트랜잭션(Transaction)은 데이터베이스의 상태를 변환시키는 하나의 논리적 기능을 수행하는 작업의 단위입니다. 트랜잭션은 여러 개의 연산(예: INSERT, SELECT, UPDATE, DELETE)을 한꺼번에 수행하여 데이터베이스의 상태를 변경합니다. 각각의 트랜잭션은 완료되면 Commit(저장)하여 변경 사항을 확정하거나, 문제가 발생하면 Rollback(철회)하여 이전 상태로 되돌릴 수 있습니다.

데이터베이스 작업 수행 중에는 다음과 같은 상황에서 오류가 발생할 수 있습니다.
작업 범위 내에서 에러가 발생하면, 이미 수행된 작업을 롤백(Rollback)하여 원래 상태로 되돌립니다. 반대로, 에러가 발생하지 않으면 데이터베이스에 수행한 모든 작업을 커밋(Commit)하여 변경 사항을 확정합니다.

프로젝트를 생성하는 방법은 다음과 같습니다.
프로젝트 생성이 완료되었습니다.
JSP 설정과 기본 프로젝트 구성 방법에 대해서는 이 링크를 참조하세요.
다음과 같이 JDBC실습.sql 파일에 작성합니다.
/* 트랜젝션 처리
: 티켓 구매와 결제를 동시에 시도해서 구매에 오류가 발생 하는 경우 모든 업무를 Rollback 처리한다.
두 개의 업무가 모두 정상적으로 처리되면 Commit해서 테이블에 적용한다. */
-- 기존 테이블 삭제
drop table transaction_pay;
drop table transaction_ticket;
-- 티켓 구매 금액을 입력하는 테이블
create table transaction_pay (
userid varchar2(30) not null,
amount number not null
);
-- 구매한 티켓의 개수를 입력하는 테이블
-- check 제약조건에 의해 5장을 초과하면 에러가 발생한다.
create table transaction_ticket (
userid varchar2(30) not null,
t_count number(2) not null
check(t_count <= 5)
);
Musthave 계정에 연결하고 실행합니다.
다음과 같이 JDBC실습.sql 파일에 작성합니다.
-- 데이터 입력 테스트 1 (정상적인 구매가 이루어지는 업무)
insert into transaction_pay values ('harim', 40000); -- 입력성공
insert into transaction_ticket values ('harim', 4); -- 입력성공
-- 데이터 입력 테스트 2
-- 6장부터는 구매할 수 없으므로 아래 쿼리문은 금액부분만 입력되고, 매수 부분은 입력되지 않는다.
-- 즉, 트랜젝션 처리가 되지 않아 매수와 금액이 일치하지 않는 오류가 발생한다.
insert into transaction_pay values ('error', 90000); -- 입력성공
insert into transaction_ticket values ('error', 9); -- check 제약조건 위배로 입력 불가
-- 확인 및 커밋
select * from transaction_pay;
select * from transaction_ticket;
commit;
Musthave 계정에 연결하고 실행합니다.
DTO를 생성합니다. 다음과 같이 com.edu.springboot.jdbc.PayDTO.java 파일을 작성합니다.
package com.edu.springboot.jdbc;
import lombok.Data;
@Data
public class PayDTO {
private String userid;
private int amount;
}
DTO를 생성합니다. 다음과 같이 com.edu.springboot.jdbc.TicketDTO.java 파일을 작성합니다.
package com.edu.springboot.jdbc;
import lombok.Data;
@Data
public class TicketDTO {
private String userid;
private int t_count;
}
서비스 인터페이스를 작성합니다. 다음과 같이 com.edu.springboot.jdbc.ITicketService.java 파일을 작성합니다.
package com.edu.springboot.jdbc;
import org.apache.ibatis.annotations.Mapper;
/* 구매한 티켓과 금액에 대한 insert 처리를 위한 추상메서드 정의 */
@Mapper
public interface ITicketService {
// 매개변수는 커맨드 객체로 처리하기 위해 각 DTO 객체를 사용
public int ticketInsert(TicketDTO ticketDTO);
public int payInsert(PayDTO payDTO);
}
컨트롤러를 작성합니다. 다음과 같이 com.edu.springboot.MainController.java 파일을 작성합니다.
package com.edu.springboot;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.web.bind.annotation.RequestMapping;
import com.edu.springboot.jdbc.ITicketService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
public class MainController {
// Mybatis 사용을 위한 인터페이스 자동 주입
@Autowired
ITicketService dao;
/* 트랜잭션 처리를 위한 빈 자동주입.
* 별도의 설정없이 스프링 컨테이너가 미리 생성해둔 것을 즉시 주입받아 사용할 수 있다. */
@Autowired
PlatformTransactionManager transactionManager;
@Autowired
TransactionDefinition definition;
@RequestMapping("/")
public String home() {
return "home";
}
// 티켓 구매 페이지 매핑
@GetMapping("/buyTicket.do")
public String buy1() {
return "buy";
}
}
홈을 작성합니다. 다음과 같이 webapp/WEB-INF/views/home.jsp 파일을 작성합니다.
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h2>스프링 부트 프로젝트</h2>
<ul>
<li><a href="/">루트</a></li>
</ul>
<h2>TransactionManager로 트랜젝션 처리</h2>
<ul>
<li><a href="./buyTicket.do">티켓구매</a></li>
</ul>
</body>
</html>
뷰를 생성합니다. 다음과 같이 webapp/WEB-INF/views/buy.jsp 파일을 작성합니다.
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h2>티켓 구매하기</h2>
<form action="buyTicket.do" method="post">
<table border="1">
<tr>
<th>아이디</th>
<td><input type="text" name="userid" value="" /></td>
</tr>
<tr>
<th>수량</th>
<td>
<!-- forEach 태그를 이용해서 <option> 태그를 반복해서 추가 -->
<select name="t_count">
<c:forEach begin="1" end="10" step="1" var="num">
<option value="${ num }">${ num }</option>
</c:forEach>
</select>
</td>
</tr>
<tr>
<th>에러발생</th>
<td>
<!-- DB에서 오류가 없더라도, Java 코드에서 인위적인 에러를 발생시킬 수 있도록 하기 위한 체크박스 -->
<input type="checkbox" name="err_flag" value="1" />
체크하면 예외가 발생합니다.
</td>
</tr>
</table>
<input type="submit" value="전송하기" />
</form>
</body>
</html>
이제 구매 페이지로 진입할 수 있습니다.
다음과 같이 실행됩니다.

'티켓구매' 링크를 클릭하면 구매 페이지로 이동합니다.

구매 페이지로 진입하는 기능이 성공적으로 구현되었습니다.
컨트롤러를 생성합니다. 다음과 같이 com.edu.springboot.MainController.java 파일에 코드를 추가합니다.
// 구매 처리
@PostMapping("/buyTicket.do")
public String buy2(TicketDTO ticketDTO, PayDTO payDTO, HttpServletRequest req, Model model) {
/* 폼값을 받기 위한 DTO 객체와 request 내장 객체 그리고 Model 객체로 매개변수 선언 */
// 구매에 성공한 경우 포워드 할 View의 경로
String viewPath = "success";
/* 자동 주입된 빈을 통해 Status 인스턴스를 생성한다. 이를 통해 트랜젝션 처리를 할 수 있다.
* 전체 작업에 대해 성공이면 commit, 실패라면 rollback 처리를 하게 된다. */
TransactionStatus status = transactionManager.getTransaction(definition);
try {
// 1. DB처리 1 : 구매금액에 관련된 입금처리. 구매장수 * 10000원
payDTO.setAmount(ticketDTO.getT_count() * 10000);
int result1 = dao.payInsert(payDTO);
// insert 성공시 콘솔에 로그 출력
if (result1 == 1) System.out.println("transaction_pay 입력 성공");
// 2. 비지니스 로직 처리(의도적인 에러발생 부분)
String errFlag = req.getParameter("err_flag");
if (errFlag != null) {
/* 구매페이지에서 체크박스에 체크한 경우 이 코드가 실행되어 예외가 발생된다.
* 문자는 정수로 변환할 수 없으므로 NumberFormatException이 발생된다. */
int money = Integer.parseInt("100원");
}
// 3. DB처리 2 : 구매한 티켓 매수에 대한 처리로 6장 이상이면 check 제약조건 위배로 DB에러가 발생한다.
int result2 = dao.ticketInsert(ticketDTO);
if (result2 == 1) System.out.println("transaction_ticket 입력성공");
// DTO를 모델에 저장
model.addAttribute("ticketDTO", ticketDTO);
model.addAttribute("payDTO", payDTO);
// 모든 작업에 대해 정상적으로 처리되었다면 commit 한다.
transactionManager.commit(status);
}
catch (Exception e) {
/* 위 3개의 단위작업 중 하나라도 오류가 발생되면 전체 작업을 rollback 한 후 에러페이지로 포워드한다. */
e.printStackTrace();
viewPath = "error";
transactionManager.rollback(status);
}
// View로 포워드
return viewPath;
}
매퍼를 생성합니다. 다음과 같이 src/main/resources/mybatis/mapper/TicketPayDAO.xml 파일에 코드를 추가합니다.
<insert id="ticketInsert"
parameterType="com.edu.springboot.jdbc.TicketDTO">
INSERT INTO transaction_ticket (userid, t_count) VALUES (#{userid}, #{t_count})
</insert>
뷰를 생성합니다. 다음과 같이 webapp/WEB-INF/views/error.jsp 파일을 작성합니다.
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h2>티켓 구매 실패</h2>
<a href="buyTicket.do">티켓구매</a>
</body>
</html>
뷰를 생성합니다. 다음과 같이 webapp/WEB-INF/views/success.jsp 파일을 작성합니다.
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h2>티켓 구매 성공</h2>
아이디 : ${ ticketDTO.userid }<br/>
구매수 : ${ ticketDTO.t_count }<br/>
결제금액 : ${ ticketDTO.amount }<br/>
<a href="buyTicket.do">티켓구매</a>
</body>
</html>
이제 구매 처리를 할 수 있습니다.
다음과 같이 실행됩니다.

수량에 5 이하의 수를 입력하고 '전송하기' 버튼을 클릭하면 티켓 구매에 성공합니다.

콘솔에는 다음과 같이 출력됩니다.
transaction_pay 입력성공
transaction_ticket 입력성공
이번에는 수량에 5 초과의 수를 입력하고 '전송하기' 버튼을 누릅니다.

데이터베이스 작업 수행 중 오류가 발생하여 티켓 구매에 실패합니다.

콘솔에는 다음과 같이 출력됩니다.
transaction_pay 입력성공
ORA-02290: 체크 제약조건(MUSTHAVE.SYS_C008566)이 위배되었습니다
마지막으로 5 이하의 수량을 입력하고, 에러발생 체크박스를 체크한 후 '전송하기' 버튼을 클릭합니다.

비지니스 로직에서 에러가 발생하여 티켓 구매에 실패합니다.

콘솔에는 다음과 같이 출력됩니다.
transaction_pay 입력성공
java.lang.NumberFormatException: For input string: "100원"
결과적으로 트랜잭션이 완료되면 저장하여 변경 사항을 저장하고, 문제가 발생하면 철회하여 이전 상태로 되돌립니다.