해당 게시글은 내용 정리 목적으로 작성되었습니다. 틀린 내용이 있다면 언제든지 말씀해 주세요

목차

1. 트랜잭션 격리성 수준 4가지
2. 트랜잭션 격리성의 동시성 문제
3. 트랜잭션 격리성의 설정 및 조회
4. 결론


트랜잭션 격리성은 여러 트랜잭션이 동시에 실행될 때 데이터의 일관성을 어떻게 유지할지를 결정하는 기준으로, 다양한 격리성 수준이 정의되어 있다.

트랜잭션 격리성 수준이 어떤 것이 존재하고, 이로 인해 해결되는 동시성 문제가 무엇인지 알아보자

1. 트랜잭션 격리성 수준 4가지

READ UNCOMMITTED (커밋 전 읽기)

  • 트랜잭션 작업이 커밋되지 않아도 데이터를 읽을 수 있다.
  • 트랜잭션 간의 락을 사용하지 않아서 성능이 가장 좋다.
  • 다만 더러운 읽기 (Dirty Read), 반복 불가능한 읽기 (Non-Repeatable Read), 팬텀 읽기 (Phantom Read)와 같은 동시성 문제가 발생할 수 있다.

READ COMMITTED (읽기 완료)

  • 트랜잭션이 커밋된 데이터만 읽을 수 있다. 즉, 다른 트랜잭션이 커밋한 변경 사항만 읽을 수 있다.
  • Dirty Read는 방지 되지만 Non-Repeatable ReadPhantom Read가 여전히 발생한다

REPEATABLE READ (반복 읽기 가능, 일관적인 읽기 가능)

  • 트랜잭션이 시작된 시점의 데이터 상태를 유지하며, 트랜잭션이 끝날 때까지 동일한 데이터를 반복해서 읽을 수 있다.
  • 즉, 트랜잭션이 실행되는 동안 읽은 데이터는 트랜잭션의 종료까지 일관되게 유지된다
  • 대부분의 DB가 REPEATABLE READ 트랜잭션 격리성 수준을 기본으로 가져간다
  • 기본적으로 공유 락을 걸어서 데이터의 변경을 방지해 일관적인 읽기가 가능하도록 하지만 Mysql에 경우 MVCC를 활용해서 일관적 읽기가 가능하도록 한다.
MVCC (Multi-Version Concurrency Control)는 트랜잭션이 데이터를 읽을 때, 데이터의 버전 관리와 스냅샷을 활용하여 읽기 일관성을 제공한다.
데이터베이스는 트랜잭션이 시작될 때의 데이터 상태를 유지하며, 이로 인해 반복 읽기 문제를 해결한다

SERIALIZABLE (직렬화 가능)

  • 가장 높은 격리성 수준으로, 모든 트랜잭션이 순차적으로 실행되는 것처럼 동작한다.
  • 즉, 트랜잭션들이 서로 간섭하지 않도록 보장한다 

2. 트랜잭션 격리성의 동시성 문제

Dirty Read (더러운 읽기)

  • 커밋전 읽기 라고도 하며 말 그대로 특정 트랜잭션에서 수정한 자원을 커밋하지 않아도 다른 트랜잭션에서 조회가 가능한 것을 의미한다. 

Non-Repeatable Read (반복 불가능한 읽기)

  • 반복 불가능한 읽기일관적이지 않은 읽기라고도 말할 수 있다.
  • 트랜잭션이 동일한 데이터를 여러 번 읽을 때 다른 트랜잭션에 의해서 데이터가 변경되어 일관적이지 않은 데이터 조회가 발생하는 현상이다.

Phantom Read (팬텀 읽기)

  • 트랜잭션이 쿼리를 실행할 때, 다른 트랜잭션이 새로운 데이터를 삽입하거나 삭제하여 결과가 달라지는 현상이다.

3. 트랜잭션 격리성 조회 및 설정

# 현재 세션의 트랜잭션 격리성 수준 조회
# MySQL
SELECT @@session.tx_isolation AS 'Isolation Level';
SELECT @@session.transaction_isolation AS 'Isolation Level';

# PostgreSQL
SHOW transaction_isolation;

# Oracle
SELECT s.SESSION_ID, s.ISOLATION_LEVEL
FROM V$SESSION s
WHERE s.SESSION_ID = SYS_CONTEXT('USERENV', 'SESSIONID');


# 글로벌 트랜잭션 격리성 설정
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;

4. 결론

트랜잭션 격리성은 트랜잭션 간의 데이터 충돌을 방지하고 데이터베이스의 일관성과 무결성을 유지하는 중요한 개념이다. 격리성 수준을 적절히 설정함으로써 성능과 데이터 무결성 간의 균형을 맞출 수 있다.

되도록이면 기본 트랜잭션 격리성을 변경하지 않는게 좋다. 다만 각 격리성에 따라서 발생할 수 있는 문제점을 잘 숙지하면 좋을 것 같다

트랜잭션이란?

데이터베이스의 상태를 변경시키는 작업 또는 한번에 수행되어야하는 연산들을 의미한다.

트랜잭션 작업이 끝나면 Commit 또는 Rollback 되어야한다.


트랜잭션의 성질

원자성(Atomicity) 
한 트랜잭션 내에서 실행한 작업들은 하나의 단위로 처리합니다 즉, 모두 성공 또는 모두 실패 
일관성(Consistency) 
트랜잭션은 일관성 있는 데이터베이스 상태를 유지한다.
격리성(Isolation) 
동시에 실행되는 트랜잭션들이 서로 영향을 미치지 않도록 격리해야한다.
영속성(Durability) 
트랜잭션을 성공적으로 처리되면 결과가 항상 저장되어야한다.

@Transactional

스프링에서 지원하는 선언적 트랜잭션이다.. xml 또는 Javaconfig를 통해 설정 할 수 있다.

Spring boot에서는 별도의 설정이 필요 없으며, 클래스 또는 메소드에 선언할 수 있다.

import com.example.bamdule.service.UserService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
//@Transactional(propagation = , isolation = ,noRollbackFor = ,readOnly = ,rollbackFor = ,timeout = )
public class UserServiceImpl implements UserService {
...
}

 


@Transactional 옵션

propagation
트랜잭션 동작 도중 다른 트랜잭션을 호출할 때, 어떻게 할 것인지 지정하는 옵션이다
isolation
트랜잭션에서 일관성없는 데이터 허용 수준을 설정한다
noRollbackFor=Exception.class
특정 예외 발생 시 rollback하지 않는다.
rollbackFor=Exception.class
특정 예외 발생 시 rollback한다.
timeout
지정한 시간 내에 메소드 수행이 완료되지 않으면 rollback 한다.  (-1일 경우 timeout을 사용하지 않는다)
readOnly
트랜잭션을 읽기 전용으로 설정한다.

1. propagation 

REQUIRED (Default)
이미 진행중인 트랜잭션이 있다면 해당 트랜잭션 속성을 따르고, 진행중이 아니라면 새로운 트랜잭션을 생성한다
REQUIRES_NEW
항생 새로운 트랜잭션을 생성한다. 이미 진행중인 트랜잭션이 있다면 잠깐 보류하고 해당 트랜잭션 작업을 먼저 진행한다
SUPPORT 
이미 진행 중인 트랜잭션이 있다면 해당 트랜잭션 속성을 따르고, 없다면 트랜잭션을 설정하지 않는다.
NOT_SUPPORT  
이미 진행중인 트랜잭션이 있다면 보류하고, 트랜잭션 없이 작업을 수행한다.
MANDATORY  
이미 진행중인 트랜잭션이 있어야만, 작업을 수행한다. 없다면 Exception을 발생시킨다.
NEVER
트랜잭션이 진행중이지 않을 때 작업을 수행한다. 트랜잭션이 있다면 Exception을 발생시킨다.
NESTED  
진행중인 트랜잭션이 있다면 중첩된 트랜잭션이 실행되며, 존재하지 않으면 REQUIRED와 동일하게 실행된다.

TransactionSynchronizationManager.getCurrentTransactionName() 을 사용하면 간단하게 테스트해 볼 수 있다.

@Service
@Transactional
public class UserServiceImpl implements UserService {

    @Autowired
    private UserDao userDao;

    @Autowired
    private ProductService productService;

    @Override
    public void save(User user) {
    
        //트랜잭션 이름을 출력한다.
        System.out.println(TransactionSynchronizationManager.getCurrentTransactionName());
        productService.testTransaction();
        userDao.save(user);
    }
}

 

@Service
public class ProductServiceImpl implements ProductService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    @Override
    public void testTransaction() {
        System.out.println(TransactionSynchronizationManager.getCurrentTransactionName());
    }

}

 

userServiceImpl.save(user) 메소드 실행 시, 트랜잭션이 생성된다.

propagation 옵션이 없으므로 Default 옵션인  REQUIRED가 설정된다. 그리고 productService.testTransaction() 메소드가 실행되면서 새로운 트랜잭션이 생성되고 이전에 진행중이던 트랜잭션은 잠시 보류된다.

(트랜잭션 이름 출력하는 것을 보면 각각 이름이 다른 것을 볼 수 있다.) 

testTransaction() 메소드가 종료되면 보류 중이던 트랜잭션이 다시 진행 된다.

com.example.bamdule.service.impl.UserServiceImpl.save
com.example.bamdule.service.impl.ProductServiceImpl.testTransaction

다른 propagation 옵션을 설정해보면서 트랜잭션이 어떻게 전파되는지 테스트해보면 잘 이해 될 것이다.


2. isolation

Default
사용하는 DB 드라이버의 디폴트 설정을 따른다. 대부분 READ_COMMITED를 기본 격리수준으로 설정한다.
READ_COMMITED
트랜잭션이 커밋하지 않은 정보는 읽을 수 없다. 하지만 트랜잭션이 읽은 로우를 다른 트랜잭션에서 수정 할 수 있다
그래서 트랜잭션이 같은 로우를 읽었어도 시간에 따라서 다른 내용이 발견될 수 있다.
READ_UNCOMMITED
가장 낮은 격리 수준이다.  트랜잭션이 커밋되기 전에 그 변화가 다른 트랜잭션에 그대로 노출된다.
하지만 속도가 빠르기 떄문에 데이터의 일관성이 떨어지더라도, 성능 극대화를 위해 의도적으로 사용하기도 한다.
REPEATABLE_READ
트랜잭션이 읽은 로우를 다른 트랜잭션에서 수정되는 것을 막아준다. 하지만 새로운 로우를 추가하는 것은 제한하지 않는다.
SERIALIZABLE
가장 강력한 트랜잭션 격리수준이다. 여러 트랜잭션이 동시에 같은 테이블 로우에 액세스하지 못하게 한다.
가장 안전하지만 가장 성능이 떨어진다.

3. rollbackFor

트랜잭션 작업 중 런타임 예외가 발생하면 롤백한다. 반면에 예외가 발생하지 않거나 체크 예외가 발생하면 커밋한다.

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

스프링에서는 데이터 엑세스 기술의 예외를 런타임 예외로 전환해서 던지므로 런타임 예외만 롤백대상으로 삼는다.

하지만 원한다면 체크예외지만 롤백 대상으로 삼을 수 있다. rollbackFor또는 rollbackForClassName 속성을 이용해서 예외를 지정한다.


4. noRollbackFor

rollbackFor 속성과는 반대로 런타임 예외가 발생해도 지정한 런타임 예외면 커밋을 진행한다.


5. timeout

트랜잭션에 제한시간을 지정한다. 초 단위로 지정하고, 디폴트 설정으로 트랜잭션 시스템의 제한시간을 따른다. 

-1 입력 시, 트랜잭션 제한시간을 사용하지 않는다.


6. readOnly

트랜잭션을 읽기 전용으로 설정한다. 특정 트랜잭션 안에서 쓰기 작업이 일어나는 것을 의도적으로 방지하기 위해 사용된다. insert,update,delete 작업이 진행되면 예외가 발생한다.


참조 : https://springsource.tistory.com/136

+ Recent posts