본문 바로가기

스프링

@Transactional

오늘은 @Transactional 어노테이션에 대해 정리해보려한다.

우선 @Transactional 어노테이션을 사용하려면 트랜잭션에 대해 알아야할 필요가있다.

 

트랜잭션이란?

데이터베이스의 상태를 변환시키는 하나의 논리적 기능을 수행하기 위한 작업 단위나 한번에 수행되어야할 일련의 연산을 의미한다.

 

특징으로는 Database시스템에서 병행제어 및 회복 작업이 수행될 때 처리되는 작업의 논리적 단위로

사용자가 어떤 서비스를 요청할 때 시스템이 응답하기 위한 상태 변환 과정의 작업단위이다.

각 트랜잭션은 commit 혹은 rollback되어야한다.(원자성 보장)

 

1. 원자성 (Atomicity)

각 트랜잭션은 commit 혹은 rollback되어야한다. 

트랜잭션 안에서의 모든 명령은 완벽하게 수행되어야하고, 만약에 이 과정에서 작은 오류라도 발생한다면 트랜잭션은 

전부 작업이 취소되어야한다.

 

2. 일관성 (Consistency)

트랜잭션은 실행이 성공적으로 완료되면 항상 일관성 있는 상태로 변환되어야한다. 그리고 시스템이 가지고 있는 고정적인 요소는 트랜잭션 수행 전, 후의 상태가 같아야한다.

 

3. 격리성 (Isolation)

어떤 트랜잭션이 실행되고 있을 때 다른 트랜잭션의 연산이 끼어들 수는 없다. 그리고 다른 트랜잭션이 수행중인 트랜잭션의 결과를 참조하기 위해서는 수행중인 트랜잭션이 완전히 완료될 때까지 기다려야한다.

 

4. 지속성 (Durability)

성공적으로 완료된 트랜잭션은 시스템이 고장나도 영구적으로 반영되어 있어야한다.

 

트랜잭션 상태

활동(Activity) - 트랜잭션이 실행 중인 상태

실패(Failed) - 트랜잭션 실행에 오류가 발생하여 중단된 상태

부분완료(Partially Committed) - 트랜잭션의 마지막 연산까지 실행했지만, Commit연산이 실행되기 직전의 상태

완료(Committed) - 트랜잭션이 성공적으로 종료되어 Commit 연산을 실행한 후의 상태

 

@Transactional

스프링에서 @Transactional을 이용하여 메소드, 클래스, 인터페이스의 트랜잭션 처리가 가능하다.

@Service
public class TestService{
	
    @Transactional
    public void test(TestDto testDto) throw Exception {
    ...
    }
}

 

또한 @Transactional에서 사용할 수 있는 옵션들이 있는데

isolation 

트랜잭션에서 일관성없는 데이터를 어떻게 허용할지에 대한 허용 수준을 정할 수 있는 옵션이다.

아래와 같은 형태로 설정해주면 된다.

@Service
public class TestService{
	
    @Transactional(isolation=Isolation.DEFAULT)
    public void test(TestDto testDto) throw Exception {
    ...
    }
}

★ DEFAULT

기본적인 격리 수준이고, 기존 DB의 Isolation Level을 따르게 된다.

 

READ_UNCOMMTIED(Level 0)

커밋되지 않은 데이터에 대한 읽기를 허용하는 방식이다.

이렇게 설정하면 Dirty Read가 되기에 문제가 발생할 수 있다.

 

문제발생예시

 - 트랜잭션 A가 어떤 값을 1에서 2로 변경하고 아직 커밋하지 않은 상황에서 트랜잭션B가 같은 값을 읽는 경우 트랜잭션 B는 2가 조회 된다.

 

 - 트랜잭션 B가 2를 조회 한 후 혹시 A가 롤백된면 결국 트랜잭션B는 잘못된 값을 읽게 된 것이다. 즉, 아직 트랜잭션이 완료되지 않은 상황에서 데이터에 접근을 허용할 경우 발생할 수 있는 데이터 불일치다.

 

★ READ_COMMITTED (Level 1)

트랜잭션이 커밋된 확정데이터만 읽기를 허용하는 방식이다.

Dirty Read는 방지되지만 Non-Repeatable Read의 문제가 발생할 수 있다.

 

문제발생예시

 - 트랜잭션 A가 1번데이터를 읽었다. 이후 A는 같은 쿼리를 또 실행할 예정인데, 그 사이에 트랜잭션 B가 값 1을 2로 바꾸고 커밋해버리면 A가 같은 쿼리 두번을 날리는 사이 두 쿼리의 결과가 다르게 되어 버린다.

 

 - 한 트랜잭션에서 같은 쿼리를 두번 실행했을 때 발생할 수 있는 데이터 불일치이다.

Dirty Read에 비해서는 발생 확률이 적다.

 

★ REPEATABLE_READ (Level 2)

동일 필드에 대해 다중으로 접근할 때 동일한 결과를 보장하는 방식이다.

트랜잭션이 완료될 때까지 SELECT 문장이 사용되는 모든 데이터들에 대해 Shared Lock이 걸려 다른 사용자가 그 데이터에 대한 접근이 불가능해진다.

어떤 트랜잭션이 수행될 때 다른 트랜잭션이 앞선 트랜잭션이 사용 중인 데이터에 대해 갱신하거나 삭제하는게 불가능하기에 트랜잭션 내에서 여러번 데이터를 접근한다해도 데이터의 일관성을 보장할 수 있다.

 

Non-Repeatable Read 문제는 방지할 수 있으나, Phantom Read 문제가 생긴다.

 

문제발생예시

한 트랜잭션에서 일정 범위의 레코드를 두번 이상 읽을 때 발생하는 데이터 불일치로,

트랜잭션 A가 어떤 조건을 사용하여 특정 범위의 값들[0,1,2,3,4]을 읽었다.

이후 A는 같은 쿼리를 실행할 예정인데, 그 사이에 트랜잭션 B가 같은 테이블에 값[5,6,7]을 추가해버리면 A가 같은 쿼리 두번을 날리는 사이 두 쿼리의 결과가 다르게 되어 버린다.

 

SERIALIZABLE (Level 3)

해당 설정은 데이터의 일관성과 동시성을 유지하기 위해 MVCC(Multi Version Concurrency Control) 을 사용하지 않는다. (MVCC 는 다중 사용자 데이터 베이스 성능 을 위한 기술로 데이터를 조회할때 LOCK 을사용하지 않고 데이터의 버전을 관리하여 데이터 일관성과 동시성을 높이는 기술)
그리고 트랜잭션이 완료될 때 까지 SELECT 문장이 사용하는 모든 데이터에 Shared Lock 을 걸어 다른 사용자가 해당 영역에 있는 모든 데이터에 대한 수정과 입력이 불가능하게 만들어 Phantom Read를 방지한다.

 

설명만 보자면 SERIALIZABLE속성을 써야하겠지만 격리수준이 높아지면 성능저하의 문제가 발생할 수 있다.

propagation

트랜잭션이 동작할 때 다른 트랜잭션이 호출되면 어떻게 처리할지를 선택할 수 있는 옵션이다.

피호출 트랜잭션에서 호출한 트랜잭션을 그대로 사용할지 새로 생성할 수 있다.

@Service
public class TestService{
	
    @Transactional(propagation=Propagation.REQUIRED)
    public void test(TestDto testDto) throw Exception {
    ...
    }
}

REQUIRED

디폴트 속성, 부모 트랜잭션 내에서 실행하며 부모 트랜잭션이 없을 경우 새로운 트랜잭션을 생성한다.

 

SUPPORS

이미 시작된 트랜잭션이 있으면 참여하고 그렇지 않으면 트랜잭션 없이 진행하게 만든다.

 

REQUIRES_NEW

부모 트랜잭션을 무시하고 무조건 새로운 트랜잭션이 생성된다.

 

MANDATORY

REQUIRED와 비슷하게 이미 시작된 트랜잭션이 있으면 참여한다.

반면에 트랜잭션이 시작된 것이 없으면 새로 시작하는 대신 예외를 발생시킨다.

독립적으로는 트랜잭션을 진행하면 안될 경우 사용.

 

NOT_SUPPORTED

트랜잭션 없이 작업을 수행한다. 만약 진행중인 트랜잭션이 있다면 트랜잭션을 잠시 보류시킨다.

 

NEVER

트랜잭션을 사용하지 않게 강제한다. 이미 진행중인 트랜잭션이 있다면 Exception을 발생시키고 트랜잭션이 진행 중이지 않을 때 작업을 수행한다.

 

NESTED

이미 진행중인 트랜잭션이 있으면 중첩된 트랜잭션을 실행하고 없으며 REQUIRED와 동일하게 새로운 트랜잭션을 생성하여 진행한다. 중첩 트랜잭션을 말 그대로 트랜잭션안에 트랜잭션을 만드는 것이다. 트랜잭션안의 트랜잭션이 커밋되거나 롤백되어도, 바깥의 트랜잭션에게 영향을 주지 않는다.

 

noRollbackFor, .rollbackFor

특정 예외 발생 시, rollback을 하거나 하지않게 하는 옵션이다.

@Transactional(noRollvackFor=CustomException.class)

선언적 트랜잭션에서는 런타임 예외가 발생하면 롤백하게된다.

반면에 예외가 전혀 발생하지 않거나 체크 예외가 발생하면 커밋한다.

체크 예외를 커밋 대상으로 삼은 이유는 체크 예외가 예외적인 상황에서 사용되기보다는 리턴 값을 대신해서 비즈니스적인 의미를 담은 결과를 돌려주는 용도로 많이 사용되기 때문이다.

 

  rollbackFor 속성

특정 예외가 발생 시 강제로 Rollback

예: @Transactional(rollbackFor=Exception.class)

 

★ noRollbackFor 속성

특정 예외의 발생 시 Rollback 처리되지 않음

 

readOnly

트랜잭션을 읽기전용으로 설정하는 옵션이다.

true로 설정하면 insert, update, delete 실행할 때 예외가 발생한다. 

default값은 false이다.

성능을 최적화 하기위해서도 사용할 수 있다고하지만 나는 보통 특정 트랜잭션 작업에서 누군가 쓰기 작업을 하는 걸 의도적으로 방지하기 위해 사용해왔다.

@Transactional(readOnly =true)

 

Timeout

지정한 시간 내에 메서드 수행이 완료되지 않으면 rollback하게 하는 옵션이다. 

 -1 이 default 값이고 이는 no timeout을 의미한다.

@Transactional(timeout=10)

중요한 설정인데 종종 놓치기 쉽다. 회사에서 이 설정을 빼먹어서 서버다운이 되었다는 풍문은 가끔 들었다.

 

 

'스프링' 카테고리의 다른 글

Interceptor와 filter의 차이  (0) 2022.03.27
@Controller와 @RestController 차이  (0) 2022.03.23
Domain(Entity) / DTO / DAO  (0) 2022.03.14
JPA 연관 관계 정리 / @OneToMany 단방향의 단점  (0) 2022.03.10
Annotation - @Entity  (0) 2022.03.09