-
Transaction에 대해서 알아보자One Cookie a day 2023. 10. 27. 05:05
📑 Transaction 이란 무엇일까
이 질문에 대해서, 데이터베이스의 상태를 변화시키기 위해 수행하는 작업 단위라고만 알고 있었는데, 덧붙여 말하자면 나눌 수 있는 최소한의 작업단위를 의미한다.
예를 들어 '이체'를 생각해보자.
- 송금하려는 계좌가 정상 계좌인지 확인한다.
- 내 계좌에 이체하려는 금액 이상이 남아있는지 확인한다.
- 해당 계좌에 이체 요청을 보낸다.
- 요청 완료 응답을 받고 이체가 끝난다.
위에 적힌 작업들이 [ 이체 ]라는 일을 하기 위해 필요한 최소한의 작업들이다.
그런데, [ 이체 ] 라는 작업 (트랜잭션) 내에서도 계좌 확인 / 보내려는 계좌가 타 은행일 경우에 해당 시스템에 확인 및 송금 요청 이런식으로 내부에 트랜잭션이 나누어져 있다.
📑 Transaction Propagation
- 트랜잭션의 경계는 하나의 커넥션을 통해 진행되므로, 하나의 커넥션이 생성되고 반납되는 범위 안에 존재한다.
- 위의 예시처럼, 한 가지의 서비스에 트랜잭션이 2가지 이상 있을 경우 어떻게 동작하고 처리할 것인가?
- 트랜잭션이 이미 있는데, 추가적인 트랜잭션이 발생한다면, 새로 만들지, 기존 트랜잭션에 끼울지, 예외를 던질지 결정해야 한다.
- 이것이 트랜잭션 전파 속성이다.
물리 트랜잭션 vs 논리 트랜잭션
물리 트랜잭션 : 실제 데이터베이스에 적용되는 트랜잭션, connection을 통해 commit / roll back하는 단위
논리 트랜잭션 : 스프링이 트랜잭션 매니저를 통해 트랜잭션을 처리하는 단위
모든 논리 트랜잭션이 커밋되어야 물리 트랜잭션이 커밋되며, 하나라도 논리 트랜잭션이 롤백되면 물리 트랜잭션도 롤백된다.
📑 PROPAGATION_REQUIRED
특징
- 아직 트랜잭션이 존재하지 않는다면 현재 범위에서 새로 생성하거나, 혹은 더 큰 범위에 존재하는 외부의 트랜잭션에 참여
- 즉, 물리적인 트랜잭션을 강제한다.
- 스프링의 기본적인 전파 속성이다.
주의사항
- 외부 트랜잭션에 참여하는 경우 로컬의 격리 수준, time out value, read only flag등을 자동으로 무시하고 외부 범위 특성에 join
- 그렇기 때문에 격리 수준 다른 트랜잭션에 참여할 때 이렇게 덮어씌워지는 것을 막으려면 validateExistingTransactions flag를 true로 바꾸는것을 고려해볼 것 (이 작업은 내부의 read-write 트랜잭션이 외부의 read-only 트랜잭션에 참여하는 시도도 막아줄 수 있다)
- 기본적으로 이 범위에 있는 트랜잭션들은 하나의 동일한 물리적 트랜잭션으로 매핑된다.
- 내부 트랜잭션 범위에서 rollback only를 세팅해두거나(default=false이다)
- isGlobalRollbackOnParticipationFailure = true(DEFAULT)로 인해 외부 트랜잭션에도 영향을 미친다.
- inner scope에서 exception 발생해도 outer transaction은 진행해버릴 수 있기 때문에 이를 방지하기 위해 UnexpectedRollbackException을 발생시켜 내부에서 rollback이 발생하였다고 명확히 알려준다
PROPAGATION_REQUIRES_NEW
📑 PROPAGATION_REQUIRES_NEW
특징- 앞서 언급한 PROPAGATIOM_REQUIRED와 달리, 항상 독립된 물리적 트랜잭션을 사용하며, 외부 범위에 있는 이미 존재하는 트랜잭션에 참여하지 않는다.
- 다른 물리적 트랜잭션에 존재하여 독립적으로 commit / roll back이 가능하다.
- 또한 inner 트랜잭션은 자신만의 isolation level, timeout, read-only 등의 옵션을 선언할 수 있으며 outer 트랜잭션의 옵션을 상속받지 않는다.
주의사항- 외부 트랜잭션의 자원들은 내부 트랜잭션이 데이터베이스 커넥션처럼, 자신의 자원을 획득하는 동안 유지되어 있다.
- 이는 커넥션 풀의 고갈로 이어질 수 있고, 잠재적으로 데드락으로 이어질 가능성이 있다.( Ex : 여러 스레드들이 자신의 외부 트랜잭션을 활성화시키고, 내부 트랜잭션들이 그들의 새로운 연결을 획득하기를 기다린다면)
- 공식 문서에 따르면, 커넥션 풀 사이즈가 이러한 상황에서도 적절히 유지되도록, 동시 스레드들 수 + 1 넘게 남아있는 것 아닌 이상 이 옵션을 사용하지 말 것을 권고하고 있다.
📑 PROPAGATION_NESTED
특징
- 하나의 물리적 트랜잭션 내부에 롤백 가능한 여러 세이브 포인트들을 사용하여 트랜잭션을 중첩(내부에 또 다른 트랜잭션)
- 내부 트랜잭션들이 자신의 범위에서 롤백을 발생시키고 일부 작업들이 롤백되어도, 외부 트랜잭션은 물리적 트랜잭션을 계속 진행할 수 있다.
주의사항
- 추가로 이 세팅은 JDBC 자원 트랜잭션들에만 동작한다. (JDBC savePoints에 매핑되므로)
- This setting is typically mapped onto JDBC savepoints, so it works only with JDBC resource transactions.
📑 SUPPORTS
현재 트랜잭션이 존재한다면 거기에 참여하고, 존재하지 않는다면 non-transactional 하게 수행
주의사항
- 트랜잭션 동기화를 사용하는 transaction manager의 경우, 아무 트랜잭션이 없는(non_transactional) 것과는 약간 다르다.
- 동기화가 적용될 트랜잭션의 범위를 정의하기 때문이다.
- 그 결과 같은 자원(jdbc connection, hibernate session 등)이 지정한 전체 스코프에 공유되며, 트랜잭션 매니저의 동기화 구성에 따라 달라질 수 있다.
📑MANDATORY
트랜잭션의 존재 여부를 강제하여 이미 시작된 트랜잭션이 있다면 해당 트랜잭션에 참여하되, 존재하는 트랜잭션이 없을 경우 exception 발생시킨다.
📑PROPAGTION_NOT_SUPPORTED
non_transactional하게 동작하고 현재 트랜잭션은 보류시킨다.
대신, 이 보류 작업은 JtaTransactionManager (javax.transaction.TransactionManager)에서만 특정하게 적용되며 모든 트랜잭션 매니저에 적용되는 것은 아니다.
📑PROPAGTION_NEVER
non-transactional하게 동작할 것을 강제하며, 트랜잭션이 존재한다면 exception을 발생시킨다.
Transaction Isolation Level
READ UNCOMMITED
- 다른 트랜잭션의 커밋되지 않은 변경 내역들까지 볼 수 있다.
- Dirty Read
- Transaction A가 아직 커밋 완료되지 않은 Transaction B가 만든 변화들을 읽을 수 있다.
READ COMMITTED
- 트랜잭션 시작 전 커밋 완료 된 트랜잭션만 읽을 수 있다.
- Unrepeatable Read
- 트랜잭션 A가 데이터를 읽었을 때, 트랜잭션 B가 데이터를 변화시킨다.
- 트랜잭션 A가 다시 데이터를 읽었을 때, 앞서 읽은 데이터와 다른 경우가 발생한다.
- 동일 쿼리에 대한 결과가 다르게 나올 수 있다.
REPEATABLE READ
- 반복해서 read 수행해도 읽은 값이 변화하지 않는다.
- 모든 read마다 트랜잭션 시작한 해당 시점 기준의 스냅숏을 읽는다.
- Phantom Read
- 트랜잭션 A가 쿼리를 수행할 때, 트랜잭션 B가 새로운 행을 삽입 또는 삭제하고 이 작업을 커밋한다.
- 다시 같은 쿼리를 트랜잭션 A가 수행하면, 새로운 행이 생겨나거나 삭제되는 경우가 발생한다.
SERIALIZABLE
- 모든 트랜잭션들이 하나의 트랜잭션 내에서 처리되는 것과 같은 높은 고립 수준을 제공한다
- 그 대신, 동시성 측면에서 성능이 좋지 않다.
- 다른 트랜잭션이 접근 시, 에러를 발생시키며 실패한 작업에 대한 처리는(재시도 등) 어플리케이션 단에서 확인 후 처리되어야 한다.
아래 이미지는 오라클 문서에서 정의한 격리 수준에 따라 발생하는 문제들의 가능성 여부이다.
참고 글
더보기'One Cookie a day' 카테고리의 다른 글
Enum을 비교할 때에는 ==과 equals()중 어떤 것을 써야 할까? (0) 2024.02.06 싱글톤 디자인 패턴에 대해 (0) 2023.10.16 Java 7 이상에서 ArrayList 크기 계산 시 비트 연산자를 활용하는 이유 (0) 2023.10.10 ArrayList는 어떻게 동적으로 크기를 늘릴 수 있을까? (0) 2023.10.09 Java 5부터 등장한 Generic은 하위 버전에 어떻게 호환이 되는걸까? (0) 2023.09.30