우리의 일상 생활에서 트랜잭션을 가장 쉽게 이해할 수 있는 상황은 바로 계좌 입출금 상황이 있다. 계좌 입출금의 과정을 살펴보면 다음과 같다.
1. A가 B에게 10000원을 송금
2. A의 계좌에 10000원을 차감
3. B의 계좌에 10000원을 증가
만약 앞선 입출금 과정에서 10000원을 차감하는 단계 혹은 10000원을 증가하는 단계에서 오류가 발생하여 부분 업데이트 현상이 발생한다면 이 세상에 존재하던 10000원이 사라지거나 이 세상에 존재하지 않던 10000원이 발생하는 문제가 발생할 것이다. 따라서 논리적인 작업 셋을 하나의 단위로 묶어서 처리하는 것에 대한 필요성이 발생하였고 해결 방법으로 트랜잭션이 개발되었다.
트랜잭션은 작업의 완전성을 보장해 주는 것이다. 즉 논리적인 작업 셋을 모두 완벽하게 처리하거나 처리하지 못할 경우에는 원 상태로 복구해서 작업의 일부만 적용되는 현상(Partial update
)이 발생하지 않게 만들어주는 기능이다.
트랜잭션은 꼭 여러개의 변경 작업을 수행하는 쿼리가 조합됐을 때만 의미 있는 개념은 아니다. 트랜잭션은 하나의 논리적인 작업 셋에 하나의 쿼리가 있든 두 개 이상의 쿼리가 있든 관계없이 논리적인 작업 셋 자체가 100% 적용되거나(Commit을 실행했을 때) 아무것도 적용되지 않아야(ROLLBACK 또는 트랜잭션을 ROLLBACK 시키는 오류가 발생했을 때)함을 보장해 주는 것이다.
Commit
), 아니면 전혀 반영되지 않아야 한다(Rollback
).데이터베이스에 작업이 들어왔을 때 모든 작업의 독립성을 보장해 하나씩 순차적으로 진행하게 된다면 CPU는 DBMS보다 인풋 아웃풋 작업을 빈번히 수행하기 때문에 트랜잭션을 순차적으로 수행하면 CPU는 응답을 기다리는 시간이 길어져 프로그램이 비효율적으로 동작하는 문제가 발생할 수 있다. 이처럼 데이터베이스에 저장된 데이터의 무결성과 동시성의 성능을 지키기 위해 트랜잭션의 설정이 중요하다.
데이터베이스에서는 각각의 명령을 하나의 트랜잭션으로 보고 보장해주기 때문에 여려 명령을 하나의 트랜잭션으로 묶고 싶은 경우 개발자가 직접 트랜잭션의 경계설정을 통해 트랜잭션을 명시하는 일이 필요하다.
@Transactional(readOnly = true)
public class Service {
@Transactional
public void save(Entity e) {
...
}
}
트랜잭션 어노테이션은 메소드
, 클래스
, 인터페이스
등에 적용할 수 있다. 클래스 상단에 적용된 어노테이션에 대해서는 해당 클래스에 존재하는 모든 메서드에 어노테이션이 적용된다.
중첩되어 존재하는 경우에는 클래스 메서드, 클래스, 인터페이스 메서드 인터페이스 순으로 우선순위(priority
)를 갖고 적용된다.
어노테이션이 적용된 메서드는 메서드 시작부터 트랜잭션이 시작되고 메서드를 성공적으로 끝마치면 트랜잭션 커밋, 도중에 문제가 발생하면 롤백하는 과정이 진행된다. 어노테이션은 데이터베이스에 여러번 접근하면서 하나의 작업을 수행하는 서비스 계층 메서드에 붙이는 것이 일반적이다.
REQUIRED
: 디폴트 설정값. 트랜잭션 내부에 트랜잭션이 존재할 경우 두 개의 트랜잭션이 모두 성공할 경우에만 커밋을 수행하고 둘 중 하나라도 실패할 경우 두 개의 트랜잭션이 모두 롤백된다.SUPPORTS
: 기존의 트랜잭션이 존재하는 경우 해당 트랜잭션에 참여하고 존재하지 않을 경우 트랜잭션 없이 메서드가 실행된다.MANDATORY
: 기존의 트랜잭션이 존재하는 경우 해당 트랜잭션에 참여하고 존재하지 않을 경우 IllegalTransactionStateException
예외가 발생합니다. 즉, 기존의 트랜잭션이 존재하지 않는다면 실행될 수 없고, 혼자서도 메서드를 실행할 수 없다.REQUIRES_NEW
: 기존의 트랜잭션이 존재해도 항상 새로운 트랜잭션을 시작한다. 기존의 트랜잭션이 존재하는 경우 기존 트랜잭션을 잠시 멈추고 새로운 트랜잭션이 독립적으로 실행된다.NOT_SUPPORTED
: 기존의 트랜잭션이 존재한다면 보류하고 트랜잭션을 사용하지 않고 자신의 메소드를 실행하는 방식이다.NEVER
: 트랜잭션을 사용하지 않도록 강제한다. 기존의 트랜잭션이 존재한다면 IllegalTransactionStateException
****예외가 발생한다.NESTED
: 기존의 트랜잭션이 존재한다면 그 안에 새로운 자식 트랜잭션을 만드는 설정이다. 만약 기존(부모) 트랜잭션에 문제가 발생한다면 새로운(자식) 트랜잭션은 롤백 처리되지만, 새로운(자식) 트랜잭션에 문제가 발생해도 기존(부모) 트랜잭션에는 롤백이 발생하지 않는다.READ_UNCOMMITTED
: 가장 낮은 격리수준으로 아직 커밋되지 않은 데이터를 다른 트랜잭션이 읽을 수 있는 설정이다. 해당 설정은 동시성 문제가 발생할 수 있다는 단점이 있다.READ_COMMITTED
: 가장 많이 사용되는 두 번째로 낮은 격리 수준으로 커밋되지 않은 정보는 읽을 수 없다. READ_UNCOMMITTED
의 문제를 해결하기 위해 존재하지만 3개의 트랜잭션이 동시에 발생했을 때 마찬가지로 동시성 문제가 발생할 수 있다는 단점이 있다. 대부분의 DB는 READ_COMMITTED
를 기본 격리수준으로 사용한다.REPEATABLE_READ
: 트랜잭션이 읽은 로우를 다른 트랜잭션이 수정할 수 없게 한다. 하지만 새로운 로우를 추가하는 것은 제한하지 않기 때문에 다시 조회했을 때 발견하지 못한 새로운 로우가 발생할 수 있다는 문제가 있다.SERIALIZABLE
: 동시에 같은 테이블의 정보를 접근할 수 없다. 하지만 트랜잭션을 동시에 사용하지 못하고 순차적으로 수행하는 것과 다를 바 없어서 성능이 매우 떨어지니 극단적인 상황에만 사용하도록 해야한다.IllegalTransactionStateException
예외가 발생하며 롤백된다.true
로 설정할 경우 트랜잭션 작업 안에서 update
, insert
, delete
작업이 수행되는 것을 방지한다.flush
모드가 manual
로 설정되어 사용자가 수동으로 flush
를 수행해야 하기 때문에 JPA의 더티체킹 기능을 무시할 수 있기 때문에 성능향상에 도움이 된다.RuntimeException
과 Error
가 발생했을 때만 롤백을 수행한다. 따라서 이외에 Exception을 롤백 대상으로 삼고 싶다면 특정 Exception을 클래스로 전달해 사용할 수 있다.RuntimeException
과 Error
가 발생하여도 커밋을 수행할 수 있도록 하는 설정이다. IOException
을 설정값으로 두면 롤백 대상인 IOException
이 발생해도 롤백하지 않고 커밋을 진행한다.부분 업데이트 현상이 발생하면 실패한 쿼리로 인해 남은 레코드를 다시 삭제하는 재처리 작업이 필요할 수 있다. 실행하는 쿼리가 하나뿐이라면 재처리 작업은 간단할 것이다. 하지만 2개 이상의 쿼리가 실행되는 경우라면 실패에 대한 재처리 작업은 다음 예제와 같이 상당한 고민거리가 될 것이다. 만약 트랜잭션을 적용하지 않은 부분 업데이트 현상이 발생한다면 부분 업데이트 결과로 쓰레기 데이터가 테이블에 남아 있을 가능성이 있다.
트랜잭션은 꼭 필요한 최소의 코드에만 적용하는 것이 좋다. 이는 프로그램 코드에서 트랜잭션의 범위를 최소화하라는 의미다. 다음은 트랜잭션 처리시 주의해야할 리스트다.