스프링에서의 DB Transaction 처리

ssongkim·2022년 1월 22일
1
post-thumbnail

Transaction

트랜잭션이란 데이터베이스의 상태를 변화시키기 위해 수행하는 작업의 단위이다. 트랜잭션이 일어난 후에는 commit을 통해 변화를 반영하거나 rollback이 이루어진다.

트랜잭션의 특징

이러한 트랜잭션은 4가지 특징을 가지고 있다. Atomicity, Consistency, Isolation, Durability의 앞글자를 따서 ACID라고 부른다.

원자성 ( Atomicity )

하나의 트랜잭션이 작업이 그중에 일부분만 실행되거나 중단되지 않는것을 보장해주는 것을 말한다.
All or Noting, 하나의 트랜잭션 즉 작업단위에 대해서 전체 성공 혹은 전체 실패만을 보장하며 데이터베이스의 부분적인 갱신으로 더 큰 문제가 야기되는 것을 방지한다.

일관성 ( Consistency )

트랜잭션이 작업이 성공적으로 완료가 되더라도 작업 이전과 같은 일관성 있는 데이터베이스 상태를 유지하는 것을 의미한다.

예를들어 정수 타입의 컬럼에 문자열 값이 들어가 이전과 다른 상태를 가지지 않는 것을 보장한다. 데이터는 미리 정의된 정책에 대해서만 수정이 가능하며 무결성 원칙이 지켜지지 않는 작업은 바로 중단된다.

고립성 ( Isolation )

Transaction 작업이 수행되고 있을 때 다른 트랜잭션이 끼어들지 못하도록 보장해주는 것을 말한다. 이것은 트랜잭션 밖에 있는 어떤 연산도 중간 단계의 데이터를 볼 수 없음을 의미한다.
원칙적으로는 트랜잭션이끼리는 서로 간섭을 할 수 없어야 하지만 성능 이슈들이 많아 가장 유연하게 설정이 가능한 제약 조건이다.

지속성 ( Durability )

성공적으로 수행된 트랜잭션에 대해서 영구히(Persistent) 반영되어야 함을 말한다.
작업이 완료되어 COMMIT까지 된 작업은 시스템 문제가 발생하거나 DB 일관성 체크등을 하더라도 영구적으로 유지 되어야 한다.

트랜잭션의 경계(시작과 종료)


스프링 프레임워크에서 트랜잭션의 경계는 프레젠테이션 층과 비즈니스 로직층 사이에 있다.

전파 속성에 따라 하나의 요청에 트랜잭션이 여러개 생성될 수 있으나 결국 프레젠테이션 층으로 넘어올 때쯤 모든 트랜잭션은 종료될 것이다.

비즈니스 로직층에서는 비즈니스 로직과 데이터액세스로 구성되는데 트랜잭션이 종료되면서 변화 사항을 데이터베이스에 반영하는 commit을 하거나 로직과 액세스 하나라도 잘못되면 변화사항이 없었던 일이 되도록 rollback한다.
컨트롤러에서 서비스의 메서드를 실행하면 트랜잭션이 시작되고 메서드 실행을 종료하고 컨트롤러로 돌아갈 때 트랜잭션 종료가 이루어지도록 설계한다.

트랜잭션 처리 구현


두가지 트랜잭션 처리 구현 방법이 존재한다. 비즈니스 로직 안에서 트랜잭션의 시작과 끝을 소스코드로 명시하는 명시적인 트랜잭션AOP를 활용하여 스프링이 제공하는 기능을 사용하는 선언적 트랜잭션이다.

AOP는 프록시 패턴으로 서비스의 메서드가 호출될 때 @Before 영역이 호출되며 트랜잭션이 시작되고 메서드가 끝날 때 @After 영역이 호출되며 커밋이 이루어지도록 한다.

AOP를 이용한 트랜잭션 처리 시 @Transactionl 어노테이션을 활용한 선언적 트랜잭션, Bean 정의 파일에 의한 선언적 트랜잭션 처리 구현 방법이 있으며 보통 어노테이션을 활용한 선언적 트랜잭션을 사용한다.

Transaction Manager

트랜잭션의 시작(start)과 종료(commit, rollback)와 트랜잭션의 정의정보를 설정하는 역할을 한다.

트랜잭션 정의 정보

트랜잭션의 정의정보는 메서드 단위로 설정한다. 정의 정보는 다음 5가지로 구성되어 있다.

1. 전파 속성
2. 독립성 수준
3. 시간 만료
4. 읽기전용 상태
5. 롤백/커밋 대상 예외

전파(propagation) 속성

@Service
public class Service1 {
    @Transactional
    public void something() {
    	...
        ...
    }
}
@Service
public class Service2 {
    @Autowired
    private Service1 service1;
    
    @Transactional
    public void anything() {
    	...
        service1.somthing();
        ...
    }
}

전파 속성은 어떤 트랜잭션이 동작중인 과정에서 다른 트랜잭션을 실행할 경우 '실행된 트랜잭션을 어떻게 처리하는가'에 대한 개념이다. 여기서는 anything()메서드 내에서 somthing()이 실행된 경우이다.
anything()이 부모 트랜잭션이고 something()이 자식 트랜잭션일 것이다.

7가지 설정 옵션이 있다.

  • REQUIRED (Defualt option)

    이미 진행중인 트랜잭션(부모 트랜잭션)이 있다면 해당 트랜잭션에 참여하고, 진행중이 아니라면 새로운 트랜잭션을 생성한다.

    중간에 롤백이 발생한다면 모두 하나의 트랜잭션이기 때문에 진행사항이 모두 롤백된다.

  • REQUIRES_NEW

    항상 새로운 트랜잭션을 생성한다. 이미 진행중인 트랜잭션(부모 트랜잭션)이 있다면 부모 트랜잭션을 잠깐 중지하고 해당 트랜잭션 작업을 먼저 진행한다.

    각각의 트랜잭션이 롤백되더라도 서로 영향을 주지 않는다.

  • NESTED

    진행중인 트랜잭션(부모 트랜잭션)이 있다면 중첩 트랜잭션을 생성한다, 존재하지 않으면 REQUIRED와 동일하게 실행된다.

    중첩된 트랜잭션 내부에서 롤백 발생시 해당 중첩 트랜잭션의 시작 지점까지만 롤백이 이루어진다. 중첩 트랜잭션은 부모 트랜잭션이 커밋될 때 같이 커밋되며 부모 트랜잭션이 롤백되면 같이 롤백된다.

  • SUPPORTS
    이미 진행 중인 트랜잭션(부모 트랜잭션)이 있다면 해당 트랜잭션에 참여하고 부모 트랜잭션이 없다면 트랜잭션 없이 진행한다.

  • NOT_SUPPORTED
    이미 진행중인 트랜잭션(부모 트랜잭션)이 있든 없든 해당 메서드는 트랜잭션 없이 진행한다.

  • MANDATORY
    이미 진행중인 트랜잭션(부모 트랜잭션)이 있어야만 작업을 수행한다. 없다면 Exception을 발생시킨다.

  • NEVER
    트랜잭션이 진행중이지 않을 때(부모 트랜잭션이 없으면) 작업을 수행한다. 부모 트랜잭션이 있다면 Exception을 발생시킨다.

독립(고립)성 수준

여러 트랜잭션이 병행되어 실행될 때 각 트랜잭션의 독립성을 결정한다
즉 해당 트랜잭션이 다른 트랜잭션이 변경한 데이터를 볼 수 있도록 할지말지 결정한다.


모든 트랜잭션은 격리 수준을 갖고 있어야 한다. 다수의 클라이언트로부터 여러 요청이 날라오므로 서버에서는 여러개의 트랜잭션이 동시에 진행될 것이다. 가능하다면 트랜잭션이 순차적으로 실행돼서 다른 트랜잭션의 작업에 독립적인 것이 좋지만, 그렇게 하면 성능이 크게 떨어질 것이다.

따라서 각 트랜잭션마다 적절한 격리 수준을 설정해 가능한 많은 트랜잭션을 동시에 실행시키면서 문제가 발생하지 않게 하는 제어가 필요하다.

  • 트랜잭션이 DB를 다루는 동안 다른 트랜잭션이 관여하지 못하게 막는 Locking이라는 개념이 존재한다.
  • 무조건적인 Locking은 트랜잭션이 순서대로 처리되며, 이는 DB 성능을 저하시킨다. 반대로, 응답성을 높이기 위해 Locking범위를 줄인다면 잘못된 값이 처리될 여지가 있다.

MVCC

MVCC(Multi-Version Concurrency Control)는 동시성을 제어하여 데이터의 일관성을 유지하기 위한 방법이다.
MySQL InnoDB 스토리지 엔진에서 MVCC를 구현하기 위해 스냅샷, Undo 로그 등의 개념을 사용한다.

  • 멀티 버전이라 함은 하나의 레코드에 대해 여러 개의 버전이 동시 에 관리된다는 의미이다.
  • MVCC의 가장 큰 목적은 잠금을 사용하지 않는 일관된 읽기를 제공하는 데 있다.

snapshot

각 트랜잭션은 시작 시점에 현재 데이터의 스냅샷을 생성하고, 해당 스냅샷으로부터 읽기 작업을 수행한다. 즉, 트랜잭션 시작 시점의 스냅샷으로부터 데이터를 읽으며, 이후에 수행되는 다른 트랜잭션이 데이터를 수정하더라도 해당 트랜잭션에서는 수정 전의 데이터를 읽게 된다.

스냅샷은 트랜잭션 단위로 생성되며, 해당 트랜잭션에서만 유효하다. 이를 통해, 트랜잭션 간에 데이터 일관성을 유지하면서도 동시성을 높일 수 있다.

스냅샷은 MVCC와 함께 사용되는 다른 기능 중 하나인 Undo 로그를 통해 구현된다.

undo log

Undo 로그는 데이터를 변경 했을때 변경 전의 데이터를 보관(백업)하는 곳이다.
이러한 변경 작업은 트랜잭션이 완료되거나 롤백될 때까지 로그에 기록된다. 롤백이나 트랜잭션 실행 도중 문제가 발생하여 복구 작업을 수행할 경우, Undo 로그의 내용을 사용하여 이전 상태로 되돌린다.

Undo 로그는 메모리상에서 관리되는 것이 아니라, 디스크에 기록된다.

고립(ioslation) 레벨

DEFAULT

데이터베이스의 기본 독립성 수준 이용

mySQL의 경우 default로 REPEATABLE_READ를 사용하고 오라클에서는 READ_COMMITTED를 사용한다고 한다.

READ_UNCOMMITTED

아직 커밋하지 않은 변경 데이터도 읽을 수 있음, Dirty Read 데이터 모순 발생 가능

READ_COMMITTED

READ COMMITTED 격리 수준에서는 커밋된 데이터만 바라볼 수 있다. 커밋되지 않은 변경된 데이터는 바라보지 않고 최초 select 시점의 undo 로그를 바라보고 있는다.

그러다 다른 트랜잭션에서 변경한 데이터가 커밋되면 현 트랜잭션에서 조회할 때 값이 바뀌어 Non-repeatable Read 발생 가능

REPEATABLE_READ

트랜잭션이 시작되고 종료되기 전까지 한 번 조회한 row는 계속 같은 값이 조회된다.

READ_COMMITTED과는 다르게 트랜잭션이 시작된 시점의 undo log를 트랜잭션이 종료될 때까지 계속 바라봄으로써 같은값을 조회한다. -> Non-repeatable Read 발생하지 않음

SERIALIZABLE

가장 강력한 격리수준 성능이 가장 떨어짐, 한 트랜잭션에서 조회 시 공유락을 걸어 다른 트랜잭션에서 수정하지 못하도록 한다. 동시에 수정하려고 할 경우 순차적으로 트랜잭션을 실행한다.

mysql은 기본적으로 insert, update delete 수행 시 해당 튜플에 대해 x 락을 획득하고 작업한다. SERIALIZABLE를 쓰면 READ 시에도 락을 사용하는 것이다.

READ_UNCOMMITTED에서 SERIALIZABLE로 내려올수록 고립 수준이 높아지며 성능이 나빠진다고 할 수 있다.

데이터 모순 발생 가능성

독립성 수준(isolation levelDirty ReadNon-repeatable ReadPhantom Read
READ_UNCOMMITTEDOOO
READ_COMMITTEDXOO
REPEATABLE_READXXO
SERIALIZABLEXXX

Dirty Read

커밋되지 않은 변화사항을 읽을 수 있어 발생하는 문제로 한 트랜잭션(T1)이 데이터에 접근하여 값을 'A'에서 'B'로 변경했고 아직 커밋을 하지 않았을때, 다른 트랜잭션(T2)이 해당 데이터를 Read 하면 T2가 읽은 데이터는 B가 될 것이다. 하지만 T1이 최종 커밋을 하지 않고 종료된다면, T2가 가진 데이터는 꼬이게 된다.

Non-repeatable Read

하나의 트랜잭션내에서 똑같은 SELECT 쿼리를 실행했을 때 row는 항상 같은 결과를 가져와야 하는 REPEATABLE READ의 정합성(무모순성)에 어긋난다.

READ_COMMITTED와 그보다 낮은 고립 레벨에서 발생할 수 있는 문제이다.
트랜잭션-bob이 실행되고 있는 상태에서 트랜잭션-alice가 update, commit하자 아직 끝나지 않는 트랜잭션-bob가 다시 테이블 값을 읽었을 때 값이 변경됨을 알 수 있다.
이러한 문제는 주로 입금, 출금 처리가 진행되는 금전적인 처리에서 주로 발생한다.
데이터의 정합성은 깨지고, 버그는 찾기 어려워 진다.
이를 방지하고자 한다면 REPEATABLE_READ를 사용한다.

Phantom Read


REPEATABLE_READ와 그보다 낮은 고립 레벨에서 발생할 수 있는 문제이다.
내가 조회한 자료 값을 내가 바꾸지 않는 이상 트랜잭션 내에서 언제나 조회해도 항상 같은 값을 제공한다는 repeatable read 격리 수준에서 그 조회한 값이 만일 어떤 범위라면, 그 범위 전체를 보관하지 못해서 발생하는 문제로 다른 트랜잭션에서 수행한 변경 작업에 의해 레코드가 보였다가 안 보였다가 하는 현상이다.
이를 방지하기 위해서는 SERIALIZABLE를 사용한다.

mysql이 사용하는 innoDB에서는 독특한 특성 때문에 REPEATABLE_READ에서도 팬텀리드가 발생하지 않는다고 한다. (키워드: 넥스트 키 락)

Non-repeatable Read와 Phantom Read의 공통점과 차이점

  • 공통점
    Non-Repeatable ReadPhantom Read는 커밋된 데이터를 읽어들이면서 발생했다는 공통점이 있다.
  • 차이점
    Non-Repeatable Read는 다른 트랜잭션의 UPDATE 쿼리 후 커밋된 데이터를 읽어서 발생하는 현상(row 범위)이고
    Phantom Read는 다른 트랜잭션의 INSERT 쿼리 후 커밋된 데이터를 읽어서 발생한 현상(scope 범위)이다.

시간 만료

트랜잭션을 수행하는 제한시간(timeout)을 설정할 수 있다.
@Transactional(timeout=10) 처럼 사용할 수 있으며, 지정된 시간 내에 메소드 수행이 완료되지 않으면 롤백된다. 이는 트랜잭션을 직접 시작할 수 있는 PROPAGATION_REQUIRED 또는 PROPAGATION_REQUIRES_NEW와 함께 사용해야만 의미가 있다.

읽기전용 상태

트랜잭션 내의 처리에 대한 읽기전용 여부를 설정한다.
읽기 전용(read only)로 설정해두면 트랜잭션 내에서 데이터를 조작하는 시도를 막아줄 수 있다. 또한 데이터 액세스 기술에 따라서 성능이 향상될 수도 있다. 즉 성능 최적화 또는 쓰기 작업 방지로 사용된다.

일반적으로 읽기 전용 트랜잭션이 시작된 이후 INSERT,UPDATE,DELETE 같은 쓰기 작업이 진행되면 예외가 발생한다. 만약 클래스 레벨에 @Transactional을 선언한다면, 쓰기 작업을 하지 않는 메소드에는 일일이 readOnly 설정을 해줘야 성능 최적화를 할 수 있다.

롤백/커밋 대상 예외(Exception) 지정

스프링의 선언적 트랜잭션에서는 UncheckedException이 발생하면 롤백하고 CheckedException이 발생하였다면 롤백하지 않는다.

CheckedException이 발생할 때 롤백하지 않는 이유는 CheckedException은 처리를 명시해주어야 하기 때문에 복구가 가능하다고 판단하기 때문이다.

CheckedException은 대표적으로 IOException, SQLException가 있으며 UncheckedException은 대표적으로 NullPointerException, IllegalArgumentException가 있다.

이를 이용해 예외가 발생해도 롤백 대상이지만 커밋하거나, 커밋 대상이지만 롤백시키는 것이 가능하다.

우리는 롤백/커밋의 동작 방식을 변경할 수 있다.
rollbackFor 또는 rollbackForClassName으로 예외를 지정하여 특정 예외 발생 시 롤백을 시킬 수 있다.

noRollbackFor 또는 noRollbackForClassName으로 예외를 지정하여 특정 예외 발생 시 진행한 부분까지 커밋하며 롤백시키지 않는다.

RuntimeException

Unchecked ExceptionRuntimeException을 상속하고 Checked ExceptionRuntimeException을 상속하지 않는다.

@Transactional

위에서 지금까지 설명한 트랜잭션의 정의정보를 @Transactional어노테이션에서 설정하는 방법을 간결하게 설명한 좋은 게시글이 있다.
https://velog.io/@kdhyo/JavaTransactional-Annotation-%EC%95%8C%EA%B3%A0-%EC%93%B0%EC%9E%90-26her30h

참고자료

웹프로그래밍 수업 자료
https://1-7171771.tistory.com/133
https://velog.io/@lsb156/Transaction-Isolation-Level#read-uncommitted
https://velog.io/@kdhyo/JavaTransactional-Annotation-%EC%95%8C%EA%B3%A0-%EC%93%B0%EC%9E%90-26her30h
https://velog.io/@lsb156/Transaction-Isolation-Level

보면 좋은 것

https://developyo.tistory.com/51

lock
https://suhwan.dev/2019/06/09/transaction-isolation-level-and-lock/

https://www.letmecompile.com/mysql-innodb-lock-deadlock/

profile
鈍筆勝聰✍️

0개의 댓글