도도한 개발자

[DB] 트랜잭션 전파 - 외부 트랜잭션과 내부 트랜잭션은 서로 어떻게 영향을 미칠까? 본문

Backend

[DB] 트랜잭션 전파 - 외부 트랜잭션과 내부 트랜잭션은 서로 어떻게 영향을 미칠까?

Kiara Kim 2023. 8. 27. 21:55

인프런 김영한님의 스프링 DB 2편 - 데이터 접근 활용 기술 강의 중 스프링 트랜잭션 전파 섹션을 기반으로 작성한 글입니다.

 

트랜잭션 전파를 정리하기 전에 트랜잭션의 동작 과정을 빠르게 짚어보겠습니다.

🐈‍⬛ 트랜잭션이란?

데이터베이스의 상태를 변화시키기 위해서 수행하는 작업의 단위를 뜻합니다.

 

로그를 찍으며 확인해봅시다.

트랜잭션 매니저를 통해 트랜잭션을 시작하고 트랜잭션을 커밋하는 코드

"트랜잭션 시작" 아래 Creating new transaction으로 트랜잭션을 시작했고, Hikari connection pool에서 커넥션을 획득합니다. 

트랜잭션을 커밋하면 커넥션이 반환되어 트랜잭션이 종료됩니다. 롤백도 마찬가지로 트랜잭션 시작 - 롤백 - 트랜잭션 종료 - 커넥션 반환이 되죠.

 

🐈‍⬛ 만약 트랜잭션이 두 개라면?

파란색 박스는 트랜잭션의 시작과 종료에 따라 커넥션이 사용되고 반환되는 과정입니다. 순서대로

 

1. 트랜잭션1을 시작하고 커넥션 풀에서 conn0 커넥션을 획득한 걸 볼 수 있습니다.

2. 트랜잭션1을 커밋하고 커넥션 풀에 conn0 커넥션을 반환합니다.

3. 트랜잭션2을 시작하고 커넥션 풀에서 conn0 커넥션을 획득합니다.

4. 트랜잭션2를 커밋하고 커넥션 풀에 conn0 커넥션을 반환합니다.

 

💭 둘은 같은 커넥션을 쓰나보네?

얼핏보면 이 둘은 같은 커넥션을 사용하는 것 처럼 보이지만 엄밀히 말하면 이 두 커넥션은 다른 커넥션입니다. 커넥션 객체의 주소를 보고 구분을 해야 하는데요, 

트랜잭션1은 HikariProxyConnection@1632648448 wrapping conn0

트랜잭션2는 HikariProxyConnection@1261832834 wrapping conn0

으로 주소가 다르게 되어 있는 걸 볼 수 있습니다. 

 

이렇게 커넥션 객체의 주소가 다른 이유는, 히카리 커넥션 풀에서 커넥션을 획득할 때 실제 커넥션을 그대로 반환하는 것이 아니라 히카리 프록시 커넥션이라는 객체를 생성해서 반환하기 때문입니다. 즉, 내부적으로 conn0 커넥션이 재사용된 것 뿐 같은 커넥션을 공유한 것은 아닙니다. 커넥션이 달라 트랜잭션이 각각 수행되면 사용되는 DB 커넥션도 서로 달라집니다.

🐈‍⬛ 트랜잭션 전파

위의 사례는 트랜잭션을 각각 사용하는 것이었습니다. 커넥션이 다르니까요. 그렇다면 이미 진행중인 트랜잭션에 추가로 트랜잭션을 수행하면 어떻게 될까요? 기존의 트랜잭션과 별도의 트랜잭션이 시작될까요 아님 기존 트랜잭션을 이어 받아 수행될까요?

 

이럴 때 어떻게 동작할지 결정하는 것을 트랜잭션 전파라고 합니다.

 

트랜잭션 전파엔 다양한 옵션이 제공되는데 default가 REQUIRED. 다른 옵션들은 다음 글에서 설명드리겠습니다.

🐾 내부 트랜잭션의 외부 트랜잭션 참여

한 트랜잭션이 수행중일 때 다른 트랜잭션이 시작됐다고 해봅시다.

먼저 수행된 트랜잭션은 상대적으로 바깥에 있으니 외부 트랜잭션, 나중에 수행된 트랜잭션을 내부 트랜잭션이라고 하고요.

그럼 위와 같이 두 개의 트랜잭션으로 나뉘게 되는데, 스프링의 경우 아래와 같이 외부 트랜잭션과 내부 트랜잭션을 묶어 하나의 트랜잭션늘 만들어줍니다. 이를 내부 트랜잭션이 외부 트랜잭션에 참여한다라고 표현합니다.

 

이 때 스프링은 논리 트랜잭션과 물리 트랜잭션이라는 개념을 사용하는데, 그림으로 보면 다음과 같습니다.

 

물리 트랜잭션은 실제 데이터베이스에 적용되는 트랜잭션으로, 실제 커넥션을 통해 트랜잭션을 시작하고 커밋, 롤백하는 단위입니다.

 

💭 그럼 논리 트랜잭션은 가짜 커넥션을 사용하는 것인가?

💭 내부 트랜잭션이 롤백되면 내부만 롤백되고 외부는 커밋이 잘 이뤄질까? 

 

💭 내부 외부 트랜잭션의 커밋 롤백이 서로에게 영향을 줄까?

 

많은 궁금증이 생기는데요, 천천히 알아봅시다. 지금부터 보이는 예시들은 모두 내부 트랜잭션이 외부 트랜잭션에 참여한 경우입니다.(마지막 REQUIRED_NEW 옵션 설정의 경우 제외) 한 트랜잭션이 다른 트랜잭션에 참여했다는 말인 즉슨, 외부에서 시작된 물리 트랜잭션의 범위가 내부 트랜잭션까지 넓어졌다고 볼 수 있고, 외부 트랜잭션과 내부 트랜잭션이 하나의 물리 트랜잭션으로 묶였다고도 표현할 수 있습니다.


🐈‍⬛ 내부 커밋 & 외부 커밋

내부 트랜잭션이 외부 트랜잭션에 참여한다는 말은, 새로운 트랜잭션이 만들어지지 않는다는 말과 같습니다. 그러니 isNewTransaction의 값이 외부 트랜잭션과는 다르게 false로 나온 것을 볼 수 있습니다.

 

💭 외부 내부 트랜잭션은 하나의 물리 트랜잭션으로 묶였는데 어떻게 커밋을 두 번할 수 있지? 자고로 트랜잭션은 커밋이나 롤백을 하면 끝나버리는 것이 아닌가?

 

로그를 볼까요? "내부 트랜잭션의 시작"을 알리고 그 밑에 Participating in existing transaction을 확인할 수 있습니다. 반복해서  말하지만 내부 트랜잭션이 기존의 외부 트랜잭션에 참여한다는 뜻입니다. "내부 트랜잭션의 커밋"을 알리는 로그 아래를 보면 DB 커넥션을 통한 트랜잭션의 커밋을 알리는 로그가 찍혀있지 않습니다. 그 아래 Committing JDBC transaction on Connection으로 외부 트랜잭션은 잘 커밋됐는데 말이죠.

 

결론부터 말하자면 스프링은 여러 트랜잭션이 참여에 참여를 더해 함께 사용되는 경우 트랜잭션을 시작한 외부 트랜잭션만 물리 트랜잭션을 관리할 수 있게 합니다.

내부 트랜잭션은 커밋할 때 그 커밋은 실질적으로 유효하지 않은데, 만약 내부 트랜젹선이 실제 물리 트랜잭션을 커밋하면 트랜잭션이 끝나버리기 때문입니다. 즉, 트랜잭션을 끝까지 이어갈 수 없게 되는 것이죠.

 

트랜잭션의 전파의 요청과 응답을 그림으로 알아볼까요?

 

🐾 내부 커밋 & 외부 커밋 - 요청

내부 트랜잭션이 외부 트랜잭션에 참여하는 과정

[요청] 외부 트랜잭션

 1. txManager.getTransaction() 메서드를 호출해서 외부 트랜잭션을 시작한다.

 2. 트랜잭션 매니저는 DataSource를 통해 커넥션을 생성한다.

 3. 생성한 커넥션을 수동 커밋 모드(setAutoCommit(false)로 설정한다. ⇒ 물리 트랜잭션 시작

DataSourceTransactionManager의 doBegin메서드

 4. 트랜잭션 매니저는 생성한 커넥션을 트랜잭션 동기화 매니저에 보관한다.

 5. 트랜잭션 매니저는 TransactionStatus에 담아 반환한다. 여기엔 신규 트랜잭션의 여부와 더불어 다음과 같은 속성이 담겨있다.

  6. 로직 A가 사용되고, 커넥션이 필요하면 트랜잭션 동기화 매니저의 커넥션을 가져와 사용한다.

 

[요청] 내부 트랜잭션

7.  txManager.getTransaction()을 통해 내부 트랜잭션을 시작한다.

8.  트랜잭션 매니저는 트랜잭션 동기화 매니저에 기존 트랜잭션이 존재하는지 확인한다.

9.  기존 트랜잭션이 있으므로 기존 트랜잭션에 참여한다.

10. 트랜잭션 매니저는 트랜잭션의 생성 결과는 TransactionStatus에 담아 반환한다. 이땐 신규 트랜잭션이 아니다.

11. 로직 B가 사용되고, 커넥션이 필요한 경우 트랜잭션 동기화 매니저의 커넥션을 가져와 사용한다.

 

🐾 내부 커밋 & 외부 커밋 - 응답

[응답] 내부 트랜잭션

12. 로직 B가 끝나고 트랜잭션 매니저를 통해 내부 트랜잭션을 커밋한다.

13. 트랜잭션 매니저는 커밋 시점에 신규 트랜잭션 여부에 따라 다르게 동작하는데, 이 경우엔 신규 트랜잭션이 아니기 때문에 커밋을 호출하지 않는다.

 

[응답] 외부 트랜잭션

14. 로직 A가 끝나고 트랜잭션 매니저를 통해 외부 트랜잭션을 커밋한다.

15. 트랜잭션 매니저는 외부 트랜잭션을 신규 트랜잭션으로 판단해서 DB 커넥션에 실제 커밋을 호출한다.

16. 트랜잭션 매니저에 커밋하는 것이 논리 커밋이라면, 실제 커넥션에 커밋하는 것을 물리 커밋이라 할 수 있다. 실제 데이터베이스에 커밋이 반영되면, 물리 트랜잭션도 끝난다.

 

정리하자면, 

신규 트랜잭션인 경우에만 실제 커넥션을 사용해서 물리 커밋과 롤백을 수행합니다. 또한 신규 트랜잭션이 아니면 실제 물리 커넥션을 사용하지 않습니다.


🐈‍⬛ 내부 롤백 & 외부 커밋

이번엔 내부 트랜잭션이 롤백되고 외부 트랜잭션은 커밋이 되는 상황을 봅시다.

외부 트랜잭션이 시작되고 내부 트랜잭션이 이에 참여하는 과정은 위에서 확인했습니다. 그런데 내부 트랜잭션을 커밋이 아닌 롤백을 하면 어떻게 될까요? "내부 트랜잭션 롤백" 로그 아래를 봅시다.

 

Participating transaction failed - marking existing transaction as rollback-only

트랜잭션 참여에 실패했다? 그 다음을 보면 기존의 트랜잭션을 rollback-only로 표시했다고 합니다. 이게 무슨 말인지는 외부 트랜잭션의 커밋 이후의 로그를 보면 알 수 있습니다.

 

Global transaction is marked as rollback-only

외부 트랜잭션은 커밋을 호출했지만 전역(=물리) 트랜잭션이 rollback-only로 표시되어있음을 확인합니다. 그러고 나서 트랜잭션을 롤백시키죠.

 

동작 과정을 그림으로 알아봅시다.

🐾 내부 롤백 & 외부 커밋 - 응답

[응답] 내부 트랜잭션

12. 로직 B가 끝나고 트랜잭션 매니저를 통해 내부 트랜잭션을 롤백한다.

13. 내부 트랜잭션은 신규 트랜잭션이 아니니 트랜잭션 매니저는 실제 롤백을 호출하지 않는다.

14. 내부 트랜잭션은 자신의 롤백을 알리기 위한 방법으로 트랜잭션 동기화 매니저에 rollback-only=true 표시를 한다.

 

[응답] 외부 트랜잭션

15. 로직 A가 끝나고 트랜잭션 매니저를 통해 외부 트랜잭션을 커밋한다.

16. 외부 트랜잭션은 신규 트랜잭션이니 DB 커넥션에 실제 커밋을 호출한다. 이때 트랜잭션 동기화 매니저에 rollback-only=true 표시를 17. 확인하여 물리 트랜잭션 롤백한다.

18. 실제 데이터베이스에 롤백이 반영되고, 물리 트랜잭션도 끝난다.

      개발자는 분명 커밋을 기대했는데 rollback-only=true 표시로 인해 롤백이 되었다. 커밋을 시도했지만, 예상하지 않은 롤백 발생을 알리기 위해 스프링은 이 경우 UnexpectedRollbackException 런타임 예외를 던진다.

 

정리하자면,

논리 트랜잭션이 하나라도 롤백되면 물리 트랜잭션은 롤백됩니다. 내부 논리 트랜잭션이 롤백되면 rollback-only=true를 표시하고, 외부 트랜잭션을 커밋할 때 해당 마크를 확인합니다. rollback-only가 true로 되어 있으면 물리 트랜잭션을 롤백한 후 UnexpectedRollbackException 예외를 던집니다.


🐈‍⬛ 내부 커밋 & 외부 롤백

그러면 이 경우는 어떨까요? 내부에선 커밋이 발생하고 외부에선 롤백이 발생했을 때 내부 로직의 변경사항만 반영이 될까요? 아님 전체가 다 롤백이 될까요?

논리 트랜잭션이 하나라도 롤백되면 이들을 감싸는 물리 트랜잭션은 롤백됩니다. 즉, 내부 로직에 변경사항이 생겨 커밋이 되어도 외부 트랜잭션에서 롤백하면 전체가 롤백됩니다.

 

🐾 내부 커밋 & 외부 롤백 - 응답

[응답] 내부 트랜잭션

12. 로직 B 가 끝나고 트랜잭션 매니저를 통해 내부 트랜잭션을 커밋한다.

13. 내부 트랜잭션은 신규 트랜잭션이 아니니 트랜잭션 매니저는 실제 롤백을 호출하지 않는다.

 

[응답] 외부 트랜잭션

14. 로직 A가 끝나고 트랜잭션 매니저를 통해 외부 트랜잭션을 롤백한다

15. 외부 트랜잭션은 신규 트랜잭션이니 DB 커넥션에 실제 롤백을 호출한다.

16. 트랜잭션 매니저에 롤백하는 것이 논리적인 롤백이라면, 실제 커넥션에 롤백하는 것을 물리 롤백이라 할 수 있다. DB에 롤백이 반영되고, 물리 트랜잭션도 끝난다.

 

이쯤되면 궁금한 점이 생깁니다.

💭 외부에서 롤백되어도 내부에서 커밋되면 내부 커밋만이라도 반영할 순 없나? 그 반대는 안되나?

이런 경우 외부 트랜잭션과 내부 트랜잭션을 완전히 분리해 사용하는 방법을 떠올릴 수 있습니다. 이때 말하는 트랜잭션의 완전한 분리란 별도의 물리 트랜잭션을 사용한다는 뜻입니다. 즉, 각각의 커밋 및 롤백이 서로에게 영향을 주지 않도록 하는 것입니다.

 

해당 방법에 관련된 정리는 다음 글에 정리가 될 예정이라 오늘은 여기까지 정리하겠습니다.

긴 글 읽어주셔서 감사합니다.

 

 

'Backend' 카테고리의 다른 글

[DB] 트랜잭션 전파 - REQUIRES_NEW  (0) 2023.08.28
Swagger - API 문서 자동화 사용기  (0) 2023.06.07