https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2/dashboard
이번 파트는 특히 놀라웠다. 이런 깊은 내용을 어떻게 학습하신지.. 선생님의 노하우가 궁금해지는 파트였다.
스프링 트랜잭션 전파에 대한 내용을 중심적으로 적겠다.
스프링 트랜잭션 전파 파트 이전에 중요하게 느낀 부분을 조금 정리하겠다.
정리
- 히카리 커넥션 풀에서 커넥션을 획득하면 실제 커넥션을 반환하는 것이 아니라 내부 관리를 위해 히카리 프록시 커넥션이라는 객체를 생성해서 반환한다.
- 물론 내부에는 실제 커넥션이 포함되어 있다. 이 객체의 주소를 확인하면 커넥션 풀에서 획득한 커넥션을 구분할 수 있다.
- 따라서 둘 다 conn0이더라도 다른 객체르 인식해야 한다.
스프링 트랜잭션 전파
트랜잭션을 각각 사용하는 것이 아니라, 트랜잭션이 이미 진행중인데, 여기에 추가로 트랜잭션을 수행하면 어떻게 될까?
이런 경우 어떻게 동작할지 결정하는 것을 트랜잭션 전파(propagation)라고 한다.
현재 기준은 트랜잭션 전파의 기본 옵션인 REQUIRED를 기준으로 설명한다.
아래 이미지를 통해 외부 트랜잭션과 내부 트랜잭션에 대한 개념을 학습하자.
외부 트랜잭션이라고 이름 붙인 것은 둘 중 상대적으로 밖에 있기 때문에 외부 트랜잭션이라 한다. 처음 시작된 트랜잭션이다.
내부 트랜잭션은 외부에 트랜잭션이 수행되고 있는 도중에 호출되기 때문에 마치 내부에 있는 것 처럼 보여서 내부 트랜잭션이라 한다
스프링은 이 경우 외부 트랜잭션과 내부 트랜잭션을 묶어서 하나의 트랜잭션을 만들어준다.
내부 트랜잭션이 외부 트랜잭션에 참여하는 것이다. 이것이 기본 동작이다.
만들어진 하나의 트랜잭션은 물리 트랜잭션이라고 한다.
외부 트랜잭션, 내부 트랜잭션처럼 물리 트랜잭션 안의 논리적인 단위의 트랜잭션은 논리 트랜잭션이라고 한다.
- 논리 트랜잭션은 하나의 물리 트랜잭션으로 묶인다.
- 물리 트랜잭션이란 우리가 이해하는 실제 데이터베이스에 적용되는 트랜잭션이다.
실제 커넥션을 통해 트랜잭션을 시작하고, 실제 커넥션을 통해 커밋을 진행하고 롤백을 하는 단위다. - 논리 트랜잭션은 트랜잭션 매니저를 통해 트랜잭션을 사용하는 단위다.
- 논리 트랜잭션 개념은 트랜잭션이 실행되는 중에 내부에 추가로 트랜잭션을 사용하는 경우에 나타난다.
단순히 트랜잭션이 하나인 경우는 둘을 구분하지 않는다. - 트랜잭션을 사용할 때 트랜잭션 안에 다른 내부 트랜잭션을 사용하면 복잡한 상황이 발생해 이런 개념이 도입된 것이다.
일단 스프링 전파 원칙은 다음과 같다.
모든 논리 트랜잭션이 커밋되어야 물리 트랜잭션이 커밋된다.
하나의 논리 트랜잭션이라도 롤백되면 물리트랜잭션은 롤백된다.
즉 모든 트랜잭션 매니저를 커밋해야 물리 트랜잭션이 커밋되는 것이다.
하나의 트랜잭션 매니저라도 롤백되면 물리 트랜잭션은 롤백된다.
그럼 이제 외부 트랜잭션과 내부 트랜잭션을 구현한 테스트 코드를 살펴보자.
외부 트랜잭션이 진행 중인데, 내부 트랜잭션을 추가로 수행한다.
@Test
void inner_commit() {
log.info("외부 트랜잭션 시작");
TransactionStatus outer = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("outer.isNewTransaction()={}", outer.isNewTransaction()); //처음 시작한 트랜잭션인가 true
log.info("내부 트랜잭션 시작");
TransactionStatus inner = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("inner.isNewTransaction()={}", inner.isNewTransaction()); //처음 시작한 트랜잭션인가 false
log.info("내부 트랜잭션 커밋");
txManager.commit(inner);
log.info("외부 트랜잭션 커밋");
txManager.commit(outer);
}
내부 트랜잭션을 시작하는 시점에는 이미 외부 트랜잭션이 진행중인 상태다. 이 경우 내부 트랜잭션은 외부 트랜잭션에 참여한다.
내부 트랜잭션이 외부 트랜잭션에 참여한다는 뜻은 내부 트랜잭션이 외부 트랜잭션을 그대로 이어 받아서 따른다는 뜻이다.
정리하면 외부 트랜잭션과 내부 트랜잭션이 하나의 물리 트랜잭션으로 묶이는 것이다!
내부 트랜잭션은 이미 진행중인 외부 트랜잭션에 참여하므로, 새로운 트랜잭션이 아니다.
따라서 inner.isNewTransaction()의 출력값은 false이다.
곰곰히 생각해보면 외부 트랜잭션과 내부 트랜잭션이 하나의 물리 트랜잭션으로 엮인다고 했다.
그런데 코드를 확인하면 코드를 두 번 호출한다. 트랜잭션에서는 하나의 커넥션에 커밋은 한 번만 가능하다.
커밋을 하면 트랜잭션이 종료되고 커넥션이 반납되기 때문이다.
고로 커밋이나 롤백을 하면 해당 트랜잭션은 끝나는데 어떻게 위의 코드는 동작하는 것일까?
아래 이미지는 위의 테스트 코드를 작동하고 출력되는 콘솔창이다. 좀 더 자세히 살펴보자.
- 내부 트랜잭션을 시작할 때 Participating in existing transaction 이라는 메시지를 확인할 수 있다.
이 메시지는 내부 트랜잭션이 기존에 존재하는 외부 트랜잭션에 참여한다는 뜻이다. - 실행 결과를 보면 외부 트랜잭션을 시작하거나 커밋할 때는 DB 커넥션을 통한 물리 트랜잭션을 시작( manual commit )하고,
DB 커넥션을 통해 커밋 하는 것을 확인할 수 있다.
그런데 내부 트랜잭션을 시작하거나 커밋할 때는 DB 커넥션을 통해 커밋하는 로그를 전혀 확인할 수 없다. - 정리하면 외부 트랜잭션만 물리 트랜잭션을 시작하고 커밋한다.
- 만약 내부 트랜잭션에서 실제 물리 트랜잭션을 커밋하면, 마무리가 된다. 그럼 외부 트랜잭션은 로직을 이어갈 수 없다.
따라서 내부 트랜잭션은 데이터베이스 커넥션을 사용한 커밋을 진행할 수 없다. - 스프링은 이렇게 여러 트랜잭션이 함께 사용되는 경우, 처음 트랜잭션을 시작한 외부 트랜잭션이 실제 물리 트랜잭션을 관리하도록 한다.
- 트랜잭션 전파 과정은 아래 그림과 같다.
그림을 통해 중요하게 느끼는 번호만 부연 설명을 남기겠다.
(데이터 소스를 기반으로 커넥션을 생성하고 꺼낸다!!! 잊지 말자!!)
- 9번 : 기존 트랜잭션에 참여하는 것은 아무것도 하지 않는 것이다.
이미 기존 트랜잭션인 외부 트랜잭션에서 물리 트랜잭션을 시작했다.
그리고 물리 트랜잭션이 시작된 커넥션을 트랜잭션 동기화 매니저에 담아두었다.
따라서 이미 물리 트랜잭션이 진행중이므로 그냥 두면 이후 로직이 기존에 시작된 트랜잭션을 자연스럽게 사용하게 되는 것이다.
이후 로직은 자연스럽게 트랜잭션 동기화 매니저에 보관된(로컬쓰레드) 기존 커넥션을 사용하게 된다. - 11번 : 로직2가 사용되고, 커넥션이 필요한 경우 트랜잭션 동기화 매니저를 통해 외부 트랜잭션이 보관한 커넥션을 획득해서 사용한다.
- 13번 : 트랜잭션 매니저는 커밋 시점에 신규 트랜잭션 여부에 따라 다르게 동작한다. 이 경우 신규 트랜잭션이 아니므로 실제 커넥션을 통해 데이터베이스 커밋을 호출하지 않는다. 만약 여기서 커넥션을 통해 커밋이나 롤백을 하면 트랜잭션이 끝나버린다. 따라서 물리 트랜잭션은 외부 트랜잭션을 종료할 때가지 이어져야한다. 잘 기억하자!!!
- 14번 : 외부 트랜잭션을 커밋
- 15번 : 외부 트랜잭션은 신규 트랜잭션이므로, 따라서 커넥션을 통한 DB에 실제 커밋을 한다.
중요하다고 느낀점은 다음과 같다.
먼저 트랜잭션 매니저에 커밋을 한다고 실제 커넥션에 물리 커밋이 발생하지는 않는다.
신규 트랜잭션인 경우에만 실제 커넥션을 통해 물리 커밋과 롤백을 수행한다. 신규 트랜잭션이 아니면 물리 커넥션을 사용하지 않는다.트랜잭션이 내부에서 추가로 사용되면, 트랜잭션 매니저를 통해 논리 트랜잭션을 관리하고,
모든 논리 트랜잭션이 커밋되면 물리 트랜잭션이 커밋된다고 이해하면 된다.
Rollback(롤백)
외부 트랜잭션에서 롤백
만약 내부 트랜잭션에서 커밋을 성공하고, 실제 커넥션을 담당하는 외부 트랜잭션(신규 트랜잭션)에서 롤백이 되면 어떠한 일이 발생할까?
앞의 학습한 내용과 동일하게 실제 데이터베이스 롤백이 반영되고, 물리 트랜잭션도 끝난다.
이 경우는 커넥션을 담당하는 신규 트랜잭션,
즉 외부 트랜잭션이 롤백을 했으므로 데이터베이스에 롤백이 적용되는 것을 이해하기 수월하다.
내부 트랜잭션에서 롤백
우리는 위에서 논리 트랜잭션이 하나라도 롤백되면 물리 트랜잭션이 롤백 된다고 학습했다. 이에 관한 좀 더 깊은 내용을 알아보자.
내부 트랜잭션이 롤백을 하면, 사실 내부 트랜잭션은 물리 트랜잭션에게 영향을 주지 않는다.
일단 이론적으로는 외부 트랜잭션이 커넥션을 사용해 데이터베이스에 커밋을 해야 한다. 그러나 실제 동작은 그렇지 않다.
스프링이 이 문제를 해결하는 방법에 대해 알아보자.
우선 내부 트랜잭션 롤백 실습 테스트 코드를 작성하고 이를 실행해봤다.
내부 트랜잭션이 기존 트랜잭션에 참조하는 것을 확인할 수 있다.
그리고 여기서 내부 트랜잭션 롤백을 진행하고 출력되는 로그에 집중하자.
Participating transaction failed - marking existing transaction as rollback-only
내부 트랜잭션을 롤백하면 실제 물리 트랜잭션은 롤백하지 않는다. 대신에 기존 트랜잭션을 롤백 전용으로 표시한다.
이제 외부 트랜잭션을 커밋하면 다음과 같은 내용이 출력된다.
Global transaction is marked as rollback-only
커밋을 호출했지만, 전체 트랜잭션이 롤백 전용으로 표시되어 있다. 따라서 물리 트랜잭션을 롤백한다.
결론
내부 트랜잭션이 롤백을 하면, 기존 트랜잭션에 롤백 전용이라는 마크를 남긴다.
이 롤백 전용이라는 마크를 어디에 남길까? 바로 트랜잭션 동기화 매니저에 rollbackOnly=true라는 표시를 남긴다.
그럼 외부 트랜잭션(신규 트랜잭션)에서 커밋을 진행할 때,
트랜잭션 동기화 매니저에 롤백 전용(rollbackOnly=true)표시가 있는지 확인한다.
롤백 전용 표시가 있으면 물리 트랜잭션을 커밋하는 것이 아니라 롤백한다.
흐름을 정리한 이미지는 아래와 같다.
전파 옵션
다양한 전파 옵션을 학습했다. 중요한 2가지만 정리해보겠다.
REQUIRED
가장 많이 사용하는 기본 설정이다. 즉 디폴트 값이다. 기존 트랜잭션이 없으면 생성하고, 이미 존재한다면 참여하다.
이번 포스팅은 이 옵션을 기준으로 설명했다. 실무에서는 대부분 이것을 사용한다고 한다.
REQUIRES_NEW
대부분 REQUIRED를 사용하고 종종 이 옵션을 사용한다고 한다.
항상 새로운 트랜잭션을 생성하는 옵션이다.
이 옵션을 사용하면 외부 트랜잭션, 내부 트랜잭션은 서로 영향을 주지 않는다. 즉 별도의 커넥션을 가지는 것이다.
내부 트랜잭션은 자체적으로 커넥션을 가져 롤백을 하고 커밋을 한다. 외부 트랜잭션도 자체적으로 커넥션을 가져 롤백을 하고 커밋을 한다.
실습 코드는 아래와 같다.
@Test
void inner_rollback_requires_new() {
log.info("외부 트랜잭션 시작");
TransactionStatus outer = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("outer.isNewTransaction()={}", outer.isNewTransaction());
log.info("내부 트랜잭션 시작");
DefaultTransactionAttribute definition = new DefaultTransactionAttribute();
definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); //옵션을 준다
TransactionStatus inner = txManager.getTransaction(definition);
log.info("inner.isNewTransaction()={}", inner.isNewTransaction());
log.info("내부 트랜잭션 롤백");
txManager.rollback(inner); //롤백
log.info("외부 트랜잭션 커밋");
txManager.commit(outer); //커밋
}
출력문을 확인하면 내부 트랜잭션은 롤백되고, 외부 트랜잭션은 커밋 된 것을 확인할 수 있다.
아래는 해당 옵션을 사용했을 때 메서드 로직을 보여주는 이미지다.
중요한 부분만 정리하겠다. 트랜잭션 매니저가 커넥션을 트랜잭션 동기화 매니저에 보관하는 것을 잘 기억하자!
- 5번: 트랜잭션 매니저는 트랜잭션을 생성한 결과를 TransactionStatus 에 담아서 반환하는데,
여기에 신규 트랜잭션의 여부가 담겨 있다. isNewTransaction 를 통해 신규 트랜잭션 여부를 확인할 수 있다.
트랜잭션을 처음 시작했으므로 신규 트랜잭션이다.(true)
- 7번: REQUIRES_NEW 옵션과 함께 txManager.getTransaction() 를 호출해서 내부 트랜잭션을 시작한다.
트랜잭션 매니저는 REQUIRES_NEW 옵션을 확인하고, 기존 트랜잭션에 참여하는 것이 아니라 새로운 트랜잭션을 시작한다. - 8번: 트랜잭션 매니저는 데이터 소스를 통해 커넥션을 생성한다.
- 9번: 물리 트랜잭션이 시작된다.
- 10번 : 트랜잭션 매니저는 트랜잭션 동기화매니저에 커넥션을 보관한다.
이때 con1은 잠시 보류되고, 지금부터는 con2을 사용한다. 내부 트랜잭션을 완료할 때까지 con2을 사용한다.
- 3번: 내부 트랜잭션이 con2 물리 트랜잭션을 롤백한다. 트랜잭션이 종료되고 con2은 종료되거나, 커넥션 풀에 반납된다.
이후에 con1의 보류가 끝나고, 다시 사용한다. - 이후에는 외부 트랜잭션도 con1을 사용해 커밋을 하고 반납한다.
이번 강의도 흥미롭게 들을 수 있었다. 사실 이번 스프링 데이터베이스 파트 2 포스팅도 이걸로 끝..?
이제는 계속 복습하자!!! 스프링 고급은 언제 듣지.. 앞으로도 정진하자.
이상으로 포스팅을 마칩니다. 감사합니다.
댓글