[스프링] 트랜잭션 관리
트랜잭션 관리
트랜잭션 관리의 목적과 관리 방법은 다음과 같다.
- 목적: 데이터의 일관성과 무결성 보장
- 관리 방법 두 가지
- 프로그램 방식 트랜잭션 관리
- 선언적 트랜잭션 관리
트랜잭션 관리를 직접 프로그래밍하는 방식에서는 비즈니스 로직을 수행하는 메서드 내에 트랜잭션 관리 코드(트랜잭션 경계 설정)를 삽입하여 커밋과 롤백을 제어한다. AOP 관점에서 트랜잭션 관리는 일종의 공통 관심사(common aspect)이다. 선언적 트랜잭션 관리는 비즈니스 로직과 트랜잭션 관리 로직을 분리한다.
일련의 작업(쿼리 실행)을 수행하는 메서드 내에서 여러 쿼리 실행을 하나의 트랜잭션으로 실행할지 결정하여 쿼리 실행이 하나라도 실패하면 전체 쿼리 실행이 실패하도록 트랜잭션을 롤백하고 쿼리 실행이 모두 성공하면 트랜잭션을 커밋하도록 한다.
프로그램 방식 트랜잭션 관리
일련의 작업을 수행하는 메서드 내에서 작업이 모두 성공적으로 종료되면 commit()
메서드 실행을 실행하여 트랜잭션을 커밋하고 작업이 하나라도 실패한 경우 rollback()
메서드를 실행하여 트랜잭션을 롤백한다.
JDBC는 기본적으로 하나의 쿼리 실행이 성공하면 곧 바로 트랜잭션을 커밋하므로 원하는 작업에 대해 트랜잭션 관리를 하기 위해 자동 커밋 기능을 설정 해제해야 한다. Connection
객체의 setAutoCommit(false)
메서드를 호출하여 쿼리 실행 완료 시 트랜잭션을 자동으로 커밋하지 않도록 설정할 수 있다.
하나의 데이터 소스에 대해서는 commit()
메서드와 rollback()
메서드로 트랜잭션 관리가 쉽게 가능하지만 여러 데이터 소스에 대한 트랜잭션을 관리하거나 자바 EE 애플리케이션 서버의 내장 트랜잭션 관리 기능을 사용하기 위해서는 JTA(Java Transaction API) 사용이 필요하며 ORM 프레임워크마다 서로 다른 JTA를 사용해야 할 수도 있다.
프로그램 방식의 트랜잭션 관리는 트랜잭션 관리 코드가 특정 데이터 접근 기술에 종속되므로 기술을 변경하려면 관련된 코드를 모두 변경해야 하는 의존성 문제가 있다. 스프링은 데이터 접근 기술에 독립적으로 간편하게 트랜잭션 관리 작업을 할 수 있도록 트랜잭션 관리자 인터페이스(PlatformTransactionManager
), 트랜잭션 템플릿 인터페이스(TransactionTemplate
), 트랜잭션 선언 기능 등 특정 기술에 의존하지 않는 유용한 트랜잭션 관리 기능을 지원한다.
트랜잭션 관리자는 새로운 트랜잭션의 시작, 트랜잭션 커밋 및 롤백을 통해 트랜잭션을 관리하는 역할을 수행한다. 트랜잭션 템플릿은 트랜잭션 관리자를 통해 트랜잭션을 직접 시작, 커밋, 롤백하는 코드를 반복적으로 작성할 필요 없이 트랜잭션을 적용할 코드 블록을 인자로 전달하여 호출만 하면 트랜잭션을 자동으로 관리해주는 메서드를 제공함으로써 트랜잭션 관리를 보다 간편하게 할 수 있게 도와준다. 트랜잭션 템플릿은 또한 트랜잭션을 적용할 코드의 실행 도중 언체크 예외가 발생하거나 코드 블록 내에서 TransactionStatus
의 setRollbackOnly()
메서드를 명시적으로 호출하면 트랜잭션를 롤백한다.
스프링이 제공하는, 트랜잭션 관리 기능의 추상화를 위한 인터페이스를 사용하면 트랜잭션 관리를 편리하게 할 수 있고 트랜잭션 관리 코드를 특정 기술에 독립적으로 작성할 수 있다.
스프링 트랜잭션 관리 인터페이스 (PlatformTransactionManager)
PlatformTransactionManager
은 스프링 트랜잭션 관리자를 포괄한 인터페이스이다. 데이터 접근 기술에 독립적인 트랜잭션 관리 기능을 캡슐화하여 트랜잭션 관리를 추상화하는 핵심 인터페이스이다. PlatformTransactionManager
인터페이스는 TransactionDefinition
인터페이스와 TransactionStatus
인터페이스를 사용해 트랜잭션을 생성하고 관리한다. PlatformTransactionManager
의 setDataSource()
메서드를 사용하여 트랜잭션을 관리할 데이터 소스를 지정할 수 있다.
스프링은 여러 데이터베이스 접근 구현 방식에 따라 적용 가능한 PlatformTransactionManager
인터페이스의 다양한 구현체들을 제공한다. JDBC로 하나의 데이터 소스에 접근하는 경우에는 DataSourceTransactionManager
를 사용한다. 자바 EE 애플리케이션 서버에서 JTA로 트랜잭션을 관리할 경우, 서버에서 트랜잭션을 탐색하기 위해 JtaTransactionManager
를 사용한다. 분산 트랜잭션(여러 리소스에 걸친 트랜잭션) 구현 시에도 JtaTransactionManager
를 사용한다. ORM 프레임워크로 데이터베이스에 접근할 경우 HibernateTransactionManager
나 JpaTransactionManager
등의 해당 프레임워크 트랜잭션 관리자를 사용한다.
PlatformTransactionManager
를 사용한 트랜잭션 관리 코드 작성은 다음 순서로 수행한다.
- 일련의 작업(쿼리 실행)을 수행하는 메서드가 정의된 클래스(
JdbcDaoSupport
상속)에PlatformTransactionManager
타입 속성 선언 및 DI를 위한 Setter 메서드 정의 - 새 트랜잭션 시작 전
TransactionDefinition
타입 트랜잭션 정의 객체 초기화DefaultTransactionDefinition
구현체 사용
- 트랜잭션 정의 객체를
PlatformTransactionManager
객체의getTransaction()
메서드의 인자로 전달하고 호출하여 트랜잭션 관리자에게 새 트랜잭션을 시작할 것을 요청transactionManager.getTransaction(transactionDefinition)
- 쿼리가 모두 정상 실행되면
getTransaction()
메서드가 반환하는, 트랜잭션 상태 추적을 위한TransactionStatus
객체(트랜잭션 상태)를 인자로 넘겨 트랜잭션 관리자에게 알림transactionManager.commit(transactionStatus)
- 스프링 JDBC 템플릿에서 발생한 예외는 모두
DataAccessException
하위 타입이므로 해당 예외가 나면 트랜잭션 관리자가 트랜잭션을 롤백하도록 설정transactionManager.rollback(transactionStatus)
TransactionManager
구현체 빈 정의 시 적절한 트랜잭션 관리자 구현체 선택 및 데이터 소스 설정transactionManager.setDataSource(dataSource())
- 트랜잭션 관리자를
PlatformTransactionManager
로 선언하였으므로 빈 구성 클래스에서JdbcDaoSupport
구현 객체 빈 정의 시TransactionManager
구현체 빈을JdbcDaoSupport
상속 클래스의setTransactionManager()
메소드의 인자로 전달JdbcDaoSupport상속객체.setTransactionManager(transactionManager())
setDataSource()
메소드의 인자로 데이터 소스 객체 전달도 필요하다.
트랜잭션 템플릿 (TransactionTemplate)
트랜잭션 관리자를 사용하여 트랜잭션을 직접 관리하는 코드를 구현하는 경우 트랜잭션 관리 코드가 중복된다. 스프링이 제공하는 트랜잭션 템플릿(TransactionTemplate
)을 사용하면 전체 트랜잭션 관리 프로세스 및 예외 처리를 효과적으로 할 수 있다. 트랜잭션 관리자를 통해 트랜잭션을 직접 실행하고 커밋 및 롤백하는 반복적인 코드 작성이 필요하지 않다.
TransactionTemplate
객체 생성을 위해서는 TransactionManager
가 필요하다. 트랜잭션 템플릿은 트랜잭션이 적용될 코드 블록을 캡슐화한 트랜잭션 콜백 객체를 실행한다.
TransactionCallback<T>
인터페이스를 구현한 콜백 클래스에서 코드 블록을 캡슐화한 뒤 TransactionTemplate
의 execute()
메서드에 전달한다. 트랜잭션 실행 결과 반환값이 있는 경우 콜백 객체의 반환값은 템플릿의 execute()
메서드가 반환한다. 트랜잭션 실행 결과 반환값이 필요 없는 경우 TransactionCallback<T>
인터페이스를 구현한 프레임워크 내장 객체인 TransactionCallbackWithoutResult
객체를 전달한다.
콜백 객체 실행이 정상적으로 종료되면 트랜잭션이 커밋되고, 콜백 객체를 실행하다가 언체크 예외(예: RuntimeException
, DataAccessException
)가 발생하거나, 명시적으로 doInTransactionWithoutResult()
메서드의 TransactionStatus
인자의 setRollbackOnly()
메서드를 호출하면 트랜잭션이 롤백된다.
TransactionTemplate
을 사용한 트랜잭션 관리 코드 작성은 다음 순서로 수행한다.
- 일련의 작업(쿼리 실행)을 수행하는 메서드가 정의된 클래스(
JdbcDaoSupport
상속)에PlatformTransactionManager
타입 속성 선언 및 DI를 위한 Setter 메서드 정의 - 새 트랜잭션 시작 전
TransactionTemplate
트랜잭션 템플릿 객체 초기화 TransactionCallback<T>
인터페이스를 구현한 콜백 클래스에서 코드 블록을 캡슐화한 뒤TransactionTemplate
의execute()
메서드의 인자로 전달- 빈 구성 클래스에서
TransactionTemplate
객체 빈 정의 시TransactionManager
빈을TransactionTemplate
의setTransactionManager()
메소드의 인자로 전달transactionTemplate.setTransactionManager(transactionManager())
- 빈 구성 클래스에서
JdbcDaoSupport
구현 객체 빈 정의 시TransactionTemplate
빈을JdbcDaoSupport
상속 클래스의setTransactionManager()
메소드의 인자로 전달JdbcDaoSupport상속객체.setTransactionTemplate(transactionTemplate())
- 트랜잭션 관리자 대신 트랜잭션 템플릿을 주입한다.
setDataSource()
메소드로 데이터 소스 객체도 전달 필요하다.
트랜잭션 템플릿 객체는 스레드-안전한 객체이므로 트랜잭션이 적용된 여러 빈에 걸쳐 사용될 수 있다.
선언적 트랜잭션 관리
트랜잭션을 적용할 메서드 또는 클래스에 @Transactional
을 추가하여 선언적으로 트랜잭션을 관리할 수 있다. 이렇게 관리되는 트렌잭션을 스프링 관리(spring-managed) 또는 컨테이너 관리(container-managed) 관리 트랜잭션이라고도 한다. 메서드에 @Transactional
만 붙이면 트랜잭션이 걸린 메서드로 선언된다. 클래스에 적용하면 해당 클래스의 모든 public
메서드에 트랜잭션이 적용된다. 인터페이스도 클래스 및 메서드 레벨에 @Transactional
을 붙일 순 있지만 클래스 기반 프록시(예: CGLIB 프록시)에서는 제대로 작동하지 않을 수 있으므로 권장하지 않는다.
선언적 트랜잭션 관리는 런타임에 적용되기 때문에 애플리케이션 코드는 트랜잭션 관리에 의존하지 않게 된다. 즉, 트랜잭션은 교차(횡단) 관심사가 된다.
@Transactional
어노테이션 기반 선언전 트랜잭션 관리를 위해 빈 구성 클래스에는 @EnableTransactionManegement
을 붙여 트랜잭션 관리 활성화 설정을 해야 한다. IoC 컨테이너에 선언된 빈들을 찾아 @Transactional
을 붙인 메서드(또는 이 어노테이션을 붙인 클래스의 모든 메서드) 중 public
메서드를 가져와 어드바이스를 적용한다.
어노테이션을 사용한 선언적 트랜잭션 관리의 경우 스프링 AOP가 프록시 기반으로 작동하는 한계(IoC 컨테이너에 선언된 빈의 public
메서드에만 어드바이스를 적용할 수 있다) 때문에 public
메서드에만 적용 가능하다. 따라서 public
메서드 스코프로 트랜잭션 관리가 국한되는 한계를 해결하고 public
외의 메서드나 IoC 컨테이너 외부에서 만든 객체의 메서드에 대해서도 트랜잭션을 관리해야 할 경우에는 AnnotationTransactionAspect
라는 AspectJ 애스팩트를 사용해야 한다.
트랜잭션 속성 설정
트랜잭션은 전파(propagation), 격리 수준(isolation level), 타임아웃, 롤백 규칙, 읽기 전용(read-only) 트랜잭션 여부 등의 속성을 설정할 수 있다. 스프링은 트랜잭션의 모든 관련 설정을 TransactionDefinition
인터페이스로 캡슐화한다. TransactionDefinition
인터페이스는 트랜잭션을 관리하는 인터페이스인 PlatformTransactionManager
에서 사용된다.
PlatformTransactionManager
의 핵심 메서드인 getTransaction()
은 TransactionDefinition
인터페이스를 인자로 전달받고 TransactionStatus
인터페이스를 반환한다. TransactionStatus
인터페이스는 트랜잭션 실행을 제어하기 위해 트랜잭션의 상태 정보를 확인하기 위해 사용된다. 이 인터페이스를 통해 트랜잭션 결과를 설정하고 트랜잭션의 완료 여부, 새 트랜잭션인지의 여부를 확인할 수 있다.
트랜잭션 전파 속성 설정
트랜잭션이 적용된 메서드(작업 메서드)를 다른 메서드(요청 메서드)가 호출하면 트랜잭션 전파가 일어난다. 호출한 메서드 역시 기존 트랜잭션 내에서 실행할지, 아니면 트랜잭션을 하나 더 생성해 자신만의 고유한 트랜잭션에서 실행할지 결정이 필요하다. 기존 트랜잭션을 그대로 적용하는 경우(요청 메서드에서 작업 메서드로 트랜잭션이 전달되는 경우) 작업 메서드에서 쿼리 실행이 실패하면 요청 메서드의 트랜잭션이 롤백된다. 기존 트랜잭션을 적용하지 않고 새로운 트랜잭션을 시작하는 경우 작업 메서드에서 쿼리 실행이 실패해도 요청 메서드의 트랜잭션이 롤백되지 않는다.
트랜잭션 전파 방식은 @Transactional
의 propagation
트랜잭션 속성에 명시한다.
@Transactional(propagation = Propagation.전파속성)
스프링 org.springframework.transaction.TransactionDefinition
인터페이스에 일곱 가지 트랜잭션 전파 방식이 정의되어 있다.
REQUIRED
(기본 전파 방식): 진행 중인 트랜잭션이 있으면 현재 메서드를 그 트랜잭션에서 실행하되, 그렇지 않을 경우 새 트랜잭션을 시작해서 실행한다.REQUIRES_NEW
: 항상 새 트랜잭션을 시작하여 현재 메서드를 실행하고 진행 중인 트랜잭션이 있으면 잠시 중단시킨다.SUPPORTS
: 진행 중인 트랜잭션이 있으면 현재 메서드를 그 트랜잭션 내에서 실행하되, 그렇지 않을 경우 트랜잭션 없이 실행한다.NOT_SUPPORTED
: 트랜잭션 없이 현재 메서드를 실행하고 진행 중인 트랜잭션이 있으면 잠시 중단시킨다.MANDATORY
: 반드시 트랜잭션을 적용하고 현재 메서드를 실행하되 진행 중인 트랜잭션이 없으면 예외를 던진다.NEVER
: 반드시 트랜잭션 없이 현재 메서드를 실행하되 진행 중인 트랜잭션이 있으면 예외를 던진다.NESTED
: 진행 중인 트랜잭션이 있으면 현재 메서드를 이 트랜잭션의 (JDBC 3.0 세이브포인트(savepoint) 기능이 있어야 가능한) 중첩 트랜잭션(nested transaction) 내에서 실행한다.- 진행 중인 트랜잭션이 없으면 새 트랜잭션을 시작해서 실행한다.
- 다른 속성들은 자바 EE 트랜잭션 전달 방식과 유사한 반면 이 방식은 스프링에서만 가능하다.
- 장시간 실행되는 업무를 처리하면서 배치 실행 도중 끊어서 커밋하는 경우 유용하다.
- 중간에 작업이 잘못되어도 중첩 트랜잭션을 롤백하면 끊어서 실행한 분량의 작업만 소실된다.
모든 트랜잭션 관리자 구현체가 이들 전파 방식을 전부 지원하는 건 아니며 하부 리소스에 따라 달라질 수도 있다. 트랜잭션 관리자가 다양한 전파 방식을 지원한다 해도 데이터베이스가 지원하는 격리 수준에 따라 영향을 받을 수밖에 없다.
트랜잭션 격리 속성 설정
서로 다른 트랜잭션이 동일한 데이터에 접근하는 경우 여러 문제가 발생할 수 있으므로 트랜잭션들은 서로 격리되어야 한다. 하나의 트랜잭션에서 일련의 쿼리를 실행하고 있는 도중에 다른 트랜잭션이 데이터를 수정하는 경우 동일한 쿼리 실행 결과가 서로 다르게 나타나는 문제가 발생하게 된다. 이러한 문제가 발생하면 데이터의 일관성(inconsistency)이 낮아진다.
트랜잭션 격리가 일어나지 않는 경우 발생할 수 있는 문제는 크게 네 가지이며 트랜잭션의 격리는 그 수준에 따라 네 가지 문제 중 일부를 막을 수 있게 해준다.
트랜잭션 격리가 일어나지 않는 경우 발생할 수 있는 네 가지 문제는 다음과 같다.
- 오염된 값 읽기 (dirty read): 트랜잭션1이 데이터를 수정하였지만 커밋을 하기 전 상태에서 트랜잭션2가 수정된 데이터를 읽고 작업을 수행하는 도중 트랜잭션1이 롤백되면 트랜잭션2가 읽은 데이터는 일시적인 값으로 더 이상 유효하지 않다.
- 재현 불가능한 읽기 (nonrepeatable ead): 트랜잭션1이 하나의 데이터를 두 번 읽을 때, 첫 번째로 데이터를 읽은 후 트랜잭션2가 해당 데이터를 수정하고 커밋을 한 경우 트랜잭션1이 동일한 데이터를 다시 읽으면 이전의 데이터와는 다른 데이터를 얻게 된다.
- 허상 읽기 (phantom read): 트랜잭션1이 쿼리(여러 데이터를 조회하는 작업 수행)를 두 번 실행할 때, 첫 번째로 실행 후 트랜잭션2가 데이터를 삽입하거나 삭제하고 커밋한 경우 트랜잭션1이 두 번째로 실행하면 트랜잭션2가 삽입한 데이터가 보이거나 삭제한 데이터가 보이지 않게 된다.
- 소실된 수정 (lost update): 트랜잭션1과 트랜잭션2가 동일한 데이터를 읽고 수정하려는 경우 트랜잭션1이 먼저 데이터를 수정하고 커밋을 하기 전에 트랜잭션2가 동일한 데이터를 수정하였고 트랜잭션1이 커밋한 후에 트랜잭션2도 커밋을 하게 되면 트랜잭션1이 수정한 데이터를 트랜잭션2가 덮어쓰게 되어 트랜잭션1이 수정한 데이터가 소실된다.
이러한 문제를 해결하기 위해 트랜잭션을 격리해야 하며 격리 수준에 따라 데이터의 일관성이 달라지게 된다. 격리 수준이 높을수록 데이터의 일관성이 높아진다.
트랜잭션 격리 수준은 다음과 같다(낮은 순서대로).
- 커밋되지 않은 데이터 읽기 (read uncommitted): 다른 트랜잭션이 아직 커밋하지 않은(uncommitted) 값을 한 트랜잭션이 읽을 수 있다. 읽기 락은 발생하지 않지만 쓰기 락은 발생 가능하다. 쓰기 락은 트랜잭션 종료까지 유지된다. 이 격리 수준에서는 오염된 값 읽기, 재현 불가한 읽기, 허상 읽기 문제가 발생 가능하다.
- 커밋된 데이터 읽기 (read committed): 한 트랜잭션이 다른 트랜잭션이 커밋한(committed) 값만 읽을 수 있다. 읽기 락과 쓰기 락 모두 발생 가능하다. 읽기 락은 작업이 완료되면 해제되지만 쓰기 락은 트랜잭션이 종료될 때까지 유지된다. 이 격리 수준에서는 오염된 값 읽기 문제를 해결할 수 있지만, 재현 불가한 읽기, 허상 읽기 문제는 발생 가능하다.
- 재현 가능한 읽기 (repeatable read): 트랜잭션이 데이터를 여러 번 읽어도 동일한 데이터를 읽도록 보장하며 읽기 작업 트랜잭션이 지속되는 동안에 다른 트랜잭션은 해당 데이터의 삽입은 가능하지만 변경, 삭제를 할 수 없다. 읽기 락과 쓰기 락 모두 발생 가능하다. 읽기 락과 쓰기 락 모두 트랜잭션이 종료될 때까지 유지된다. 이 격리 수준에서는 오염된 값 읽기, 재현 불가한 읽기 문제를 해결할 수 있지만, 허상 읽기 문제는 발생 가능하다.
- 직렬화 가능한 읽기 (serializable read): 트랜잭션이 테이블을 여러 번 읽어도 정확히 동일한 로우를 읽도록 보장하며 읽기 작업 트랜잭션이 지속되는 동안에는 다른 트랜잭션이 해당 테이블에 삽입, 변경, 삭제를 할 수 없다. 읽기 락과 쓰기 락 모두 발생 가능하다. 이 때 두 락은 로우 레벨에 대한 락이다. 읽기 락과 쓰기 락 모두 트랜잭션이 종료될 때까지 유지된다. 이 격리 수준은 동시성 문제를 모두 해결하지만 성능이 현저히 떨어진다.
재현 가능한 읽기와 직렬화 가능한 읽기 격리 수준의 차이는 트랜잭션이 지속되는 도중 삽입을 할 수 있는지 그렇지 않은지이다.
트랜잭션 격리 수준은 @Transactional
의 isolation
트랜잭션 속성에 다음과 같이 명시한다.
@Transactional(isolation = Isolation.격리속성)
스프링 org.springframework.transaction.TransactionDefinition
인터페이스에 다섯 가지 격리 수준이 정의되어 있다.
DEFAULT
: 데이터베이스의 기본 격리 수준을 사용한다.- 대다수 데이터베이스의 기본 격리 수준은 커밋된 데이터 읽기이다.
READ_UNCOMMITTED
READ_COMMITTED
REPEATABLE_READ
SERIALIZABLE
Comments