Kim-Baek 개발자 이야기

JPA 트랜잭션의 전파 및 격리 본문

개발

JPA 트랜잭션의 전파 및 격리

김백개발자 2020. 8. 4. 17:07

1. JPA 트랜잭션의 전파방식, 격리 수준에 대한 설명

@Transactional을 클래스 또는 메써드 레벨에 명시하면 해당 메써드 호출시 지정된 트랜잭션이 작동하게 된다. 단, 조건이 있다. 해당 클래스의 Bean을 다른 클래스의 Bean에서 호출할 때만 @Transactional을 인지하고 작동하게 된다.(같은 빈 내에서 @Transactional이 명시된 다른 메써드를 호출해도 작동하지 않는다.) Spring Framework는 내부적으로 AOP를 통해 해당 어노테이션을 인지하여 프록시를 생성하여 트랜잭션을 자동 관리하기 때문이다.

Propagation.REQUIRED

@Transactional(propagation = Propagation.REQUIRED)

  • 특정 메써드의 트랜잭션이 Propagation.REQUIRED로 설정되었을 때의 트랜잭션 동작은 다음과 같다. 기본적으로 해당 메써드를 호출한 곳에서 별도의 트랜잭션이 설정되어 있지 않았다면 트랜잭션를 새로 시작한다.(새로운 연결을 생성하고 실행한다.) 만약, 호출한 곳에서 이미 트랜잭션이 설정되어 있다면 기존의 트랜잭션 내에서 로직을 실행한다.(동일한 연결 안에서 실행된다.) 예외가 발생하면 롤백이 되고 호출한 곳에도 롤백이 전파된다. 이러한 Propagation.REQUIRED 동작 방식을 원할 경우 기본값으로 설정되어 있기 때문에 생략해도 된다.
  • 만약, 해당 메써드가 호출한 곳와 별도의 쓰레드라면 어떤 동작이 일어날까? 답은 전파 레벨과 상관 없이 무조건 별도의 트랜잭션을 생성하여 해당 메써드를 실행한다. Spirng은 내부적으로 트랜잭션 정보를 ThreadLocal 변수에 저장하기 때문에 다른 쓰레드로 트랜잭션이 전파되지 않는다. 

Propagation.REQUIRES_NEW

@Transactional(propagation = Propagation.REQUIRES_NEW)

  • Propagation.REQUIRES_NEW로 설정되었을 때에는 매번 새로운 트랜잭션을 시작한다.(새로운 연결을 생성하고 실행한다.) 만약, 호출한 곳에서 이미 트랜잭션이 설정되어 있다면(기존의 연결이 존재한다면) 기존의 트랜잭션은 메써드가 종료할 때까지 잠시 대기 상태로 두고 자신의 트랜잭션을 실행한다. 새로운 트랜잭션 안에서 예외가 발생해도 호출한 곳에는 롤백이 전파되지 않는다. 즉, 2개의 트랜잭션은 완전히 독립적인 별개로 단위로 작동한다.Propagation.NESTED

Propagation.NESTED

@Transactional(propagation = Propagation.NESTED)

  • Propagation.NESTED는 기본적으로 앞서 설명한 Propagation.REQUIRED와 동일하게 작동한다. 중요한 차이점은, SAVEPOINT를 지정한 시점까지 부분 롤백이 가능하다는 것이다. 유의할 점은, 데이터베이스가 SAVEPOINT 기능을 지원해야 사용이 가능하다. (대표적으로 Oracle이 해당한다.)
  • 내부에 중첩 트랙잭션을 만드는데 중첩된 트랜잭션은 먼저 시작된 부모 트랜잭션의 커밋과 롤백에는 영향을 받지만 자신의 커밋과 롤백은 부모 트랜잭션에게 영향을 주지 않는다.

 

4. 다수의 트랜잭션이 경쟁시 발생할 수 있는 문제

다수의 트랜잭션이 동시에 실행되는 상황에선 트랜잭션 처리방식을 좀 더 고려해야 한다.

예를들어 특정 트랜잭션이 처리중이고 아직 커밋되지 않았는데 다른 트랜잭션이 그 레코드에 접근한 경우 다음과 같은 문제가 발생할 수 있다.

▶ Problem1 - Dirty Read

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

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

▶ Problem2 - Non-Repeatable Read

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

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

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

▶ Problem3 - Phantom Read

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

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

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

5. 스프링 트랜잭션 속성

위와 같은 상황 (다수의 트랜잭션이 경쟁시) 을 방지할 수 있는 속성, 이외 별도 속성들을 확인해보자.

[ 1. isolation (격리수준) ]

 - 트랜잭션에서 일관성이 없는 데이터를 허용하도록 하는 수준을 말한다. 

▶ DEFAULT

기본 격리 수준(기본설정, DB의 Isolation Level을 따름)

▶ READ_UNCOMMITTED (level 0)

 - 커밋되지 않는(트랜잭션 처리중인) 데이터에 대한 읽기를 허용

 - 즉 어떤 사용자가 A라는 데이터를 B라는 데이터로 변경하는 동안 다른 사용자는 B라는 아직 완료되지 않은(Uncommitted 혹은 Dirty) 데이터 B를 읽을 수 있다.

 - Problem1 - Dirty Read 발생

우리 개발 환경에서는 해당 Transaction 레벨을 허용하지 않아 
[ READ_COMMITTED와 SERIALIZABLE만이 적합한 트랜잭션 레벨입니다 ] 라는 오류가 발생한다. 다른 옵션들은 문제 발생하지 않음.

▶ READ_COMMITTED (level 1)

 - 트랜잭션이 커밋 된 확정 데이터만 읽기 허용

 - 어떠한 사용자가 A라는 데이터를 B라는 데이터로 변경하는 동안 다른 사용자는 해당 데이터에 접근할 수 없다.

 - Problem1 - Dirty Read 방지

▶ REPEATABLE_READ (level 2)

 - 트랜잭션이 완료될 때까지 SELECT 문장이 사용하는 모든 데이터에 shared lock이 걸리므로 다른 사용자는 그 영역에 해당되는 데이터에 대한 수정이 불가능하다.

 - 선행 트랜잭션이 읽은 데이터는 트랜잭션이 종료될 때까지 후행 트랜잭션이 갱신하거나 삭제가 불가능 하기때문에 같은 데이터를 두 번 쿼리했을 때 일관성 있는 결과를 리턴한다.

 - Problem2 - Non-Repeatable Read 방지

▶ SERIALIZABLE (level 3)

 - 데이터의 일관성 및 동시성을 위해 MVCC(Multi Version Concurrency Control)을 사용하지 않음

(MVCC는 다중 사용자 데이터베이스 성능을 위한 기술로 데이터 조회 시 LOCK을 사용하지 않고 데이터의 버전을 관리해 데이터의 일관성 및 동시성을 높이는 기술)

 - 트랜잭션이 완료될 때까지 SELECT 문장이 사용하는 모든 데이터에 shared lock이 걸리므로 다른 사용자는 그 영역에 해당되는 데이터에 대한 수정 및 입력이 불가능하다.

 - Problem3 - Phantom Read 방지

   (※ 격리 수준이 올라갈 수록 성능 저하의 우려가 있음)

 

반응형
Comments