본문 바로가기
Spring Boot

Spring 트랜잭션 관리

by 배털 2022. 3. 28.

제 글에 문제가 있다면 댓글로 알려주시면 감사하겠습니다! 🙇‍♂️

트랜잭션?

트랜잭션이란 쪼갤 수 없는 업무 처리의 최소 단위를 말합니다.
트랜잭션 종류에는 데이터베이스 트랜잭션, 메시지 큐 트랜잭션 등등이 있습니다.

트랜잭션의 성질

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

Spring에서 트랜잭션을 관리하는 방법은 크게 서로 대비되는 2가지 방법으로 나눌 수 있습니다.

프로그램에 의한 트랜잭션 관리 (Programmatic)

@Autowired private PlatformTransactionManager transactionManager; 

public void operateSome() { 
    TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition()); 
    try { 
    } catch (RuntimeException e) { 
        transactionManager.rollback(status); 
        throw e; 
    } finally { 
        if (status.isRollbackOnly()) { 
            transactionManager.rollback(status); 
        } else { 
            transactionManager.commit(status); 
        } 
    } 
}

위의 코드와 같이 트랜잭션 매니저를 통해서 직접 트랜잭션 개시, 커밋, 롤백 등을 수행하는 방법으로
소스코드에 직접 기술하기 때문에 가독성을 떨어뜨리고 실수할 가능성도 높아지므로 많이 사용하는 방식은 아닙니다.

내부 구현을 외부에 노출하고 싶지 않은 경우에 사용할 수 있습니다.

선언적 트랜잭션 관리 (Declarative)

트랜잭션에 관한 코드를 비즈니스 코드로부터 분리해서 비 침투적인 방법으로 기술하여 트랜잭션을 관리하는 방법입니다.

@Transactional 
public class SomeService() { 
    @Transactional 
    public void operateSome() {
    }
}

@Transactional 어노테이션을 사용할 경우에는 프로그램을 사용할 때와 같이 트랜잭션 매니저를 직접 지정하지 못하기 때문에,
transactionManager라는 속성을 통해서 사용할 트랜잭션 매니저를 지정할 수 있습니다.

AOP 설정을 이용하여 트랜잭션을 관리할 수도 있습니다.

@Transactional 어노테이션 이외의 방식은 거의 사용되지 않으므로 알고만 있으시면 좋을 것 같습니다.

Java 트랜잭션 rollback에 대해 많은 사람들이 잘못 알고 있는 부분이 있습니다.

Java에서 Checked Exception, UnChecked Exception이냐에 따라서 Rollback을 한다, 안 한다 라는 정보들이 돌아다니고 있더라고요.
Java에서 Checked Exception, UnChecked Exception이냐에 따라서 Rollback을 할 거냐 말 거냐는 개발자들이 정하는 겁니다.
Checked Exception, UnChecked Exception인가에 따라 rollback 한다는 규칙 같은 건 없어요.
Checked Exception, UnChecked Exception이냐에 따라서 Rollback을 한다 안 한다
이 말의 기원은 Spring의 트랜잭션 처리에서 나온 말입니다.

Spring의 트랜잭션 처리는 기본적으로 RuntimeException 계열은 rollback을 합니다.
Checked Exception은 rollback을 하지 않습니다.

한마디로 정리하면

컴파일 시점에 Check 하는 Exception이라 CheckedException

RollBack을 하지 않음

 

컴파일 시점에 UnCheck 하는 Exception이라 UnCheckedException (RuntimeException 상속을 받음)

RollBack

그러나 이건 어디까지나 기본적으로 설정된 동작이며, @Transactional 옵션으로 설정할 수 있게끔 지원해줍니다.

RuntimeException 중에서도 특정 예외는 rollback 하지 않게끔,
Checked Exception임에도 불구하고 특정 예외는 rollback을 하도록 설정할 수도 있습니다.

Java랑 Spring을 동일시 생각한다거나, Java에서 트랜잭션 처리를 Spring 트랜잭션 기본 전략으로만 처리한다면 위에 말이 맞을 수는 있겠네요.

Spring 트랜잭션 속성

속성 설명
propagation 트랜잭션 동작 도중 다른 트랜잭션을 호출할 때, 어떻게 할 것인지 지정하는 옵션
isolation 트랜잭션에서 일관성이 없는 데이터 허용수준을 설정
readOnly 트랜잭션을 읽기 전용으로 지정하는 속성
timeout 트랜잭션의 타임아웃(초단위)을 지정하는 속성
rollbackFor=Exception.class 특정 예외 발생시 rollback
noRollbackFor=Exception.class 특정 예외 발생 시 rollback하지 않음

propagation (전파 속성)

적용 방법

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

isolation (격리 레벨)

적용방법

@Transactional(isolation=Isolation.DEFAULT)
public void addUser(UserDTO dto) throws Exception {
    // 로직 구현
}
  • DEFAULT : 기본 격리 수준 (기본이며, DB의 isolation level을 따른다)
  • READ_UNCOMMITED (level 0) : 커밋되지 않는 데이터에 대한 읽기를 허용어떤 사용자가 A라는 데이터를 B라는 데이터로 변경하는 동안 다른 사용자는 B라는 아직 완료되지 않은(Uncommitted 혹은 Dirty) 데이터 B를 읽을 수 있다. (Dirty Read 발생)
  • READ_COMMITED (level 1) : 커밋된 데이터에 대해 읽기 허용어떠한 사용자가 A라는 데이터를 B라는 데이터로 변경하는 동안 다른 사용자는 해당 데이터에 접근할 수 없다. (Dirty Read 방지)
  • REPEATEABLE_READ (level 2) : 동일 필드에 대해 다중 접근 시 모두 동일한 결과를 보장트랜잭션이 완료될 때까지 SELECT 문장이 사용하는 모든 데이터에 shared lock이 걸리므로 다른 사용자는 그 영역에 해당되는 데이터에 대한 수정이 불가능하다.
    선행 트랜잭션이 읽은 데이터는 트랜잭션이 종료될 때까지 후행 트랜잭션이 갱신하거나 삭제가 불가능하기 때문에 같은 데이터를 두 번 쿼리 했을 때 일관성 있는 결과를 리턴한다. (Non-Repeatable Read 방지)
  • SERIALIZABLE (level 3) : 가장 높은 격리, 성능 저하의 우려가 있음데이터의 일관성 및 동시성을 위해 MVCC(Multi Version Concurrency Control)을 사용하지 않음.
    (MVCC는 다중 사용자 데이터베이스 성능을 위한 기술로 데이터 조회 시 LOCK을 사용하지 않고 데이터 버전을 관리해 데이터의 일관성 및 동시성을 높이는 기술)
    트랜잭션이 완료될 때까지 SELECT 문장이 사용하는 모든 데이터에 shared lock이 걸리므로 다른 사용자는 그 영역에 해당되는 데이터에 대한 수정 및 입력이 불가능하다. (Phantom Read 방지)

격리 수준에 따른 문제

  • Dirty Read
    : 트랜잭션 1이 수정 중인 데이터를 트랜잭션 2가 읽을 수 있다.
    만약 트랜잭션 1의 작업이 정상 커밋되지 않아 롤백되면,
    트랜잭션 2가 읽었던 데이터는 잘못된 데이터가 되는 것이다. (데이터 정합성에 어긋남)
  • Non-repeatable read
    : 트랜잭션 1이 회원 A를 조회 중에 트랜잭션 2가 회원 A의 정보를 수정하고 커밋한다면,
    트랜잭션 1이 다시 회원 A를 조회했을 때는 수정된 데이터가 조회된다. (이전 정보를 다시 조회할 수 없음)
    이처럼 반복해서 같은 데이터를 읽을 수 없는 경우이다.
  • Phantom read
    : 트랜잭션 1이 10살 이하의 회원을 조회했는데 트랜잭션 2가 5살 회원을 추가하고 커밋하면
    트랜잭션 1이 다시 10살 이하 회원을 조회했을 때 회원 한 명이 추가된 상태로 조회된다.
    이처럼 반복 조회 시 결과 집합이 달라지는 경우이다.
  • 트랜잭션 격리 수준의 필요성
    : 당연히 레벨이 높아질수록 데이터 무결성을 유지할 수 있다.
    하지만, 무조건적인 상위 레벨을 사용할 시 Locking으로 동시에 수행되는 많은 트랜잭션들이
    순차적으로 처리하게 되면서 DB의 성능은 떨어지게 되고 비용이 높아진다.
    그렇다고 Locking의 범위를 줄이게 되면 잘못된 값이 처리될 여지도 발생한다.
    그러므로 최대한 효율적인 방안을 찾아 상황에 맞게 사용하는 것이 중요하다.

readOnly (읽기 전용)

true시 insert, update, delete 실행 시 예외 발생
Default = false

적용방법

@Transactional(readonly = true)
public void addUser(UserDTO dto) throws Exception {
    // 로직 구현
}

timeout (시간지정)

지정한 시간 내에 해당 메서드 수행이 완료되지 않을 경우 rollback 수행
-1일 경우 no timeout
Default = -1

적용방법

@Transactional(timeout=10)
public void addUser(UserDTO dto) throws Exception {
    // 로직 구현
}

rollbackFor (예외 추가)

특정 예외 발생 시 강제로 rollback

적용방법

@Transactional(rollbackFor=Exception.class)
public void addUser(UserDTO dto) throws Exception {
    // 로직 구현
}

noRollbackFor (예외 무시)

특정 예외 발생 시 rollback 처리하지 않음

적용방법

@Transactional(noRollbackFor=Exception.class)
public void addUser(UserDTO dto) throws Exception {
    // 로직 구현
}

댓글