[Spring Boot] 10. TransactionManager를 활용한 트랜잭션 관리

하림·2024년 9월 2일

Spring

목록 보기
11/16
post-thumbnail

1. 트랜잭션

1.1 트랜잭션의 개념

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


1.2 트랜잭션의 전형적인 예


1.3 트랜잭션 설정 및 처리

데이터베이스 작업 수행 중에는 다음과 같은 상황에서 오류가 발생할 수 있습니다.

  1. 데이터베이스 자체에서 작업 수행 중 에러가 발생할 수 있습니다.
  2. 일반적인 비즈니스 로직에서 에러가 발생할 수 있습니다.

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




2. 프로젝트 생성 및 설정

2.1 프로젝트 생성 및 의존성 설정하기

프로젝트를 생성하는 방법은 다음과 같습니다.

  1. 메뉴에서 File > New > Spring Starter Project 를 선택합니다.
    프로젝트 생성창이 뜨면 프로젝트를 설정합니다.
  2. 생성할 프로젝트에 필요한 의존성을 설정합니다. 필요한 의존성은 다음과 같습니다: Spring Web, Lombok, JDBC API, Oracle Driver, Mybatis Framework

프로젝트 생성이 완료되었습니다.


2.2 JSP 설정 및 기본 프로젝트 구성하기

JSP 설정과 기본 프로젝트 구성 방법에 대해서는 이 링크를 참조하세요.




3. 테이블 생성

3.1 테이블 생성하기

다음과 같이 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 계정에 연결하고 실행합니다.


3.2 더미데이터 생성하기

다음과 같이 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 계정에 연결하고 실행합니다.




4. 구매 페이지

4.1 소스코드 작성하기

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>

이제 구매 페이지로 진입할 수 있습니다.


4.2 실행하기

다음과 같이 실행됩니다.

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

구매 페이지로 진입하는 기능이 성공적으로 구현되었습니다.




5. 구매 처리하기

5.1 소스코드 작성하기

컨트롤러를 생성합니다. 다음과 같이 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.2 실행하기

다음과 같이 실행됩니다.

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

콘솔에는 다음과 같이 출력됩니다.

transaction_pay 입력성공
transaction_ticket 입력성공

이번에는 수량에 5 초과의 수를 입력하고 '전송하기' 버튼을 누릅니다.

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

콘솔에는 다음과 같이 출력됩니다.

transaction_pay 입력성공
ORA-02290: 체크 제약조건(MUSTHAVE.SYS_C008566)이 위배되었습니다

마지막으로 5 이하의 수량을 입력하고, 에러발생 체크박스를 체크한 후 '전송하기' 버튼을 클릭합니다.

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

콘솔에는 다음과 같이 출력됩니다.

transaction_pay 입력성공
java.lang.NumberFormatException: For input string: "100원"

결과적으로 트랜잭션이 완료되면 저장하여 변경 사항을 저장하고, 문제가 발생하면 철회하여 이전 상태로 되돌립니다.

0개의 댓글