일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- SQL 처리
- 레이디스코드
- nginx
- 악보
- Inside Of Me
- 신입
- IT
- 봄 사랑 벚꽃 말고
- 인덱스
- db
- 니가 참 좋아
- 러블리즈
- 오라클
- 장범준
- 핑거스타일
- DBMS 구성요소
- 말 더듬
- DBMS
- 개발자
- 6학년 8반 1분단
- 슬픔의 후에
- 오라클 아키텍처
- index
- 기타
- 데이터베이스
- 스위트라떼
- 천공의 시간
- oracle
- I'm fine thank you
- 아이유
취미로 음악을 하는 개발자
[Spring Boot] Transaction (Manager, Template, Propagation) 본문
[Spring Boot] Transaction (Manager, Template, Propagation)
영월특별시 2019. 8. 22. 14:14Transaction 사용하는 이유
A가 B에게 이체를 해야한다고 했을 때, 아래와 같은 코드로 만들 수 있다. 로직만 본다면 이상이 없다.
1 2 3 4 5 6 7 8 9 10 | try { A 계좌 잔고 조회; if (이체 가능) { A 계좌 잔고 감소 업데이트; // 데이터베이스 작업수행 1 B 계좌 잔고 증가 업데이트; // 데이터베이스 작업수행 2 } } catch (Exception e) { System.out.println("에러발생"); } // 거래 중 에러가 발생하면 롤백 시킨다. | cs |
하지만 만약 이체할 수 있는 금액이 없는데 있는 것처럼 오류가 난 상태에서 이체를 시도하려고하면 큰 손실을 가져온다.
이것들을 위한 해결방법이 트랜잭션인데, 트랜잭션은 해당 범위안에서 에러가 나면 그 범위안에서 수행한 작업을 롤백한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 | try { 작업1; balance.put(id, amount); 작업2; // 데이터베이스 작업수행1 order.put(id, count); 작업3; // 데이터베이스 작업수행2 } catch (Exception e) { System.out.println("에러발생"); } // 데이터베이스 작업 자체의 에러일 수도 이고, 일반 오류일 수도 있다. // 여기 코드부분에서 에러가 발생하면 데이터베이스에 수행한 작업을 롤백하겠다. // 여기 코드부분에서 에러가 없다면 데이터베이스에 수행한 작업을 커밋하겠다. | cs |
프로젝트 생성
코드 구현
* 1,2 로 나누어진 클래스들 중 1의 코드만 적어놨지만 2의 코드는 1의 코드에서 1을 2로만 바꿔주면 된다.
1 2 3 4 5 6 7 8 9 | package com.study.springboot.dto; import lombok.Data; @Data public class Transaction1Dto { private String consumerId; private int amount; } | cs |
고객 아이디와 티켓의 구매수를 가진 테이블
1 2 3 4 5 6 7 8 | package com.study.springboot.dao; import org.apache.ibatis.annotations.Mapper; @Mapper public interface ITransaction1Dao { public void pay(String consumerId, int amount); } | cs |
1 2 3 4 5 6 7 8 9 10 11 | <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.study.springboot.dao.ITransaction1Dao"> <insert id="pay"> insert into transaction1 (consumerId, amount) values (#{param1}, #{param2}) </insert> </mapper> | cs |
1 2 3 4 5 | package com.study.springboot.service; public interface IBuyTicketService { public int buy(String consumerId, int money, String error); } | cs |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | package com.study.springboot.service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionStatus; import com.study.springboot.dao.ITransaction1Dao; import com.study.springboot.dao.ITransaction2Dao; @Service public class BuyTicketService implements IBuyTicketService { @Autowired ITransaction1Dao transaction1; @Autowired ITransaction2Dao transaction2; @Autowired PlatformTransactionManager transactionManager; @Autowired TransactionDefinition definition; @Override public int buy(String consumerId, int amount, String error) { TransactionStatus status = transactionManager.getTransaction(definition); try { transaction1.pay(consumerId, amount); // 의도적 에러 발생 if (error.equals("1")) { int n = 10 / 0; } transaction2.pay(consumerId, amount); transactionManager.commit(status); return 1; } catch(Exception e) { System.out.println("[PlatformTransactionManager] Rollback"); transactionManager.rollback(status); return 0; } } } | cs |
status가 선언되는 부분부터 트랜잭션을 설정한다고 보면 된다.
37번 줄까지 정상적으로 호출되면 그대로 커밋하고 중간에 에러가 발생하면 예외를 발생시켜 로그를 출력하고 롤백한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | package com.study.springboot; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import com.study.springboot.service.IBuyTicketService; @Controller public class MyController { @Autowired IBuyTicketService buyTicket; @RequestMapping("/") public @ResponseBody String root() throws Exception { return "Transaction"; } @RequestMapping("/buy_ticket") public String buy_ticket() { return "buy_ticket"; } @RequestMapping("/buy_ticket_card") public String buy_ticket_card(@RequestParam("consumerId") String consumerId, @RequestParam("amount") String amount, @RequestParam("error") String error, Model model) { int nResult = buyTicket.buy(consumerId, Integer.parseInt(amount), error); model.addAttribute("consumerId", consumerId); model.addAttribute("amount", amount); if (nResult == 1) return "buy_ticket_end"; else return "buy_ticket_error"; } } | cs |
buy_ticket은 buy_ticket.jsp를 호출하고 buy_ticket_card는 buy_ticket.jsp에서 입력된 데이터를 가지고 실행되는데 buy_ticket.jsp의 form 안의 변수들을 가지고 buy함수를 실행한다.
이에 따라 consumerId, amount, error 값이 결정되는데 consumerId와 amount는 모델의 속성 값으로 들어간다.
단, error가 발생여부에 따라 buy_ticket_end.jsp나 buy_ticket_error.jsp가 호출된다.
// buy_ticket.jsp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | <%@ 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> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>BuyTicket</title> </head> <body> <p>카드 결제</p> <form action="buy_ticket_card"> 고객 아이디 : <input type="text" name="consumerId"> <br /> 티켓 구매수 : <input type="text" name="amount"> <br /> 에러 발생 여부 : <input type="text" name="error" value="0"> <br /> <input type="submit" value="구매"> <br /> </form> <hr> 에러 발생 여부에 1을 입력하면 에러가 발생합니다. </body> </html> | cs |
// buy_ticket_end.jsp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <%@ 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> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>BuyTicketEnd</title> </head> <body> buy_ticket_end.jsp 입니다. <br /> ${consumerId } <br /> ${amount } <br /> </body> </html> | cs |
// buy_ticket.error.jsp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <%@ 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> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>BuyTicketError</title> </head> <body> buy_ticket_error.jsp 입니다. <br /> <h1> 에러 발생 </h1> ${consumerId } <br /> ${amount } <br /> </body> </html> | cs |
// transaction1, 2 테이블 현재 상태
// 정상적인 결과
오류가 발생하지 않으면 정상적으로 결과가 커밋된다.
// 오류 발생
코드 상에서 보는 것과 같이 오류 발생 전(transaction1)에는 오류 여부와 상관없이 값이 커밋되었고,
오류 발생 후(transaction2)에는 커밋이 아니라 롤백이 된 것을 볼 수 있다.
또 다른 코드, BuyTicketService
TransactionManager를 안 쓰고 TransactionTemplate를 쓸 수도 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | package com.study.springboot.service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionTemplate; import com.study.springboot.dao.ITransaction1Dao; import com.study.springboot.dao.ITransaction2Dao; @Service public class BuyTicketService implements IBuyTicketService { @Autowired ITransaction1Dao transaction1; @Autowired ITransaction2Dao transaction2; @Autowired TransactionTemplate transactionTemplate; @Override public int buy(String consumerId, int amount, String error) { try { transactionTemplate.execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus arg0) { transaction1.pay(consumerId, amount); // 의도적 에러 발생 if (error.equals("1")) { int n = 10 / 0; } transaction2.pay(consumerId, amount); } }); return 1; } catch(Exception e) { System.out.println("[PlatformTransactionManager] Rollback"); return 0; } } } | cs |
TransactionTemplate는 트랜잭션과 관련된 작업을 한 번에 처리해주는 클래스로,
Manager와는 설정 방법만 다를 뿐 같은 결과가 나옴, 커밋과 롤백이 같이 구성 되어 있어서 Template를 더 선호하는 편.
Manager의 결과는 위의 결과 사진처럼 트랜잭션1은 커밋이 되었고 트랜잭션2는 롤백이 되었지만
Template의 결과는 에러가 발생하면 둘 다 롤백이 되므로 데이터베이스에 데이터가 들어가지 않는다.
Propagation 속성
: A 클래스에서 트랜잭션이 수행될 때, 그 안에 있는 B 클래스의 메소드를 호출하는 상황이 있다. 그런데 B 클래스에도 트랜잭션이 있을 때, 이 트랜잭션들 간의 관계 설정하는 것
required (0) : Default, 전체 처리
-> A의 트랜잭션과 B의 트랜잭션을 합침
supports (1) : 기존 트랜잭션에 의존
mandatory (2) : 트랜잭션에 꼭 포함되어야 하고 트랜잭션이 있는 곳에서 호출해야 함.
requires_new (3) : 각각 트랜잭션을 처리
-> A의 트랜잭션과 B의 트랜잭션을 각각 분리
not_supported (4) : 트랜잭션에 포함하지 않고 기존의 트랜잭션이 존재하면 일시 중지.
메소드 실행이 끝난 후에 트랜잭션을 계속 진행.
never (5) : 트랜잭션에 절대 포함하지 않고 트랜잭션이 있는 곳에서 호출하면 에러 발생.
-> 현재 트랜잭션은 다른 트랜잭션에 포함시키지 않을 때 사용.
Propagation 속성 사용하는 코드, MyController와 BuyTicketService
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | package com.study.springboot; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionTemplate; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import com.study.springboot.dao.ITransaction3Dao; import com.study.springboot.service.IBuyTicketService; @Controller public class MyController { @Autowired IBuyTicketService buyTicket; @Autowired TransactionTemplate transactionTemplate; @Autowired ITransaction3Dao transaction3; @RequestMapping("/") public @ResponseBody String root() throws Exception { return "Transaction"; } @RequestMapping("/buy_ticket") public String buy_ticket() { return "buy_ticket"; } @RequestMapping("/buy_ticket_card") public String buy_ticket_card(@RequestParam("consumerId") String consumerId, @RequestParam("amount") String amount, @RequestParam("error") String error, Model model) { model.addAttribute("consumerId", consumerId); model.addAttribute("amount", amount); try { transactionTemplate.execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus arg0) { int nResult = buyTicket.buy(consumerId, Integer.parseInt(amount), error); // 의도적 에러 발생 if (error.equals("2")) { int n = 10 / 0; } transaction3.pay(consumerId, Integer.parseInt(amount)); } }); } catch (Exception e) { System.out.println("[Transaction Propagation #1] Rollback"); return "buy_ticket_end"; } return "buy_ticket_error"; } } | cs |
새로운 트랜잭션3를 Autowired 설정을 하고 44번줄에서 트랜잭션을 사용 중인데 그 중간에 48번 줄처럼 새로운 트랜잭션을 사용 중인 상태.
Propagation은 "두 번째 실행된 트랜잭션이 첫 번째 실행된 트랜잭션에 어떤 방식으로 참여를 하는가"라고 생각하면 됨.
* MyController에 구현되어 있지만 원래는 Service 단에 구현해야 함
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | package com.study.springboot.service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionTemplate; import com.study.springboot.dao.ITransaction1Dao; import com.study.springboot.dao.ITransaction2Dao; @Service public class BuyTicketService implements IBuyTicketService { @Autowired ITransaction1Dao transaction1; @Autowired ITransaction2Dao transaction2; @Autowired TransactionTemplate transactionTemplate; @Transactional(propagation=Propagation.REQUIRES_NEW) //@Transactional(propagation=Propagation.REQUIRED) @Override public int buy(String consumerId, int amount, String error) { try { transactionTemplate.execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus arg0) { transaction1.pay(consumerId, amount); // 의도적 에러 발생 if (error.equals("1")) { int n = 10 / 0; } transaction2.pay(consumerId, amount); } }); return 1; } catch(Exception e) { System.out.println("[PlatformTransactionManager] Rollback"); return 0; } } } | cs |
현재 전파속성은 REQUIRES_NEW 타입으로 독립적으로 처리하고 있다.
에러가 0이면 정상 처리가 되기 때문에 트랜잭션 1, 2, 3 모두 커밋이 된다.
에러를 2로 입력하면 MyController에서 48번 줄은 정상적으로 처리되지만 그 다음에 에러가 발생했으므로 54번 줄은 오류로 처리가 된다.
따라서 트랜잭션 1과 2는 커밋, 3은 롤백 상태가 된다.
만약 에러를 1로 입력하면 48번 줄부터 오류가 발생하므로 트랜잭션 1, 2, 3 모두 롤백 상태가 된다.
전파속성을 REQUIRED로 바꾸면 트랜잭션 전체를 합치는 방식이 된다.
이 때는 에러가 1이나 2일 때 모두 롤백 상태가 된다. 그래서 에러가 발생하지 않았는데 롤백이 되는 상황을 방지하기 위해서는 전파 속성을 'NEVER'로 설정하면 된다.
'공대인 > Spring[Boot]' 카테고리의 다른 글
[Spring Boot] Security Form, Status Check (0) | 2019.08.24 |
---|---|
[Spring Boot] Security (0) | 2019.08.22 |
[Spring Boot] logback (0) | 2019.08.16 |
[Spring Boot] MyBatis HashMap 사용 (2) | 2019.08.16 |
[Spring Boot] MyBatis 파라미터 사용 (0) | 2019.08.14 |