트랜잭션

업데이트:

  • 트랜잭션은 데이터베이스 시스템에서 병행 제어 및 회복 작업 시 처리되는 작업의 논리적 단위이다. 하나의 트랜잭션은 Commit되거나 Rollback된다.

트랜잭션의 성질

Atomicity(원자성)

  • 트랜잭션의 연산은 데이터베이스에 모두 반영되든지 아니면 전혀 반영되지 않아야 한다.
  • 트랜잭션 내의 모든 명령은 반드시 완벽히 수행되어야 하며, 모두가 완벽히 수행되지 않고 어느 하나라도 오류가 발생하면 트랜잭션 전부가 취소되어야 한다.

Consistency(일관성)

  • 트랜잭션이 그 실행을 성공적으로 완료하면 언제나 일관성 있는 데이터베이스 상태로 변환한다.
  • 시스템이 가지고 있는 고정요소는 트랜잭션 수행 전과 트랜잭션 수행 완료 후의 상태가 같아야 한다.

Isolation(독립성,격리성)

  • 둘 이상의 트랜잭션이 동시에 병행 실행되는 경우 어느 하나의 트랜잭션 실행중에 다른 트랜잭션의 연산이 끼어들 수 없다.
  • 수행중인 트랜잭션은 완전히 완료될 때까지 다른 트랜잭션에서 수행 결과를 참조할 수 없다.

Durablility(영속성,지속성)

  • 성공적으로 완료된 트랜잭션의 결과는 시스템이 고장나더라도 영구적으로 반영되어야 한다.

Spring에서 제공하는 트랜잭션 기능

  1. 트랜잭션 동기화
  2. 트랜잭션 추상화
  3. AOP를 이용한 트랜잭션 분리

트랜잭션 동기화

JDBC를 사용할 때 모든 커넥션을 하나의 트랜잭션으로 관리하기 위한 작업을 직접 수행한다면 불필요한 작업들이 많이 생겨 번거로울 것이다. Spring에서는 이러한 문제를 해결하고자 트랜잭션 동기화(Transaction Synchronization) 기술을 지원한다.

트랜잭션을 시작하기 위한 Connection 객체를 저장소에 보관해두고 필요할 때 꺼내쓸 수 있게 해주는 기술이다. 작업 쓰레드마다 Connection 객체를 따로 관리하기 때문에 멀티쓰레드 환경에서도 충돌이 발생하지 않는다.

하지만 이 기술은 JDBC에 종속적이기 때문에 이를 해결하기 위해 트랜잭션 추상화가 등장했다.

트랜잭션 추상화

Spring은 트랜잭션 추상화 기술을 제공하고 있다. 이를 이용함으로써 애플리케이션에 각 기술마다(JDBC, JPA 등) 일관되게 트랜잭션을 처리할 수 있도록 해주고 있다. Spring이 제공하는 트랜잭션 추상 인터페이스는 PlatformTransactionManager이다.

이제 우리는 사용하는 기술과 무관하게 PlatformTransactionManager를 통해 트랜잭션을 공유하고, 커밋하고, 롤백할 수 있게 되었다. 하지만 여전히 비즈니스 코드와 트랜잭션 코드가 결합되어 있다.

public void updateMember(Member member) {
    TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
    
    try {
        if (validateMember(member)) {
            memberRepository.save(member);
        }
        this.transactionManager.commit(status);
    } catch (Exception e) {
        this.transactionManager.rollback(status);
        throw e;
    }
}

이를 없애기 위해 스프링에서는 트랜잭션 부분을 분리하는 기능을 제공해준다.

AOP를 통한 트랜잭션 분리

트랜잭션 부분을 분리하기 위해 AOP(Aspect Oriented Programming)를 고안 및 적용하게 되었고, 이를 사용하기 위해 트랜잭션 어노테이션@Transactional을 지원해준다.

트랜잭션의 세부 설정

@Transactional

선언적 트랜잭션으로 선언한 클래스나 메서드에 트랜잭션 기능이 적용된 프록시 객체가 생성된다. 이 프록시 객체는 @Transactional이 포함된 메소드가 호출될 경우 PlatformTransactionManager를 통해 트랜잭션을 시작하고, 정상 여부에 따라 Commit 또는 Rollback 한다.

어노테이션 내부를 살펴보면 여러 속성들이 있다. 이 속성들을 통해 트랜잭션을 세부적으로 이용할 수 있게 해준다.

  • propagation
  • isolation
  • timeout
  • readOnly

트랜잭션 전파(propagation)

전파 속성 설명
Required(default) 모든 트랜잭션 매니저가 지원한다. 미리 시작된 트랜잭션이 있으면 참여하고 없으면 새로 시작한다.
Supports 이미 시작된 트랜잭션이 있으면 참여하고 없으면 트랜잭션 없이 진행한다.
Mandatory 이미 시작된 트랜잭션이 있으면 참여하고 없으면 예외를 뱉는다.
Requires_new 항상 새로운 트랜잭션을 시작한다. 이미 시작된 트랜잭션이 있으면 트랜잭션을 잠시 보류한다.
Nested 이미 진행중인 트랜잭션이 있으면 중첩 트랜잭션을 시작한다. 중첩 트랜잭션이란 트랜잭션 안에 트랜잭션을 만드는 것으로, 먼저 시작된 부모 트랜잭션의 커밋과 롤백에는 영향을 받지만 자신의 커밋과 롤백은 부모 트랜잭션에게 영향을 주지 않는다.
Never 이미 진행중인 트랜잭션이 있으면 예외를 발생시키며 트랜잭션을 사용하지 않도록 강제한다.
Not_supported 이미 진행중인 트랜잭션이 있으면 이를 보류하고 트랜잭션을 사용하지 않도록 한다.

실제로 진행이 되는지 궁금해서 테스트를 진행했다. 내용은 여기

격리 수준(isolation)

격리 수준 설명
Default 데이터 액세스 기술, DB 드라이버의 디폴트 설정을 따른다.
Read_uncommited 가장 낮은 격리 수준으로서 하나의 트랜잭션이 커밋되기 전에 그 변화가 다른 트랜잭션에 노출된다.
Read_commited 가장 많이 사용된다. Spring에서의 Default이다.다른 트랜잭션이 커밋하지 않은 정보는 읽을 수 없지만, 하나의 트랜잭션이 읽은 로우를 다른 트랜잭션이 수정할 수 있다.
Repeatable_read 하나의 트랜잭션이 읽은 로우를 다른 트랜잭션이 수정할 수 없도록 막아준다. 하지만 새로운 로우를 추가하는 것은 막지 않는다.
Serializable 가장 강력한 격리 수준으로, 트랜잭션을 순차적으로 진행한다.

모든 DB 트랜잭션은 격리수준을 가지고 있어야 한다. 서버에서는 여러 개의 트랜잭션이 동시에 진행될 수 있는데, 모든 트랜잭션을 독립적으로 만들고 순차 진행 한다면 안전하겠지만 성능이 크게 떨어질 수 밖에 없다.

따라서 적절하게 격리수준을 조정해서 가능한 많은 트랜잭션을 동시에 진행시키면서 문제가 발생하지 않도록 제어해야 한다. 이는 JDBC 드라이버나 DataSource 등에서 재설정할 수 있고, 트랜잭션 단위로 격리 수준을 조정할 수도 있다.

DefaultTransactionDefinition에 설정된 격리수준은 ISOLATION_DEFAULT로 DataSource에 정의된 격리 수준을 따른다는 것이다. 기본적으로는 DB나 DataSource에 설정된 기본 격리 수준을 따르는 것이 좋지만, 특별한 작업을 수행하는 메소드라면 독자적으로 지정해줄 필요가 있다.

제한시간(timeout)

timeout 속성을 이용해 트랜잭션에 제한 시간을 지정할 수 있다.

읽기전용(readOnly)

readOnly=true 속성을 주게 되면 스프링 프레임워크가 하이버네이트 세션의 플러시 모드를 Manual로 설정한다.

이렇게 하면 강제로 플러시를 호출하지 않는 한 플러시가 일어나지 않는다. 따라서 트랜잭션을 커밋하더라도 영속성 컨텍스트가 플러시 되지 않아 엔티티의 등록, 수정, 삭제가 동작하지 않는다.

플러시가 일어나지 않기 때문에 Dirty Checking을 할 필요가 없고, 그러므로 Snapshot을 보관하지 않아 성능이 향상된다.

DB가 master와 slave로 나누어져 있는 경우에 readOnly 속성을 주면 읽기 전용으로 slave를 호출하게 된다.

참고

https://mangkyu.tistory.com/154

https://cupeanimus.tistory.com/90

태그: ,

업데이트:

댓글남기기