[Spring] @Transactional 개념, 사용법
Java & Spring

[Spring] @Transactional 개념, 사용법

이번 포스팅에선 트랜잭션의 개념과 스프링에서 @Transactional 어노테이션을 통해 트랜잭션 관리를 하는 방법을 알아본다.

트랜잭션?

트랜잭션은 데이터베이스의 상태를 변화시키는 하나의 논리적 기능을 수행하기 위한 작업의 단위이다. 

예를 들어, A가 B에게 돈을 송금한다고 해보자. A의 계좌에서 돈을 차감하고, B의 계좌에 돈을 추가하는 과정은 하나의 작업의 단위이다. 둘 중 하나가 실패했을 경우 전체가 원상태로 돌아가야(rollback)하고 두 작업이 모두 완료되어야 성공(commit)되어야 한다. 이 작업의 단위를 트랜잭션이라고 한다.

 

우리는 보통 트랜잭션의 특징을 말할 때 줄여서 ACID라고 말한다.

원자성(Atomicity) 트랜잭션이 DB에 모두 반영되거나, 혹은 전혀 반영되지 않아야 된다.
일관성(Consistency) 트랜잭션의 작업 처리 결과는 항상 일관성 있어야 한다.
독립성(Isolation) 둘 이상의 트랜잭션이 동시에 병행 실행되고 있을 때, 어떤 트랜잭션도 다른 트랜잭션 연산에 끼어들 수 없다.
지속성(Durability) 트랜잭션이 성공적으로 완료되었으면, 결과는 영구적으로 반영되어야 한다.  

 

스프링에서는 @Transactional 어노테이션을 통해 트랜잭션을 관리해준다. 우리는 일련의 작업을 수행하는 메서드를 만들고 위에 @Transactional만 붙여주면 해당 메서드 내에서 오류가 발생할 경우 전체가 롤백되고, 모두 성공해야 데이터베이스에 커밋되도록 관리해줄 수 있다.

@Transactional
public void test() {
	...
}

사용법

속성값

@Transactional을 사용할 때 아래와 같이 속성값을 넣어 줄 수 있다. 다양한 속성값이 있는데 아래에서 알아보자.

// 종류: propagation, isolation, readOnly, rollbackFor, timeout..
@Transactional(propagation=Propagation.SUPPORT)
public void testMethod() {
	...
}

1. propagation: 비즈니스 로직에서의 트랜잭션 범위를 정의 == 트랜잭션의 전파 설정

  • 특정 트랜잭션이 붙은 메서드 내부에서 또 트랜잭션이 붙은 메서드를 호출할 경우 어떻게 될까? 새로운 트랜잭션이 생성될 수도 있고, 부모 트랜잭션에 합류할 수도 있다. 이 방식을 정해주는 속성이다.
  • 옵션 종류
REQUIRED 기본값. 부모 트랜잭션이 존재하면 부모 트랜잭션으로 합류. 없다면 새로운 트랜잭션 생성
REQUIRES_NEW 무조건 새로운 트랜잭션 생성. 각각의 트랜잭션이 롤백되더라도 서로 영향 주지 않음
MANDATORY 부모 트랜잭션에 합류. 만약 부모 트랜잭션이 없다면 예외 발생
NESTED 부모 트랜잭션이 존재하면 중첩 트랜잭션 생성. 중첩 트랜잭션 내부에서 롤백 발생하면 해당 중첩 트랜잭션의 시작 지점까지만 롤백됨. 커밋은 부모 트랜잭션이 커밋될 때 같이 커밋됨. 부모 트랜잭션이 없다면 새로운 트랜잭션 생성
NEVER 트랜잭션 생성하지 않음. 부모 트랜잭션이 존재한다면 예외 발생
SUPPORTS 부모 트랜잭션이 있다면 합류. 만약 부모 트랜잭션이 없다면 트랜잭션 생성하지 않음
NOT_SUPPORTED 부모 트랜잭션이 있다면 보류. 만약 부모 트랜잭션이 없다면 트랜잭션 생성하지 않음  
@Transactional
public void parent() {
	...
    child(); // Transaction 내부에서 새로운 Transaction 호출
    ...
}

@Transactional
public void child() {
	...
}

 

2. isolation: 트랜잭션 격리 수준을 정의

  • 동시에 여러개의 트랜잭션에 의한 변경사항을 어떻게 적용할지에 대한 설정.
  • 종류: READ_UNCOMMITED, READ_COMMITED, REPEATABLE_READ, SERIALIZABLE
  • 먼저, 동시에 여러개의 트랜잭션으로 인해 발생할 수 있는 문제는 다음과 같다.
문제 내용 발생 Level
Dirty Read 변경사항이 반영되지 않은 값을 다른 트랜잭션에서 읽도록 허용할 경우 발생하는 데이터 불일치 Read Uncommitted
Non-Repeatable Read 한 트랜잭션 내에서 값을 조회할 때, 동시성 문제로 인해 같은 쿼리가 다른 결과를 반환하는 경우. 즉, 트랜잭션이 끝나기 전에 수정사항이 반영되어 트랜잭션 내에서 쿼리 결과가 일관성을 가지지 못하는 경우 Read Committed, Read Uncommitted
Phantom Read 외부에서 수행되는 입력/삭제 작업으로 인해 트랜잭션 내에서의 동일한 쿼리가 다른 값을 반환하는 경우 Repeatable Read, Read Committed, Read Uncommitted
  • 동시에 여러개의 트랜잭션으로 인해 발생할 수 있는 동시성 문제를 해결하는 isolation 속성
옵션명 내용 발생 문제
DEFAULT(기본값)
별도의 값을 설정하지 않은 경우. DBMS의 isolation level을 따름
-
READ_UNCOMMITED
아직 commit 되지 않은 데이터를 다른 트랜잭션이 읽는 것을 허용
Dirty Read, Non-repeatable Read, Phantom Read
READ_COMMITED
commit이 이루어진 트랜잭션만 조회 가능
Non-repeatable Read, Phantom Read
REPEATABLE_READ
트랜잭션 범위 내에서 조회한 데이터가 항상 동일함을 보장
다른 사용자는 트랜잭션 영역에 해당되는 데이터에 대한 수정 불가능
Phantom Read
SERIALIZABLE
완벽한 읽기 일관성 모드를 제공
다른 사용자는 트랜잭션 영역에 해당되는 데이터 수정 및 입력 불가능
성능 저하의 우려가 있음
없음
// 사용방법
@Transactional(isolation=Isolation.READ_COMMITED)
public void test() {
	...
}

 

참고사항

Non-Repeatable Read VS Phantom Read

1번이 동일한 쿼리를 2번 실행하던 중 2번이 그 사이에 트랜잭션을 실행하고 commit까지 쳤다고 가정하자.

  • Non-Repeatable Read는 1번이 실행한 쿼리의 결과가 두번째에서 다른 값을 가지게 되는 경우다.
  • Phantom Read는 모든 쿼리의 결과가 처음 실행했을 때와 두번째 실행했을 때 동일하지만, 다른 row가 select 된 경우다. (2번이 몇개를 지우거나 삽입했기 때문) 예를 들어서 SELECT SUM(x) FROM TABLE;을 하는 경우 어떤 row도 update 되지 않았지만 특정 row가 삽입되거나 삭제되었기 때문에 다른 값을 가져올 것이다.

 

각 DB별 isolation level

MYSQL: REPEATABLE READ
ORACLE: READ COMMITTED
H2: READ COMMITTED

 

3. readOnly: true/false(default는 false). 트랜잭션 내에서 데이터를 읽기만 할건지 정의. 쓰지 않아도 되지만 성능 최적화 및 실수로 데이터를 변경하는 일을 방지할 수 있는 옵션(readOnly = true). 사용하는 DB에 따라 락을 생략하거나 더티 체킹을 건너뛰는 등 여러 최적화 작업이 일어나 성능이 향상됨.

 

4. rollbackFor: 트랜잭션에서 예외 발생 시 롤백. 특정 예외를 지정할 수 있음. 특정 예외 발생시 롤백하지 않도록 지정할 수도 있음. 아무 속성도 주지 않았을 경우 디폴트로 RuntimeException과 Error에 대해서만 롤백. 따라서 unchecked Exception은 롤백하고 Checked Exception은 커밋한다(Default)

자바의 예외 구분(출처: https://nesoy.github.io/articles/2018-05/Java-Exception)

// 디폴트값(rollbackFor 속성을 주지 않은 경우)
@Transactional(rollbackFor = {RuntimeException.class, Error.class})
public void test() {
	...
}

// 런타임 예외가 발생하면 롤백
@Transactional(rollbackFor = {RuntimeException.class})
public void test() {
	...
}

// 모든 예외에 대해 전부 트랜잭션을 롤백
@Transactional(rollbackFor = {Exception.class})
public void test() {
	...
}

참고사항

 

@Transactional에 rollbackFor 속성을 주지 않고 디폴트로 놔뒀다고 가정하자.

아래와 같이 코드를 작성하면 str2의 길이를 가져오는 getLength(str2)에서 오류가 나서 롤백될 것이라고 생각할 수도 있다.

하지만 위에서 말했듯이, rollbackFor의 디폴트 속성은 Checked Exception은 롤백시키지 않기 때문에 그대로 커밋된다.

Checked Exception도 롤백시키고 싶다면 따로 옵션을 주거나 위처럼 모든 예외에 대해 롤백시키도록 설정하면 된다.

public int getLength(String str) throws Exception {
	if (str == null) throw new Exception();
    return str.length();
}

@Transactional
public void test() throws Exception {
	String str = "문자 1" ;
	saveStr(str);	// 문자를  DB에 저장하는 코드 
	
	String str2 = null;
	int length = getLength(str2);  // null 이기에 여기서 Exception이 발생한다.
	saveStr(str2);	// 문자를  DB에 저장하는 코드 
}

 

5. timeout: 지정한 시간 내에 해당 트랜잭션의 수행이 완료되지 않은 경우 롤백. 값은 정수형. -1로 설정하면 timeout 설정을 해제. (default는 -1)

 

테스트 환경에서의 사용

@Test와 @Transactional을 함께 붙이면 메서드가 종료될 때 자동으로 롤백된다.

만약 롤백하고 싶지 않다면 아래와 같이 2가지 방법으로 설정하면 된다.

// 첫번째 방법 @Rollback(false)
@Test
@Transactional
@Rollback(false)
public void testMethod() {
	...
}


// 두번째 방법 @TransactionConfiguration
@TransactionConfiguration(transactionManager="transactionManager", defaultRollback=false)
public class Test {
	...
}

 

 

 

Reference

https://deveric.tistory.com/86 트랜잭션 propagation

https://programmer93.tistory.com/75 트랜잭션 rollbackFor

https://stackoverflow.com/questions/11043712/what-is-the-difference-between-non-repeatable-read-and-phantom-read Non-repeatable read vs phantom read

 

 

728x90
반응형