Home Transaction 롤백 동작 및 시나리오 정리
Post
Cancel

Transaction 롤백 동작 및 시나리오 정리


Unchecked Exception 발생 시에는 롤백 되지만,
Checked Exception 발생 시에는 롤백되지 않는건 유명한 정보이다.
하지만 예외를 catch 했을때 어떤식으로 처리가 되는지는 햇갈려서 케이스별로 정리해 보려고 한다.

transaction_progation.PNG

스프링에서 지원해주는 전파속성은 위와 같이 많이 존재하지만
실제로 자주 사용하는 REQUIRED, REQUIRES_NEW 만 정리해보겠다.



트랜잭션 X + UnCheckedException

1
2
3
4
5
6
public void test() {  
    boardRepository.save(Board.create());
    throw new RuntimeException();  
}

결과 : 롤백 안됨



트랜잭션 O + UnCheckedException

1
2
3
4
5
6
7
@Transactional
public void test() {  
    boardRepository.save(Board.create());
    throw new RuntimeException();  
}

결과 : 정상 롤백



트랜잭션 X + CheckedException

1
2
3
4
5
6
public void test() throws Exception {  
    boardRepository.save(Board.create());
    throw new IOException();  
}

결과 : 롤백 안됨



트랜잭션 O + CheckedException

1
2
3
4
5
6
7
@Transactional
public void test() throws Exception {  
    boardRepository.save(Board.create());
    throw new IOException();  
}

결과 : 롤백 안됨

여기까진 알고있는 결과와 일치 했다.
이제 예외를 잡은경우를 테스트 해보자. —



트랜잭션 O + UnCheckedException + catch

1
2
3
4
5
6
7
8
9
10
11
@Transactional  
public void test() throws Exception {  
    boardRepository.save(Board.create());
  
    try {  
        throw new RuntimeException();  
    } catch (Exception ignored) {  
    }
}

결과 : 롤백 안됨

위와 같이 예외를 잡아주면 롤백이 진행되지 않았다.
사실 이것도 예상가능한 결과이다.
예외가 전파 되지않고 catch 블록에서 처리됐기 때문이다.

외부에서 호출할때도 마찬가지이다.

1
2
3
4
5
6
7
try {
	service.test()
} catch(Exception ignored) {
}

이런식으로 잡아줘도 롤백이 진행되지 않는다.
롤백을 원한다면 catch문 내부에서 명시적으로 예외를 날려줘야 한다.



부모 트랜잭션 X + 자식 트랜잭션 O + UnCheckedException

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void test() throws Exception {  
    test2();  
}  
  
@Transactional  
public void test2() throws Exception {  
    boardRepository.save(Board.create());
  
    throw new RuntimeException();  
}

결과 : 롤백 안됨

 예제는 유명한 자기호출 이슈이다.

이렇게 적용시 Intellij 에서도 주의 문구를 아래와 같이 띄워준다.

@Transactional self-invocation (in effect, a method within the target object calling another method of the target object) does not lead to an actual transaction at runtime 

자세한 설명은 아래 공식문서 참고
spring docs 참고




부모 트랜잭션 O + 자식 트랜잭션 X + UnCheckedException

1
2
3
4
5
6
7
8
9
10
11
12
@Transactional  
public void test() throws Exception {  
    test2();  
}  
  
public void test2() throws Exception {  
    boardRepository.save(Board.create());
  
    throw new RuntimeException();  
}

결과 : 정상 롤백



부모: REQUIRED + 자식: REQUIRES_NEW

위와같이 전파속성을 설정하고 여러 케이스를 테스트 해보겠다.

자식: UncheckedException

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
// 1번째 케이스
@Transactional  
public void test() throws Exception {  
    test2();  
}  
  
@Transactional(propagation = Propagation.REQUIRES_NEW)  
public void test2() throws Exception {  
    boardRepository.save(Board.create());
    throw new RuntimeException();  
}

결과 : 정상 롤백


-------------------------------------------------------------------
// 2번째 케이스
@Transactional  
public void test() throws Exception {   
    boardRepository.save(Board.create());  
    test2();  
}  
  
@Transactional(propagation = Propagation.REQUIRES_NEW)  
public void test2() throws Exception {    
    boardRepository.save(Board.create());
    throw new RuntimeException();  
}


첫번째 케이스와 다르게 부모 트랜잭션에서도 데이터를 저장해주고 있다.
잠시 생각해보면, parent와 child가 서로 다른 트랜잭션이기 때문에
parent의 데이터는 저장되고 child는 롤백  거라고 예상할  있다.

틀렸다..

코드상으로는 자식의 예외가 부모까지 전파되기 때문에   롤백이 된다.

결과 :   롤백


-------------------------------------------------------------------
// 3번째 케이스

@Transactional  
public void test()  {  
    boardRepository.save(Board.builder().title("te").content("te").build()); 
    System.out.println("em.getDelegate() = " + em.getDelegate());  
  
    try {  
        test2();  
    } catch (Exception ignored) {  
    }  
}  
  
@Transactional(propagation = Propagation.REQUIRES_NEW)  
public void test2() {  
    System.out.println("em.getDelegate() = " + em.getDelegate());  
    boardRepository.save(Board.create());  
    throw new RuntimeException();  
}


그럼 이번엔 자식에서 발생한 예외를 catch해보자

여태까지 배웠던 지식대로면 해당 코드는
child는 롤백되지만 parent는 정상 commit 되어야 한다.

근데  코드를 실행해보면   커밋이 되는걸 알수가있다.

이유가 왜일까?..

비밀은 프록시에 있다.

eneityManager로 getDelegate 값을 찍어보면  메서드의 세션값이 똑같은걸 확인할  있다.

세션값이 같다는건 트랜잭션이 같다는걸 의미한다.
같은 Bean 내에서는 전파속성이 제대로 동작하지 않는다. (내부적으로 같은 트랜잭션범위를 같기때문에)

원하는 결과를 얻기 위해선 빈을 분리해 줘야한다.



-------------------------------------------------------------------
// 4번째 케이스

@Transactional  
public void test()  {  
    boardRepository.save(Board.builder().title("te").content("te").build()); 
    System.out.println("em.getDelegate() = " + em.getDelegate());  
  
    try {  
		testService2.test2();
    } catch (Exception ignored) {  
    }  
}  

 
public class TestService2 {  
    @Transactional(propagation = Propagation.REQUIRES_NEW)  
    public void test2() {  
        boardRepository.save(Board.create());  
        System.out.println("em.getDelegate() = " + em.getDelegate());  
        throw new RuntimeException();
    }  
}

이런식으로 해주면 원하는대로 parent은 commit되고
child는 rollback되는걸 확인할  있다.



부모: UnCheckedException

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
자식 코드는 아래와 같이 고정이다.
@Transactional(propagation = Propagation.REQUIRES_NEW)  
public void test2() {  
    boardRepository.save(Board.create());  
}

@Transactional  
public void test()  {  
    boardRepository.save(Board.builder().title("te").content("te").build());  
     testService2.test2();  
    throw new RuntimeException();
}

parent: rollback
child: commit

독립적인 트랜잭션으로 동작하는걸 확인할  있다.

CheckedException은 따로 테스트를 진행하지 않겠습니다.
CheckedException의 기본 rollback은 옵션은 false 이며,
예외 발생시 롤백을 원한다면 @Transactional 어노테이션의 rollbackFor 옵션을 주면 됩니다.

참고

This post is written by PRO.

Java HashMap 동작 방식

JPA N+1 발생 원인 및 해결방법