트랜잭션 간단정리
서론
스프링을 사용하는 자바 개발자가 알아야 할 트랜잭션에 대한 간단한 개념과 주의사항에 대해 포스팅합니다.
1. 트랜잭션이란 무엇인가?
- 트랜잭션이란 데이터베이스의 상태를 변화시키기 위해 수행하는 작업의 단위입니다. 여러 쿼리문이 있더라도 commit이나 rollback이 수행되어야지만 트랜잭션이 종료되었다고 볼 수 있습니다.
- 주로 자바 프로젝트에서는 DB에 엑세스하는 비즈니스 로직에 트랜잭션에 대한 처리를 하거나 혹은 AOP를 통해서 공통 트랜잭션 처리를 할 것입니다.
- 트랜잭션 처리가 중요한 이유는 DB에 데이터를 엑세스하는 도중 어떠한 문제가 발생했을 경우(애플리케이션 서버 문제, 네트워크, DB제약조건 등) 비즈니스 로직과 DB의 상태를 안전하게 보장하기 위해서입니다. 물론 그 외의 다른 이유들도 있지만 주된 이유는 안정성유지 및 정상상태로의 회복을 위해서입니다.
2. 트랜잭션의 특징
트랜잭션의 특징에는 흔히들 알고 있는 ACID 네가지 특징이 있습니다.
- A (Atomicity, 원자성) - 트랜잭션 안의 작업들은 모두 성공하거나 모두 실패해야 합니다. All or Nothing.
- C (Consistency, 일관성) - 모든 트랜잭션은 일관성 있는 DB 상태를 유지해야 합니다.(DB의 무결성 제약조건을 항상 만족해야함)
- I (Isolation, 격리성) - 한 트랜잭션이 작업 도중 다른 트랜잭션으로 인해 영향받지 않아야 한다. 성능에 가장 중요한 요소인 격리성은 네가지 특성중 가장 유연하게 설정 가능합니다. 동시성 이슈에 따라 격리성 레벨을 조절해야 합니다.
- D (Durability, 지속성) - 트랜잭션이 성공적으로 끝나든, 실패하든 그 결과는 DB에 영원히 지속되어야 합니다. (로그)
3. 스프링에서의 트랜잭션
개발자가 비즈니스 로직에만 집중할 수 있도록 스프링은 크게 2가지 트랜잭션 기술을 지원해줍니다.
- 선언적방식
- 프로그래밍 방식
3-1. 선언적 트랜잭션
바로 @Transactional 어노테이션을 이용한 트랜잭션 방식입니다. 선언적방식은 Spring AOP를 이용한 방식입니다.
- 선언적방식은 유지보수가 쉽습니다.
- 다수의 트랜잭션을 관리해야할 때 선호하는 방식입니다.
- 프록시 방식이므로, 내부 메서드(private)에서는 사용이 불가능합니다.
- 클래스, 메서드 둘 다 어노테이션을 달 수 있겠지만, 더 좁은 범위인 메서드에 다는 것이 유리합니다.
3-2. 프로그래밍 트랜잭션
TransactionTemplate을 구현한PlatformTransactionManager구현체를 사용합니다.- 해당 구현체에는 getTransaction(), commit(), rollback() 세 가지 메소드가 있습니다.
- 데이터에 엑세스하기 위해서는 JPA 뿐만 아니라 R2DBC, JdbcTemplate 등 여러가지 방법이 존재하고, 각각 트랜잭션 관리 방법이 다르기 때문에 스프링에서는 트랜잭션 관리를 다양하게 하기 위한 이와 같은 추상화 개념을 지원합니다.
4. 트랜잭션 경쟁
트랜잭션의 특징 중 하나인 격리레벨에 따라 트랜잭션 경쟁 문제는 복잡해집니다.
참고로 흔히들 사용하는 mysql의 기본 격리 레벨은 REPEATABLE READ(Level 2) 입니다.
데이터베이스의 격리레벨은 다음과 같습니다.
-
READ UNCOMMITED : 트랜잭션 실행 도중 커밋되지 않은 데이터를 읽을 수 있습니다.
더티 리드(Dirty Read) : 다른 트랜잭션이 아직 커밋하지 않은 데이터를 읽을 수 있습니다.
-
READ COMMITED : 트랜잭션 실행 도중 읽는 데이터들은 모두 커밋된 데이터들입니다.
UNREPEATABLE READ : 다른 트랜잭션에 의해 커밋된 데이터를 읽게 되어 한 트랜잭션 내에서 SELECT를 두번 했을 경우 다른 값이 나올 수 있습니다.
-
REPEATABLE READ : 트랜잭션 실행 후 읽는 모든 데이터는 같은 값을 보장합니다. (mysql Default level)
트랜잭션 실행동안 읽는 모든 데이터에 Shared Lock을 걸어서 읽기는 가능하지만 수정은 불가능하게 만듭니다.
팬텀 리드(PHANTOM READ) : 일정 범위의 레코드를 읽었을 때 다른 결과값이 나올 수 있습니다. (트랜잭션이 끝나지 않았는데도 원래는 없던 유령값이 존재할 수 있음.)
-
SERIALIZABLE LEVEL : 트랜잭션 종료시까지 모든 데이터에 락을 걸어 최고 수준의 정합성을 보장합니다.
팬텀리드 방지.
당연히 SERIALIZABLE LEVEL을 사용해야 할 것 같지만, 성능문제로 주로 READ COMMITED, REPEATABLE READ 격리레벨을 주로 사용합니다. 4번으로 갈수록 데이터 정합성은 높지만, 성능이 떨어집니다.
5. 트랜잭션 주의할 점
-
트랜잭션은 가능한 한 짧아야 합니다. -> Locking 시간을 짧게 가져가자.
- 트랜잭션이 길어지게 되는 상황에서는 다른 트랜잭션에 의해 데드락 혹은 점유시간이 길어지게 되어 네트워크 지연 문제가 발생할 수 있습니다.
- 트랜잭션이 걸린 로직안에 DB 엑세스 로직 뿐만 아니라 예를 들어, S3 업로드 라던지 이미지 변환 같은 작업이 들어가게되면 트랜잭션 시간이 길어지게 됩니다. 이는 예기치 못한 장애로 이어질 가능성이 높습니다.
- 트랜잭션이 길어지게 되는 상황에서는 다른 트랜잭션에 의해 데드락 혹은 점유시간이 길어지게 되어 네트워크 지연 문제가 발생할 수 있습니다.
-
CheckedException이 발생하는 경우에는 트랜잭션이 롤백되지 않습니다. UnchckedException은 예상치 못한 오류라 롤백시키는 것이 Default 전략입니다.(~라고 우아한 형제들에서 누군가가~)
-
동일한 클래스 내에서
@Transactional이 붙지 않은 상위 메소드가@Transactional이 붙어 있는 하위 메소드를 호출할 경우 트랜잭션은 동작하지 않습니다. 1. 이 경우처럼 트랜잭션 관리가 어려운 이유는 , 트랜잭션이 동작하지 않는 오류를 프로그램 실행 전에는 알 수 없다는 점 때문입니다. 2. 스무스하게 프로그램은 실행되겠지만, 오류가 발생했을 경우 롤백이 되지 않는다던가 예기치 못한 데드락 상황을 겪게 될 수 있습니다.
-
테스트코드에서
@Transactional은 무조건 Rollback 만 일어난다는 점.
-
@Transaction에서 발생한 예외라고 해서 무조건 롤백은 아닙니다.
rollback 예외 옵션을 통해 무시(?)하고 커밋을 할 수 도 있긴 합니다.
참고사이트 :
https://yeonyeon.tistory.com/223
https://goddaehee.tistory.com/167
https://joojimin.tistory.com/68
