트랜잭션

김성혁·2022년 6월 2일
0

트랜잭션이란?

데이터베이스의 상태를 변화시키는 작업의 단위를 트랜잭션이라고 한다.

트랜잭션의 성질(ACID)


원자성(Atomicity)

  • 한 트랜잭션 내에서 실행되는 작업들은 하나의 단위로 처리된다.
  • 한 트랜잭션 내에서 실행한 작업들은 모두 성공하거나, 반대로 전부 실패되는 성질

일관성(Consistency)

  • 데이터베이스의 상태가 일관되어야 한다.
  • 다시 말해, 트랜잭션이 일어난 이후의 데이터베이스는 데이터베이스의 제약이나 규칙을 만족해야 한다는 뜻

고립성(Isolation) → Isolation level의 default 값을 알고 계시나요?

  • 모든 트랜잭션은 다른 트랜잭션으로부터 독립되어야 한다.
  • 트랜잭션이 수행되고 있을 때, 다른 트랜잭션의 연산작업이 중간에 끼어들어 기존 작업에 영향을 주지 못하도록 하는 것
  • 동시성과 관련된 성능 이슈로 인해 트랜잭션 격리 수준을 선택

지속성(Durability)

  • 트랜잭션 성공 후 데이터베이스에 반영된 것은 영구히 반영되어있어야 한다는 것을 의미한다.
  • 시스템에 문제가 발생하거나 종료되더라도 데이터베이스에 반영된 값은 그대로 유지되어야 한다.

선언적 트랜잭션

스프링에서 어노테이션 방식(@Transactional )으로 선언적 트랜잭션 처리를 지원한다.

트랜잭션 적용 범위에서 트랜잭션 기능이 포함된 프록시 객체가 생성되어 자동으로 commit, rollback을 해준다.

@Transactional 옵션

  • isolation
    • 동시에 여러 트랜잭션이 처리될 때, 트랜잭션끼리 얼마나 고립되어 있는지를 나타내는 것
  • propagation
    • 트랜잭션 동작 도중 다른 트랜잭션을 호출할 때, 어떻게 할 것인지 지정하는 옵션이다.
  • noRollbackFor
    • 특정 예외 발생 시 rollback하지 않는다.
  • rollbackFor
    • 특정 예외 발생 시 rollback한다.
  • timeout
    • 지정한 시간 내에 메서드 수행이 완료되지 않으면 rollback한다. (-1일 경우 timeout을 사용하지 않는다)
  • readOnly
    - 트랜잭션을 읽기 전용으로 설정한다.

propagation


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

isolation

  • Default(기본 격리 수준) DB의 isolation level을 따른다. 대부분의 RDB에서 기본적으로 사용되고 있는 격리 수준은 READ_COMMITED

  • READ_COMMITED (level 0) - 커밋되기 전 데이터에 대한 읽기 허용 가장 낮은 격리 수준으로, 각 트랜잭션에서의 변경 내용이 커밋이나 롤백 여부에 상관없이 다른 트랜잭션에서 값을 읽을 수 있다.
    • 어떤 문제가 있을까? Dirty READ (트랜잭션 작업이 완료되지 않았지만 다른 트랜잭션이 값을 읽을 수 있는 현상) 트랜잭션 1의 작업이 완료되지 않았는데 트랜잭션 2에서 읽을 수 있다. 만약 트랜잭션 1의 작업이 롤백되면, 트랜잭션 2가 읽었던 데이터는 잘못된 데이터가 되는 것. 데이터의 정합성이 깨짐.
      1. A 트랜잭션에서 10번 사원의 나이를 27살에서 28살로 바꿈

      2. 아직 커밋하지 않음

      3. B 트랜잭션에서 10번 사원의 나이를 조회함

      4. 28살이 조회됨

        이를 더티 리드(Dirty Read)라고 한다

      5. A 트랜잭션에서 문제가 발생해 ROLLBACK

      6. B 트랜잭션은 10번 사원이 여전히 28살이라고 생각하고 로직을 수행함


  • READ_COMMITED (level 1) - 커밋된 데이터에 대한 읽기 허용 어떤 트랜잭션의 변경 내용이 커밋 되어야만 다른 트랜잭션에서 읽을 수 있도록 허용한다. Dirty READ가 발생하지 않도록 보장
    1. B 트랜잭션에서 10번 사원의 나이를 조회

    2. 27살이 조회됨(Undo 영역에 백업된 데이터를 조회)

    3. A 트랜잭션에서 10번 사원의 나이를 27살에서 28살로 바꾸고 커밋

    4. B 트랜잭션에서 10번 사원의 나이를 다시 조회(변경되지 않은 이름이 조회됨)

    5. 28살이 조회됨

      정합성 문제가 해결된 것처럼 보이지만, 하나의 트랜잭션내에서 똑같은 SELECT를 수행했을 경우 항상 같은 결과를 반환해야 한다는 REPEATABLE READ 정합성에 어긋나는 것이다.


  • REPEATABLE_READ (level 2)

    트랜잭션이 시작되기 전에 커밋된 내용에 대해서만 조회할 수 있는 격리수준

    MYSQL에서 기본으로 사용하고 있고, 해당 격리수준에서는 NON-REPEATABLE READ 부정합이 발생하지 않는다.

    1. 10번 트랜잭션이 500000번 사원의 이름을 조회
    2. 12번 트랜잭션이 500000번 사원의 이름 변경하고 커밋
    3. 10번 트랜잭션이 50000번 사원을 다시 조회
    4. Undo 영역에 백업된 데이터 반환

MySQL에서는 트랜잭션마다 트랜잭션 ID를 부여하여 트랜잭션 ID보다 작은 트랜잭션 번호에서 변경한 것만 읽게 된다. Undo 영역에 백업된 모든 레코드는 변경을 발생시킨 트랜잭션의 포함되어 있다.

REPEATABLE READ에서 발생할 수 있는 데이터 부정합

  1. UPDATE 부정합

    START TRANSACTION; -- transaction id : 1
    SELECT * FROM Member WHERE name='junyoung';
    
        START TRANSACTION; -- transaction id : 2
        SELECT * FROM Member WHERE name = 'junyoung';
        UPDATE Member SET name = 'joont' WHERE name = 'junyoung';
        COMMIT;
    
    UPDATE Member SET name = 'zion.t' WHERE name = 'junyoung'; -- 0 row(s) affected
    COMMIT;

이 상황에서 최종 결과는 name = joont 가 된다.

REPETABLE READ이기 때문에,

2번 트랜잭션에서 name = joont로 변경하고 COMMIT을 하면 name = junyoung의 내용을 Undo 영역에 남겨놔야 한다.

그래야 1번 트랜잭션에서 일관되게 데이터를 보는 것을 보장해줄 수 있기 때문이다.

이 상황에서 아래 구문에서 UPDATE 문을 실행하게 되는데, UPDATE의 경우 변경을 수행할 로우에 대해 잠금이 필요하다

하지만 현재 1번 트랜잭션이 바라보고 있는 name = junyoung의 경우 레코드 데이터가 아닌 Undo 영역의 데이터이고, Undo 영역에 있는 데이터에 대해서는 쓰기 잠금을 걸 수가 없다.

그러므로 위의 UPDATE 구문은 레코드에 대해 쓰기 잠금을 시도하려고 하지만 name = junyoung인 레코드는 존재하지 않으므로, 0 row(s) affected가 출력되고, 아무 변경도 일어나지 않게 된다.

그러므로 최종적으로 결과는 name = joont가 된다. 자이언티가 되지 못해 아쉽다.

  1. Phantom READ

    한 트랜잭션 내에서 같은 쿼리를 두 번 실행했는데, 첫 번째 쿼리에서 없던 유령(Phantom) 레코드가 두 번째 쿼리에서 나타나는 현상을 말한다.

    REPETABLE READ 이하에서만 발생하고(SERIALIZABLE은 발생하지 않음), INSERT에 대해서만 발생한다.


  • SERIALIZABLE (level 3)
    • 가장 단순한 격리 수준이지만 가장 엄격한 격리 수준
    • InnoDB에서 기본적으로 순수한 SELECT 작업은 아무런 잠금을 걸지않고 동작하는데, 격리수준이 SERIALIZABLE일 경우 읽기 작업에도 공유 잠금을 설정하게 되고, 이러면 동시에 다른 트랜잭션에서 이 레코드를 변경하지 못하게 된다.
    • 동시처리 능력이 다른 격리수준보다 떨어지고, 성능저하가 발생

noRollbackFor

런타임 예외가 발생해도 지정한 런타임 예외면 커밋을 진행한다.

rollbackFor

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

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

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

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

timeout

지정한 시간 내에 메서드 수행이 완료되지 않으면 rollback한다. (-1일 경우 timeout을 사용하지 않는다)

readOnly

트랜잭션을 읽기 전용으로 설정한다. 특정 트랜잭션 안에서 쓰기 작업이 일어나는 것을 의도적으로 방지하기 위해 사용하거나 조회 기능만 감겨두어 조회 속도가 개선됨. insert, update, delete 작업이 진행되면 예외가 발생한다.

0개의 댓글