[정리] Transaction(트랜잭션)

Junseo Kim·2021년 5월 18일
2

📚 개념 정리

목록 보기
1/3
post-thumbnail

트랜잭션이란?

DB상태를 변환시키는 하나의 논리적 기능을 수행하기 위한 단위나 한 번에 수행되어야할 일련의 연산이다. 즉 하나의 작업을 위한 하나 이상의 db연산들의 모음이라고 볼 수 있으며 논리적인 작업 단위이다. 쪼개질 수 없는 하나의 단위로 보면된다.

트랜잭션 성질

  • 원자성 : 한 트랜잭션 내에서 실행한 연산들은 하나의 작업으로 본다. 즉, 모두 성공하거나 모두 실패한다.
  • 일관성 : 작업 처리 결과는 항상 일관성이 있어야한다. db상태를 일관성있게 유지한다.
  • 독립성 : 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않아야 한다.
  • 지속성 : 트랜잭션 작업이 성공하면 결과는 영구적으로 반영되어야한다.

한 트랜잭션 내에서 실행한 연산들은 모두 성공하면 commit되고, 하나라도 실패하면 성공한 연산이 있더라도 모두 rollback된다. 이런식으로 관리되어야 db가 모순이 없이 일관성을 유지할 수 있다.

트랜잭션이 진행중인 경우 db상태가 변경되더라도, 트랜잭션이 시작될 때의 db 상태로 트랜잭션이 진행되므로 일관성이 보장된다.

Spring에서 트랜잭션 처리하기

Spring에서는 @Transactional 애노테이션을 클래스나, 메서드에 붙여주면 트랜잭션 기능이 적용된 프록시 객체가 생성된다. 생성된 프록시 객체는 PlatformTransactionManager를 사용하여 트랜잭션을 시작하고, 동작여부에 따라 commit이나 rollback을 결정한다.

@Transactional은 클래스단이 아니라 CUD 메서드 단에 붙여주는 것이 좋다. db외에 다른 외부 자원을 사용하는 경우에 지연이 발생하면 db까지 영향이 갈수도 있고, 각 메서드마다 상황에 따라 트랜잭션을 어떻게 처리할지 달라질 수도 있기 때문이다.

CUD 뿐만아니라 Read인 경우에도 필요하다면 @Transactional을 붙여줄 수 있다. 이때 readOnly 옵션을 준다. 그렇지만 이때는 다른 이슈가 없는지 판단해보고 결정해야한다.

하나의 서비스 로직에서 다수의 외부 API를 호출하거나 하는 경우면 트랜잭션으로 묶지 않는 것이 좋다. 다수의 요청이 같은 트랜잭션으로 묶이면 동일한 DB Connection이 오랫동안 물려있어서 타임아웃 등의 이슈가 생길 수 있다.

하지만 나중에 JPA를 쓰거나 하면 어차피 CrudRepository 자체적으로 @Transactional을 가지고 있다.

Read Only

@Transactional에는 readOnly 옵션이 있다. 트랜잭션을 읽기 전용으로 설정하는 옵션이다. 이 옵션을 주면 해당 트랜잭션 내부에서 CUD 연산이 일어날 경우 예외를 던진다.

@Transactional(readOnly = true)
public Station findById(Long id) {
    // ...
}

But! JDBC를 구현한 벤더마다 readOnly 지원 여부가 다르다고한다.. h2에서는 CUD가 일어나도 예외를 던지지 않고, mysql 5.6.5 이후 버전에서는 CUD가 일어나면 예외를 던진다고 한다.

단순하게 조회만 하는 경우는 거의 없어서 readOnly자체가 많이 쓰이지는 않는다고 한다.

@Transactional이 작동하지 않는 경우!

1) 메서드의 접근제한자가 public이 아닌 경우 작동하지 않는다.

2) 한 클래스 내부에 @Transactional이 붙어있지 않은 메서드가 있는 경우, 해당 메서드에서 @Transactional이 붙어있는 다른 메서드를 호출한다해도 트랜잭션처리가 되지 않는다.

@Service
public class LineService {
    // ...
    
    public void method1() {
        method2();
    }
    
    @Transactional
    public void method2() { // 트랜잭션 처리 되지 않는다.
        // ...
    }
}

이 경우 호출하는 쪽(위의 경우 method1())에 @Transactional을 붙여주거나, 아에 method1과 method2를 별도의 클래스로 분리한다면 트랜잭션 처리가 잘 작동한다.

왜 이럴까? 🤔

@Transactional은 Proxy 기반이며 AOP로 구성되어 있다. 즉 method가 실행되기 전 트랜잭션을 묶는다. 스프링에서는 인스턴스에서 처음 호출하는 메서드나 클래스의 속성을 따라가게 되는데, 위의 예시에서는 method1가 method2의 상위 메서드이기 때문에 method1에 @Transactional이 없다면 method2에 선언되있더라도 전이되지 않는다.

격리 수준

다수의 트랜잭션이 동시에 실행되면 여러 문제가 발생할 수 있다.

  • Dirty Read : 트랜잭션 1이 수정중인(변경만 하고 commit하지 않은 상태) 내용을 트랜잭션 2가 read하는 경우, 트랜잭션1이 roll back되면 트랜잭션 2의 데이터는 잘못된 데이터가 된다

  • Non-repeatable Read : 트랜잭션 1이 어떤 데이터 A를 2번 조회하는 연산으로 이루어져있다고 하자. 트랜잭션 1이 A를 한 번 읽은 후, 트랜잭션 2가 A를 수정하고 commit 한다면, 트랜잭션 1이 다시 A를 읽을때 수정된 데이터가 조회된다.

  • phantom read : 트랜잭션 1이 어떤 데이터 A를 2번 조회하는 연산으로 이루어져있다고 하자. 트랜잭션 1이 A를 조회해서 [1, 2, 3, 4, 5]의 집합을 얻은 후, 트랜잭션 2가 A에 [6, 7]을 추가한다면, 트랜잭션 1이 다시 A를 조회하면 결과 집합이 달라진다.

@Transactional은 이런 상황을 방지할 수 있는 옵션이 존재한다.

Isolation

여러개의 트랜잭션이 동시에 데이터베이스에 접근했을때, 해당 트랜잭션들을 어떻게 처리할 것인지에 대한 설정(동시에 처리할 것인지, 하나 하나 처리할 것인지 등) 기본값은 default로 DB의 기본값을 따라간다.(대부분 READ_COMMITTED 방식이 default값이다.)

@Transactional(isolation = Isolation.REPEATABLE_READ)
public Station findById(Long id) {
    // ...
}
  • DEFAULT : 기본 격리 수준(db의 격리 수준을 따름)

  • READ UNCOMMITED : commit 되지 않은 데이터를 읽을 수 있게 허용해준다. Dirty Read, Non-repeatable read, phantom read 발생 가능

  • READ COMMITED : commit 된 데이터만 읽을 수 있게 허용해준다. Non-repeatable read, phantom read 발생 가능

  • REPEATABLE READ : 반복 가능한 읽기. 트랜잭션이 시작되기 전에 commit 된 내용에 대해서만 조회할 수 있는 격리 수준이다. 트랜잭션이 완료될 때까지 select 쿼리가 접근하는 모든 데이터에 shared lock이 걸린다.(수정할 수 없게 됨). phantom read 발생 가능

  • SERIALIZABLE : 직렬화 가능. 가장 강한 격리수준. 성능은 좋지 못하다. 트랜잭션이 완료될 때까지 select 쿼리가 접근하는 모든 데이터에 shared lock이 걸린다.

내부 트랜잭션

이런 경우가 있을 수 있다. Service 메서드에 @Transactional이 붙어있고, 해당 메서드 내부에서 다른 Service의 CUD를 수행하는 메서드를 호출할때 다른 Service의 CUD 메서드에는 @Transactional을 붙여야할까?

붙여주는 것이 좋다. 왜냐하면 해당 메서드가 미래에는 다른 곳에서 별개로 쓰일 수 도 있기 때문이다. 메서드에서 호출하는 다른 객체의 메서드에 트랜잭션이 있어도 스프링에서 트랜잭션을 어떻게 전파할지 결정하는 기능이 있다.

Propagation

트랜잭션을 어떻게 전파시킬 것인지에 관한 것이다. 예를 들어 @Transactional을 가지고 있는 메서드가 있고, 그 메서드 안에서 다른 메서드를 호출했는데 그 메서드도 @Transactional을 가지고있는 경우, 첫 번째 메서드가 가지고 있던 트랜잭션을 두 번째 메서드가 이어갈 것이냐, 두 번째 메서드 자체에서 트랜잭션을 만들어서 따로 사용할 것이냐를 결정하는 것이다.(nested transaction에 관한 내용)

@Transactional(propagation = Propagation.REQUIRED)
public Station findById(Long id) {
    // ...
}
  • REQUIRED : 부모 트랜잭션을 이어간다. 부모 트랜잭션이 없을 경우 새로운 트랜잭션을 생성한다.

  • REQUIRES_NEW : 부모 트랜잭션이 있던 말던, 무조건 새로운 트랜잭션이 생성된다.

  • SUPPORT : 부모 트랜잭션을 이어간다. 부모 트랜잭션이 없을 경우 non-transactional 하게 실행한다.

  • MANDATORY : 부모 트랜잭션을 이어간다. 부모 트랜잭션이 없을 경우 예외를 발생시킨다.

  • NOT_SUPPORT : 부모 트랜잭션이 있던 말던, non-transactional로 실행한다. 부모 트랜잭션이 있다면 부모 트랜잭션을 중지시킨다.

  • NEVER : 항상 non-transactional하게 실행된다. 부모 트랜잭션이 존재한다면 예외를 발생시킨다.

  • NESTED : 부모 트랜잭션에서 진행될 경우 별개로 commit되거나 roll back될 수 있다. 부모 트랜잭션이 없을 경우 REQUIRED처럼 동작한다.

테스트 클래스의 @Transactional

@Transactional은 테스트 클래스에 붙여줄 경우 프로덕션 코드와 조금 다르게 동작한다. 메서드마다 데이터를 roll back 해준다. 하지만 auto_increament 된 값은 롤백되지 않는다.

참고

(Spring/Spring Boot) transactional annotation 속성 격리 isolation
Spring Transaction 옵션
[spring] @Transactional 작동 안할때 확인해봐야 할 것
@Transactional(readOnly = true)
트랜잭션(Transaction)이란?
[DB]트랜잭션(Transaction)이란?/트랜잭션의 개념,특징, 연산과정/savepoint
[Spring] Transactional 정리 및 예제
Transaction Propagation and Isolation in Spring @Transactional

1개의 댓글

comment-user-thumbnail
2023년 5월 11일

It will definitely be widely disseminated.
snake games

답글 달기