-
스프링 DB 데이터접근 기술 - 스프링 트랜잭션 전파 기본Spring-Boot/스프링 DB 2편 - 데이터 접근 기술 2022. 9. 27. 18:31
* 트랜잭션 2개가 실행 시 주의! *
- 트랜잭션1과 트랜잭션2가 같은 `conn0` 커넥션을 사용중이다. 이것은 중간에 커넥션 풀 때문에 그런 것이다.
- 트랜잭션1은 `conn0` 커넥션을 모두 사용하고 커넥션 풀에 반납까지 완료
- 이후에 트랜잭션2가 `conn0`를 커넥션 풀에서 획득함
- 따라서 둘은 완전히 다른 커넥션으로 인지하는 것이 맞다
- 히카리 커넥션 풀에서 커넥션을 다루는 프록시 객체의 주소가 트랜잭션1과 트랜잭션2는 서로 다르다.
- 결과적으로 `conn0`을 통해 커넥션이 재사용 된 것이고 각각의 커넥션 풀에서 커넥션을 조회한 것을 확인
- 트랜잭션이 각각 수행되면서 사용되는 DB 커넥션도 각각 다르다
- 이 경우 트랜잭션을 각자 관리하기 때문에 전체 트랜잭션을 묶을 수 없다.
예를 들어서 트랜잭션1이 커밋하고, 트랜잭션2가 롤백하는 경우 트랜잭션1에서 저장한 데이터는 커밋되고,
트랜잭션2에서 저장한 데이터는 롤백된다.
트랜잭션 전파
- 트랜잭션이 이미 진행 중인데 추가로 트랜잭션을 수행 시 어떻게 동작할 지 결정하는 것이 트랜잭션 전파(propagation)이라 한다.
* 외부 트랜잭션이 수행중인데, 내부 트랜잭션이 추가로 수행됨 *
- 스프링은 이 경우 외부 트랜잭션과 내부 트랜잭션을 묶어서 하나의 트랜잭션을 만들어준다.
- 내부 트랜잭션이 외부 트랜잭션에 참여하는 것이다. 이것이 기본 동작이고, 옵션을 통해 다른 동작방식도 선택할 수 있다.
* 물리 트랜잭션, 논리 트랜잭션 *
- 스프링은 이해를 돕기 위해 논리 트랜잭션과 물리 트랜잭션이라는 개념을 나눈다
- 논리 트랜잭션들을 하나의 물리 트랜잭션으로 묶인다
- 물리 트랜잭션은 우리가 이해하는 실제 데이터베이스에 적용되는 트랜잭션을 뜻한다. 실제 커넥션을 통해서 트랜잭션을 시작(`setAutoCommit(false)`) 하고, 실제 커넥션을 통해서 커밋, 롤백하는 단위이다.
- 논리 트랜잭션은 트랜잭션 매니저를 통해 트랜잭션을 사용하는 단위이다.
- 이러한 논리 트랜잭션 개념은 트랜잭션이 진행되는 중에 내부로 추가로 트랜잭션을 사용하는 경우 나타난다.
* 원칙 *
1. * 모든 논리 트랜잭션이 커밋되어야 물리 트랜잭션이 커밋된다 *
2. * 하나의 논리 트랜잭션이라도 롤백되면 물리 트랜잭션은 롤백된다 *
@Test void inner_commit() { log.info("외부 트랜잭션 시작"); TransactionStatus outer = txManager.getTransaction(new DefaultTransactionAttribute()); log.info("outer.isNewTransaction() = {} ", outer.isNewTransaction()); log.info("내부 트랜잭션 시작"); TransactionStatus inner = txManager.getTransaction(new DefaultTransactionAttribute()); log.info("inner.isNewTransaction() = {} ",inner.isNewTransaction()); log.info("내부 트랜잭션 커밋"); txManager.commit(inner); log.info("외부 트랜잭션 커밋"); txManager.commit(outer); }
- 외부 트랜잭션 수행중인데, 내부 트랜잭션을 추가로 수행했다.
- 외부 트랜잭션은 처음 수행된 트랜잭션이다. 이 경우 신규 트랜잭션( `isNewTransaction=true`) 이된다.
- 내부 트랜잭션을 시작하는 시점에는 이미 외부 트랜잭션이 진행중인 상태이다. 이 경우 내부 트랜잭션은 외부 트랜잭션에 참여한다.
- 트랜잭션 참여
ㆍ내부 트랜잭션이 외부 트랜잭션에 참여한다는 뜻은 내부 트랜잭션이 외부 트랜잭션을
그대로 이어 받아서 따른다는 뜻
ㆍ다른 관점으로 보면 외부 트랜잭션의 범위가 내부 트랜잭션까지 넓어진다는 뜻
ㆍ외부에서 시작된 물리적인 트랜잭션의 범위가 내부 트랜잭션까지 넓어진다는 뜻
ㆍ정리하면 * 외부 트랜잭션과 내부 트랜잭션이 하나의 물리 트랜잭션으로 묶이는 것 *
- 내부 트랜잭션은 이미 진행중인 외부 트랜잭션에 참여한다. 이 경우 신규 트랜잭션이 아니다( `isNEwTransaction=false` )
- 정리하면 외부 트랜잭션만 물리 트랜잭션을 시작하고, 커밋한다.
- 만약 내부 트랜잭션이 실제 물리 트랜잭션을 커밋하면 트랜잭션이 끝나버리기 때문에, 트랜잭션을 처음 시작한 외부 트랜잭션까지 이어갈 수 없다. 따라서 내부 트랜잭션은 DB 커넥션을 통한 물리 트랜잭션을 커밋하면 안된다.
- 스프링은 이렇게 여러 트랜잭션이 함께 사용되는 경우, *처음 트랜잭션을 시작한 외부 트랜잭션이 실제 물리 트랜잭션을 관리* 하도록 한다. 이를 통해 트랜잭션 중복 커밋 문제를 해결한다.
* 핵심 정리 *
- 여기서 핵심은 트랜잭션 매니저에 커밋을 호출한다고해서 항상 실제 커넥션에 물리 커밋이 발생하지 않는다는 점이다
- 신규 트랜잭션인 경우에만 실제 커넥션을 사용해서 물리 커밋과 롤백을 수행한다. 신규 트랜잭션이 아니면 실제 물리 커넥션을 사용하지 않는다.
- 이렇게 트랜잭션이 내부에서 추가로 사용되면 트랜잭션 매니저에 커밋하는 것이 항상 물리 커밋으로 이어지지 않는다. 그래서 이 경우 논리 트랜잭션과 물리 트랜잭션을 나누게 된다. 또는 외부 트랜잭션과 내부 트랜잭션으로 나누어 설명하기도 한다.
- 트랜잭션이 내부에서 추가로 사용되면, 트랜잭션 매니저를 통해 논리 트랜잭션을 관리하고, 모든 논리 트랜재션이 커밋되면 물리 트랜잭션이 커밋된다고 이해하면 된다.
* 내부 롤백 정리 *
- 논리 트랜잭션이 하나라도 롤백되면 물리 트랜잭션은 롤백된다
- 내부 논리 트랜잭션이 롤백되면 롤백 전용 마크를 표시한다
- 외부 트랜잭션을 커밋할 때 롤백 전용 마크를 확인한다. 롤백 전용 마크가 표시되어 있으면 물리 트랜잭션을 롤백하고,
`UnexpectedRollbackException` 예외를 던진다.
* 참고 *
- 애플리케이션 개발에서 중요한 원칙은 모호함을 제거하는 것이다. 개발은 명확해야 한다
- 커밋을 호출했는데 내부에서 롤백이 발생한 경우 모호하게 두면 아주 심각한 문제가 발생한다.
- 기대한 결과가 다른 경우 예외를 발생시켜서 명확하게 문제를 알려주는 것이 좋은 설계이다.
* REQUIRES_NEW *
- 물리 트랜잭션을 분리하려면 내부 트랜잭션을 시작할 때 `REQUIRES_NEW` 옵션을 사용하면 된다
- 외부 트랜잭션과 내부 트랜잭션이 각각 별도의 물리 트랜재션을 가진다.
- 별도의 물리 트랜잭션을 가진다는 뜻은 DB 커넥션을 따로 사용한다는 뜻
- 이 경우 내부 트랜잭션이 롤백되면서 로직2가 롤백되어도 로직1에서 저장한 데이터에는 영향을 주지 않는다.
- 최종적으로 로직2가 롤백되고, 로직1은 커밋된다.
* 정리 *
- `REQUIRES_NEW` 옵션을 사용하면 물리 트랜잭션이 명확하게 분리된다.
- `REQUIRES_NEW` 를 사용하면 데이터베이스 커넥션이 동시에 2개가 사용된다는 점을 주의
다양한 전파 옵션
- 스프링은 다양한 트랜잭션 전파 옵션을 제공한다.
- 전파 옵션에 별도의 설정을 하지 않으면 `REQUIRED`가 기본으로 사용된다.
- 참고로 실무에서는 대부분 `REQUIRED` 옵션을 사용한다. 그리고 아주 가끔 `REQUIRED_NEW`을 사용하고, 나머지는 거의 사용하지 않는다.
* REQUIRED *
- 가장 많이 사용하는 기본 설정이다. 기존 트랜잭션이 없으면 생성하고, 있으면 참여한다
- 트랜잭션이 필수라는 의미로 이해하면 된다. ( 필수이기 때문에 없으면 만들고, 있으면 참여한다. )
ㆍ기존 트랜잭션 없음 : 새로운 트랜잭션을 생성한다.
ㆍ기존 트랜잭션 있음 : 기존 트랜잭션에 참여한다.
* REQUIRES_NEW *
- 항상 새로운 트랜잭션을 생성한다
ㆍ기존 트랜잭션 없음 : 새로운 트랜잭션을 생성한다.
ㆍ기존 트랜잭션 있음 : 새로운 트랜잭션을 생성한다.
* SUPPORT *
- 트랜잭션을 지원한다는 뜻이다. 기존 트랜잭션이 없으면, 없는대로 진행하고, 있으면 참여한다.
ㆍ기존 트랜잭션 없음 : 트랜잭션 없이 진행한다.
ㆍ기존 트랜잭션 있음 : 기존 트랜잭션에 참여한다.
* NOT_SUPPORT *
- 트랜잭션을 지원하지 않는다는 의미이다.
ㆍ기존 트랜잭션 없음 : 트랜재션 없이 진행한다.
ㆍ기존 트랜잭션 있음 : 트랜잭션 없이 진행한다. ( 기존 트랜잭션은 보류한다 )
* MANDATORY *
- 의무사항이다. 트랜잭션이 반드시 있어야 한다. 기존 트랜잭션이 없으면 예외가 발생한다
ㆍ기존 트랜잭션 없음 : `IllegalTransaciotnStateException` 예외 발생
ㆍ기존 트랜잭션 있음 : 기존 트랜잭션에 참여한다
* NEVER *
- 트랜잭션을 사용하지 않는다는 의미이다. 기존 트랜잭션이 있으면 예외가 발생한다.
- 기존 트랜잭션도 허용하지 않는 강한 부정의 의미로 이해하면 된다
ㆍ기존 트랜잭션 없음 : 트랜잭션 없이 진행한다
ㆍ기존 트랜잭션 있음 : `IllegalTransaciotnStateException` 예외 발생
* NESTED *
ㆍ기존 트랜잭션 없음 : 새로운 트랜잭션을 생성한다
ㆍ기존 트랜재션 있음 : 중첩 트랜재션을 만든다.
- 중첩 트랜잭션은 외부 트랜잭션의 영향을 받지만, 중첩 트랜재션은 외부에 영향을 주지 않는다
- 중첩 트랜잭션은 롤백 되어도 외부 트랜잭션은 커밋할 수 있다
- 외부 트랜잭션이 롤백 되면 중첩 트랜잭션도 함께 롤백된다
- 참고
ㆍJDBC savepoint 기능을 사용한다. DB드라이버에서 해당 기능을 지원하는지 확인이 필요하다
ㆍ중첩 트랜잭션은 JPA에서는 사용할 수 없다.
* 트랜잭션 전파와 옵션 *
- `isolation`, `timeout`, `readOnly`는 트랜잭션이 처음 시작될 때만 적용된다. 트랜잭션에 참여하는 경우 적용되지 않는다.
- 예를 들어서 `REQUIRED`를 통한 트랜잭션 시작, `REQUIRED_NEW`를 통한 트랜잭션 시작 시점에만 적용
* @Transactional과 REQUIRED *
- 트랜잭션 전파의 기본 값은 `REQUIRED`이다.
ㆍ@Transactional(propagation = Propagation.REQUIRED)
ㆍ@Transactional
- `REQUIRED` 는 기존의 트랜잭션이 없으면 새로운 트랜잭션을 만들고, 기존의 트랜잭션이 있으면 참여
* 정리 *
- 논리 트랜잭션 중 하나라도 롤백되면 전체 트랜잭션은 롤백된다.
- 내부 트랜잭션이 롤백되었는데, 외부 트랜잭션이 커밋되면 `UnexpectedRollbackException` 예외가 발생한다
- `rollbackOnly` 상황에서 커밋이 발생하면 `UnexpectedRollbackException` 예외가 발생한다.
* `REQUIRES_NEW` 주의 *
- `REQUIRES_NEW` 를 사용하면 하나의 HTTP 요청에 동시에 2개의 데이터베이스 커넥션을 사용하게 된다. 따라서 성능이 중요한 곳에서는 이런 부분을 주의해서 사용해야 한다
- `REQUIRES_NEW` 를 사용하지 안호 문제를 해결할 수 있는 단순한 방법이 있다면, 그 방법을 선택하는 것이 더 좋다.
'Spring-Boot > 스프링 DB 2편 - 데이터 접근 기술' 카테고리의 다른 글
스프링 DB 데이터접근 기술 - 스프링 트랜잭션 이해 (0) 2022.09.23 스프링 DB 데이터접근 기술 - 활용 방안 (0) 2022.09.23 스프링 DB 데이터접근 기술 - Querydsl (0) 2022.09.23 스프링 DB 데이터접근 기술 - 스프링데이터 JPA (0) 2022.09.22 스프링 DB 데이터접근 기술 - JPA (0) 2022.09.22