Transaction

김하영·2021년 3월 15일
0

1. Transaction의 개념

트랜잭션(Transaction)이란?

트랜잭션은 데이터베이스의 상태를 변환 시키는 하나의 논리적 기능을 수행하기 위한 작업의 단위 또는 한꺼번에 모두 수행되어야 할 일련의 연산들을 의미하며 작업의 완전성을 보장해준다.

즉, 논리적인 작업 셋을 모두 완벽하게 처리하거나 또는 처리하지 못할 경우에는 원 상태로 복구해서 작업의 일부만 적용되는 현상이 발생하지 않게 만들어주는 기능이다.

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

  • 사용자가 시스템에 대한 서비스 요구 시 시스템이 응답하기 위한 상태 변환 과정의 작업 단위이다.

  • 하나의 트랜잭션은 Commit되거나 Rollback된다.

Commit / Rollback 연산

  • Commit 연산

한개의 논리적 단위(트랜잭션)에 대한 작업이 성공적으로 끝났고 데이터베이스가 다시 일관된 상태에 있을 때, 이 트랜잭션이 행한 갱신 연산이 완료된 것을 트랜잭션 관리자에게 알려주는 연산이다.

  • Rollback 연산

하나의 트랜잭션 처리가 비정상적으로 종료되어 데이터베이스의 일관성을 깨드렸을 대, 이 트랜잭션의 일부가 정상적으로 처리되었더라도 트랜잭션의 원자성을 구현하기 위해 이 트랜잭션이 행한 모든 연산을 취소(Undo)하는 연산이다.

Rollback 시에는 해당 트랜잭션을 재시작하거나 폐기한다.

트랜잭션의 특성

데이터의 무결성(Integrity)을 보장하기 위하여 DBMS의 트랜잭션이 가져야 할 특성이다.

트랜잭션의 상태

  1. Active : 트랜잭션의 활동 상태. 트랜잭션이 실행중이며 동작중인 상태를 말한다.

  2. Failed : 트랜잭션 실패 상태. 트랜잭션이 더이상 정상적으로 진행 할 수 없는 상태를 말한다.

  3. Partial Committed : 트랜잭션의 Commit 명령이 도착한 상태. 트랜잭션의 commit 이전sql문이 수행되고 commit만 남은 상태를 말한다.

  4. Committed : 트랜잭션 완료 상태. 트랜잭션이 정상적으로 완료된 상태를 말한다.

  • Partial Committed 와 Committed 의 차이점

Commit 요청이 들어오면 상태는 Partial Commited 상태가 된다. 이후 Commit을 문제없이 수행할 수 있으면 Committed 상태로 전이되고, 만약 오류가 발생하면 Failed 상태가 된다. 즉, Partial Commited는 Commit 요청이 들어왔을때를 말하며, Commited는 Commit을 정상적으로 완료한 상태를 말한다.

  • 트랜잭션을 사용할 때 주의할 점

트랜잭션은 꼭 필요한 최소의 코드에만 적용하는 것이 좋다.
즉 트랜잭션의 범위를 최소화하라는 의미다. 일반적으로 데이터베이스 커넥션은 개수가 제한적이다.
그런데 각 단위 프로그램이 커넥션을 소유하는 시간이 길어진다면 사용 가능한 여유 커넥션의 개수는 줄어들게 된다.
그러다 어느 순간에는 각 단위 프로그램에서 커넥션을 가져가기 위해 기다려야 하는 상황이 발생할 수도 있다.

2. Transaction 이 보장되지 않을 때 발생하는 문제

무결성 제약조건, 병행 제어, 복구의 기능이 제대로 동작하지 못한다.

1. 무결성 제약조건

  • 무결성이란?

데이터베이스에 저장된 데이터 값과 그것이 표현하는 현실 세계의 실제값이 일치하는 정확성을 의미한다.

  • 무결성을 유지하는 방법

대표적으로 사용되는 방법은 중앙 통제(트랜잭션)에 의한 데이터 갱신으로서, 이 방법은 검증 프로그램을 이용하여 모든 갱신 처리 과정에서 반드시 검증 단계를 거치도록 통제를 가한다.

검증 프로그램이 무결성을 검증하기 위해 무결성 규정을 사용한다.

  • 무결성의 종류

  • 무결성 제약조건

데이터베이스에 들어있는 데이터의 정확성(일관성)을 보장하기 위해 부정확한 자료가 데이터베이스 내에 저장되는 것을 방지하기 위한 제약 조건을 의미한다.

트랜잭션이 보장되지 않는다면 무결성 제약조건을 체크하지 않기 때문에 데이터의 일관성이 없어진다.

2. 병행제어(Concurrency Control)

병행실행시 Transaction들 간의 격리성(Isolation)을 유지하여 Transaction 수행에 문제가 발생하지않도록 제어한다.

  • 로킹(Locking)

하나의 Transaction이 사용하는 DB내의 data를 다른 Transaction이 접근하지 못하게 하나의 Transaction 실행될때는 Lock을 설정하여 다른 Transaction이 접근하지 못하도록 잠근후 실행, 실행 완료후 Unlock 처리한다.

  • 교착상태(Deadlock)

Lock상태가 오래 유지되어 다른 Transaction들이 더이상 진행하지 못하고 무한정 대기상태를 뜻한다.

  • 로킹의 접근허용(교착상태 해결법)

공유락(Shared Lock): 사용중인 data를 다른 Transaction이 읽기허용, 쓰기불허용으로 설정

베타락(Exclusive Lock): 사용중인 data를 다른 Transaction이 읽기, 쓰기 모두 불허용

  • 로킹단위

Table, 속성, 튜플 단위로 Lock을 설정할 수 있다.

단위가 크면 많은양의 data lock 가능하다.
단위가 크면 lock을 설정한 가짓수는 적어진다
단위가 크면 병행성 수준이 낮아진다

  • 2단계로킹기법(Two-Phase Locking Protocol)

Lock설정대상 date가 여러개인 경우 모든 data에 lock을 설정하는 단계와 완료후 Lock을 해제하는 단계

확장단계(Growing Phase): Lock을 설정하는 단계

축소단계(Shirinking Phase): Lock을 해제하는 단계

교착상태(Deadlock)이 발생할수있다

  • 타임스탬프(Time Stamp)

병행제어의 또다른 기법.
각 Transaction이 data에 접근할 시간을 미리 지정하여 그시간(Time Stamp)의 순서에 따라 순서대로 data에 접근하여 수행, 제한적인 시간이 존재하므로 교착상태가 발생하지 않는다.

트랜잭션이 보장되지 않는다면 병행제어가 불가능하기 때문에 아래와 같은 문제점이 발생한다.

  • 병행제어를 하지 않았을 경우의 문제점

3. 복구

트랜잭션의 버퍼 관리 정책에 따라서 REDO/UNDO 복구를 실행한다.

  1. UNDO : 롤백(RollBack)을 위해서 UNDO 테이블스페이스(Disk)에 기록

  2. REDO : 시스템 복구를 위해 시스템에 로그를 기록(redo log buffer)

트랜잭션이 보장되지 않는다면 복구가 되지않으면서 데이터의 원자성이 보장되지 않으며 처리가 완료되었다는 지속성도 불안정해진다.

3. 스프링에서 제공하는 Transaction 구현 방식

Spring(스프링)에서 트랜잭션(Transaction)을 관리하는 방법은 크게 2가지로 나눌 수 있다.

  1. 프로그램에 의한(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);
        }
    }
}

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

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

다음은 선언적 트랜잭션 관리이다.

선언적 트랜잭션 관리라는 말이 어려울 수 있지만 간단하게 설명하면 트랜잭션에 관한 코드를 비지니스 코드로 부터 분리해서 비침투적인 방법으로 기술하여 관리하는 방법을 의미한다.

  • 선언적 트랜잭션

  • @Transactional 어노테이션을 통한 트랜잭션

스프링에서는 xml 또는 Javaconfig를 통해 설정 할 수 있다.
Spring boot에서는 별도의 설정이 필요 없으며, 클래스 또는 메소드에 선언할 수 있다.

import com.example.bamdule.service.UserService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
//@Transactional(propagation = , isolation = ,noRollbackFor = ,readOnly = ,rollbackFor = ,timeout = )
public class UserServiceImpl implements UserService {
...
}

@Transactional을 클래스 단위 혹은 메서드 단위에 선언해주면 된다.

클래스에 선언하게 되면, 해당 클래스에 속하는 메서드에 공통적으로 적용된다.
메서드에 선언하게 되면, 해당 메서드에만 적용된다.

  • @Transactional 동작원리

트랜잭션은 어노테이션 기반 AOP를 통해 구현되어있다.

AOP ( Aspect Oriented Programming )
관점 지향 프로그래밍. 관점 지향은 쉽게 말해 어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나누어서 보고 그 관점을 기준으로 각각 모듈화하겠다는 것이다. 여기서 모듈화란 어떤 공통된 로직이나 기능을 하나의 단위로 묶는 것을 말한다.

예로들어 핵심적인 관점은 결국 우리가 적용하고자 하는 핵심 비즈니스 로직이 된다. 또한 부가적인 관점은 핵심 로직을 실행하기 위해서 행해지는 데이터베이스 연결, 로깅, 파일 입출력 등을 예로 들 수 있다.

AOP에서 각 관점을 기준으로 로직을 모듈화한다는 것은 코드들을 부분적으로 나누어서 모듈화하겠다는 의미다. 위와 같이 Aspect로 모듈화하고 핵심적인 비즈니스 로직에서 분리하여 재사용하겠다는 것이 AOP의 취지다.

따라서, 아래와 같은 특징이 있다

클래스, 메소드에 @Transactional이 선언되면 해당 클래스에 트랜잭션이 적용된 프록시 객체 생성

프록시 객체는 @Transactional이 포함된 메서드가 호출될 경우,
트랜잭션을 시작하고 Commit or Rollback을 수행

CheckedException or 예외가 없을 때는 Commit
UncheckedException이 발생하면 Rollback

  • @Transactional 모드

@Transactional은 Proxy Mode와 AspectJ Mode가 있는데 Proxy Mode가 Default로 설정되어있다.

Proxy Mode는 다음과 같은 경우 동작하지 않는다.

private method에서는 동작하지 않음

Service 객체를 Proxy를 통해서 얻어오고, 얻어온 객체를 접근할 때 Proxy에 의하여 Transaction이 시작되기 때문에 당연히 이런 상황에서는 Transaction이 적용되지 못한다.

Non-Public 메서드에 적용하고 싶으면 AspectJ Mode를 고려해야한다.

transaction이 적용되지 않은 public method 내부에서 transaction이 적용된 public method를 호출하는 경우, transaction이 동작하지 않는다.

readonly transaction이 적용된 public method 내부에서 not-readonly transaction이 적용된 public method를 호출하는 경우, 모든 method는 readonly transaction으로 동작하게 된다.

  • @Transactional 옵션

1. propagation

2. isolation

우선 알아 두어야 할 것은 격리 수준이 위에서부터 아래로 강해진다는 것이다.

즉, READ UNCOMMITTED가 가장 약한 격리 수준이고, SERIALIZABLE이 가장 강한 격리 수준이다.
여기서 약한 격리 수준이라는 것은 잠금의 강도가 약함을 의미한다.
따라서 약하게 잠기기 때문에 데이터 접근에 대한 유연성은 좋아지지만, 데이터의 일관성은 그만큼 떨어진다.

이 격리 수준에 대해 먼저 이야기하는 이유는 격리 수준에 따라 트랜잭션이 다르게 동작하기 때문이다.
트랜잭션 격리 수준을 설정하기 위해서는 아래와 같은 형식을 사용한다.


SET TRANSACTION ISOLATION LEVEL
{
    -- 아래 것들중 하나만 선택이 가능하다
    READ UNCOMMITTED
    READ COMMITTED
    REPEATABLE READ
    SNAPSHOT
    SERIALIZABLE
}

여러 사용자가 동시에 하나의 데이터에 접근할 때 발생하는 문제는 다음 세 가지로 분류할 수 있다.

  • Dirty read : 커밋되지 않은 데이터 읽기
  • Unrepeatable read : 반복되지 않은 데이터 읽기
  • Phantom read : 가상 읽기

지금부터 각각의 상황이 무엇인지,
그리고 이러한 상황에 대처하기 위해 어떠한 격리 수준을 사용해야 하는지에 대해 알아보도록 하자.

  1. Dirty read

"데이터 캐시에는 변경이 되었지만, 아직 디스크에는 변경되지 않은 데이터(페이지)"를 더티 페이지라고 한다.

이 더티 페이지를 읽는 행위를 Dirty read(더티 리드)라고 한다.

이 더티 리드의 문제점은 아직 커밋되지 않은 데이터를 읽어오기 때문에,
더티 페이지를 읽어온 후에 더티 페이지의 데이터가 롤백된다면 이미 읽어온 데이터는 잘못된 데이터가 된다는 점이다.

이 더티 리드는 동시성이 좋아서 커밋을 기다리지 않고 값을 읽어올 수 있는 장점은 있지만,
(즉, SELECT문이 TRAN LOCK을 대기하지 않도록 하여 응답성을 높일 수 있지만)
확실히 데이터의 일관성은 떨어지게 된다.

이를 수행하려면 아래와 같이 격리 수준을 READ UNCOMMITTED로 설정하여야 한다.

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED

트랜잭션이 변경한 임시 값(=더티 페이지)이 확실히 커밋될 것이라는 전제가 있을 때만 사용하여야 한다.

  1. Unrepeatable read

반복되지 않은 읽기(Unrepeatable read)란 트랜잭션 내에서 한 번 읽은 데이터가
트랜잭션이 끝나기 전에 변경되었다면, 다시 읽었을 때 새로운 값이 읽히는 것을 의미한다.

즉, 두번째 트랜잭션이 같은 행에 여러번 엑세스하며, 이때마다 다른 데이터를 읽을 경우를 생각하면 된다.

잠금은 트랜잭션이 발생하게 되면 걸리게 되는데,
SELECT를 수행할 경우에는 공유 잠금(Shared Lock)이 걸리고,
INSERT/UPDATE/DELETE를 수행할 경우에는 배타적 잠금(Exclusive Lock)이 걸리게 된다.

현재 SELECT에 의해 공유 잠금이 걸린 상태에서도
다른 사용자의 데이터의 접근을 꼭 막아야 하는 경우가 있다면, REPEATABLE READ 격리 수준을 사용해야 한다.

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ

다시 정리하면, "REPEATABLEREAD"는 SELECT에 의한 공유 잠금이 걸려도 데이터의 변경을 막아 줘서,
트랜잭션 중에는 일관되게 데이터를 읽을 수 있게 해 준다.

이렇게 하면 누군가 데이터를 읽기(공유 잠금)만 하고 있어도 그 데이터는 변경할 수 없게 되므로,
READ COMMITTED에 비해 일관성이 조금 좋아졌지만, 동시성은 그 만큼 더 나빠졌다고 할 수 있다.
(READ COMMITTED는 변경중인 데이터를 읽을 수 없음을 의미한다)

  1. Phantom read

REPEATABLE_READ 격리 수준에서는 공유 잠금인 상태의 데이터에 대해 변경 불가가 보장되었다.
하지만, 그 데이터를 변경시키지 못할 뿐 새로운 데이터를 추가/삭제하는 것은 가능하다.
이것을 팬텀 읽기(Phantom read, 가상 읽기)라고 부르는 것이다.

정리하면, 트랜잭션 중에 없던 행이 추가되어 새로 입력된 데이터를 읽는 것 또는
트랜잭션 중에 데이터가 삭제되어 다음 읽기시 이전에 존재하던 행이 사라지는 것을 팬텀 읽기라고 한다.

이 팬텀 읽기를 방지하려면, 격리 수준을 SERIALIZABLE 또는 SNAPSHOT 으로 설정하면 된다.

1) SERIALIZABLE

SERIALIZABLE은 트랜잭션이 수행중일 때 INSERT/DELETE도 제한된다.

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE

REPETABLE_READ가 UPDATE만 금지했던 것이 비해 INSERT/DELETE까지 제한되도록 잠금이 강화된 것이다.

2) SNAPSHOT

SERIALIZABLE 격리 수준이 트랜잭션이 수행 중일 때 INSERT/DELETE를 원천적으로 봉쇄시킨 방법이라면,
SNAPSHOT은 INSERT/DELETE 수행을 블록시키진 않고, 우선 수행은 하게 해 준다.

SNAPSHOT은 트랜잭션이 진행 중인 테이블에 새 데이터를 추가하면,
그 데이터를 실제 테이블에 적용하는 것이 아니라 우선 tempDB에 적용을 시켜놓고,
원래 테이블의 트랜잭션이 종료되면 이 tempDB에 적용시켰던 데이터를 다시 원래 테이블로 입력하는 것이다.

이것이 SNAPSHOT의 동작 방식이다.

READ UNCOMMITTED는 모든 동시성 부작용 문제가 있으며, SERIALIZABLE은 모든 동시성 부작용에서 자유롭다.
하지만, 동시성 부작용이 없을수록 동시 접근성이 떨어지는 문제는 간과하면 안 될 것이다.

3. rollbackFor

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

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

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

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

4. noRollbackFor

rollbackFor 속성과는 반대로 런타임 예외가 발생해도 지정한 런타임 예외면 커밋을 진행한다.

5. timeout

트랜잭션에 제한시간을 지정한다. 초 단위로 지정하고, 디폴트 설정으로 트랜잭션 시스템의 제한시간을 따른다.

-1 입력 시, 트랜잭션 제한시간을 사용하지 않는다.

6. readOnly

트랜잭션을 읽기 전용으로 설정한다. 특정 트랜잭션 안에서 쓰기 작업이 일어나는 것을 의도적으로 방지하기 위해 사용된다. insert,update,delete 작업이 진행되면 예외가 발생한다.

자세한 내용 정리
https://velog.io/@ha0kim/Transactional
https://velog.io/@ha0kim/TransactionManager

참고 사이트
https://mangkyu.tistory.com/30
https://reiphiel.tistory.com/entry/understanding-of-spring-transaction-management-practice

profile
Back-end Developer

0개의 댓글