취미로 음악을 하는 개발자

[Spring Boot] Transaction (Manager, Template, Propagation) 본문

공대인/Spring[Boot]

[Spring Boot] Transaction (Manager, Template, Propagation)

영월특별시 2019. 8. 22. 14:14
728x90

Transaction 사용하는 이유


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
Comments