[Transactional]Spring 프레임워크는 트랜잭션을 어떻게 구현하였는가? 본문

우아한테크코스 4기

[Transactional]Spring 프레임워크는 트랜잭션을 어떻게 구현하였는가?

giron 2022. 7. 10. 16:39
728x90

제목에 대한 질문에 답을 하지 못했다.이 질문에 대해서 정리를 해보고자 한다. 그전에 트랜잭션이 무엇인지 정리하고 시작하려고 한다.

트랜잭션이란?

데이터베이스에 작용하는 여러 읽기와 쓰기를 수행하는 하나의 작업 단위

트랜잭션 범위

트랜잭션의 범위는 커넥션을 기준으로 한다.

예시

커넥션이 다르다면 6에서 롤백이 일어나면, 3번과 5번에 해당한 쿼리 롤백이 된다. 즉, 4번에서 쿼리들은 롤백되지 않고 반영이 된다. 

다른 말로, 여러 메서드에서 하나의 트랜잭션을 갖고 싶다면 이 여러 메서드들을 하나의 커넥션을 사용하도록 하는 방법이 필요하다. -> 이러한 방법이 트랜잭션 전파를 사용한다.

트랜잭션 전파

보통 스프링에선 @Transactional을 통해 해당 어노테이션이 선언된 내부 메서드들도 한 커넥션에 묶인다.

예시

트랜잭션 범위 안에 외부 연동이 섞여 있으면 주의를 해야한다.

왼쪽 그림은 외부 api호출하다가 실패를 했을 때, 2번과 3번이 같이 롤백이 된다. 반면에 오른쪽을 보면 2번과 외부 api가 성공을 했다. 그런데 4번에서 실패하면 2번은 롤백이 되지만 외부API는 그대로 반영이 되었다.

, 롤백이 일어났다면 외부 시스템의 상태도 롤백하는 방법으로 진행해야 할 것같다.

외부 시스템 연동 후 어떤 문제가 발생하면 외부 시스템에 취소 요청을 보내는 방식을 보통 사용 합니다. 문제가 생긴 건을 모아서 일 배치로 하기도 하고, 문제 건을 주기적으로 확인해서 취소 요청을 하기도 합니다. 문제가 생겼을 때 즉시 취소 요청을 보내기도 하구요.

출처: https://www.youtube.com/watch?v=urpF7jwVNWs&t=336s 

 

REDO, UNDO

트랜잭션의 일관성을 지키기 위해서 DBMS는 로그 파일을 따로 보존하며 로그 파일에는 데이터베이스에서 일어난 모든 변화가 기록되어있다. 트랜잭션의 관리는 이 로그 파일의 기록을 바탕으로 이루어진다. 트랜잭션의 비정상적인 종료를 회복하기 위해서 DBMS는 REDO  UNDO 를 사용한다.

 

트랜잭션이 정상 커밋이 안되었을 때, 해당 트랜잭션이 변경한 테이블은 트랜잭션 이전 상태로 복구되어야 한다. 이를 UNDO라고 한다. - rollback

 

REDO는 UNDO의 반대 개념으로 이미 커밋한 트랜잭션의 수정을 재반영하는 연산이다. 만약 페이지 버퍼가 커밋 시점에 변경 사항을 모두 디스크에 반영한다면 REDO가 필요 없다. 하지만 거의 대부분의 DBMS는 효율성을 위해 트랜잭션이 커밋되더라도 일정시간동안 이를 디스크에 반영하지 않고 버퍼에만 저장하고 있는데, 만약 이 시점에 문제가 생긴다면 REDO를 통해 수정 사항을 재반영 해주어야한다.

 

UNDO는 반영 중간에 잘못되면 RollBack 할 느낌, REDO는 중간 버퍼 느낌으로 반영했던 걸 커밋된 걸 다시 DB에 반영하는 느낌으로 가져가면 될 것 같다.

 

시스템 장애가 발생하게 되면 UNDO 데이터도 모두 날아갑니다.

장애로 인해 재시작되면 어떻게 복구가 되나?

장애 발생 이후 데이터베이스가 재시작 복구하는 경우에는 크게 3 단계로 복구가 이루어진다.

 

1 단계는 로그 분석 단계로, 마지막 체크포인트(checkpoint) 시점부터 최근 로그(EOL, End of Log)까지 로그를 탐색하면서 어디서부터 시스템이 복구를 시작해야 하는지, 어느 트랜잭션들을 복구해야 하는지 등등을 알아내는 단계이다.

 

2 단계는 REDO 복구 단계로 복구를 시작해야 하는 시점부터 장애 발생 직전 시점까지 REDO가 필요한 모든 로그를 REDO 복구를 하는 단계이다. 이 단계에서는 심지어 실패한 트랜잭션의 REDO 로그조차도 REDO를 하게 되는데, 언뜻 보면 불필요한 것으로 생각되지만 이렇게 하면 이후의 복구 단계를 매우 간단하게 하는 효과를 가져다 준다. 이 단계에서는 모든 트랜잭션에 대해서 REDO 복구만 한다는 점이 중요한데, 이러한 REDO 복구가 완료된 시점의 데이터베이스 상태는 장애 발생 시점의 상태와 같게 된다. 

 

마지막 3 단계는 UNDO 복구 단계로 로그를 최신 시점부터 다시 역방향으로 탐색하면서 UNDO 복구가 필요한 로그들에 대해서 UNDO 복구를 수행한다. 여기서 수행하는 UNDO는 결국 위에서 설명한 트랜잭션 철회 시에 수행하는 UNDO와 같은 방식으로, repeating history를 통해 데이터베이스 상태를 장애 시점까지 복원해두고 UNDO 복구를 여러 트랜잭션의 철회로 간단하게 해결할 수 있다. 한 트랜잭션만 철회시키는 것이 아니라 여러 트랜잭션을 철회시킨다는 차이점만 존재한다. 이 단계의 UNDO 복구를 개별 트랜잭션의 UNDO와 구별하여 Global UNDO라고도 부른다.

즉, REDO가 UNDO를 복구하고 최종적으로 UNDO가 복구(rollback)를 하게 됩니다. 

트랜잭션 고립

  1. READ UNCOMMITTED
    1. 10번 트랜잭션이 데이터 수정하면 커밋 되기 전에 다른 트랜잭션이 조회하면 수정된걸로 조회(더티리드)
  2. READ COMMITTED(oracle)
    1. 10번 트랜잭션이 데이터 수정하면 커밋이 되기 전에는 수정전 꺼, 된 후면 수정된 거로 조회
    2. ex)
    3. B 트랜잭션에서 10번 사원의 나이를 조회
    4. 27살이 조회됨
    5. A 트랜잭션에서 10번 사원의 나이를 27살에서 28살로 바꾸고 커밋
    6. B 트랜잭션에서 10번 사원의 나이를 다시 조회
    7. 28살이 조회됨
    8. 문제는 한 트랜잭션이 두 번 조회 했는데 할때마다 결과가 달라짐(Non-Reapetable)(정합성 깨짐)
  3. REPETABLE READ(mysql)
    1. 10번 트랜잭션이 500000번 사원을 조회
    2. 12번 트랜잭션이 500000번 사원의 이름을 변경하고 커밋
    3. 10번 트랜잭션이 500000번 사원을 다시 조회(변경전 이름 조회)
    4. (HOW? UNDO 영역에 백업된 데이터 반환)
    5. 10번 트랜잭션을 닫고 다시 조회하면 변경된 이름 조회 성공
  4. SERIALIZABLE
    1. InnoDB에서 기본적으로 순수한 SELECT 작업은 아무런 잠금을 걸지 않고 동작하는데,
    2. 그냥 읽기만 할 때도 접근을 못하게 잠근다.

Non-repeatableRead VS PhantomRead

Non-repeatable reads are when your transaction reads committed UPDATES from another transaction. The same row now has different values than it did when your transaction began.
Phantom reads are similar but when reading from committed INSERTS and/or DELETES from another transaction. There are new rows or rows that have disappeared since you began the transaction.

반복 불가능한 읽기는 트랜잭션 이 다른 트랜잭션에서 커밋된 업데이트를 읽을 때입니다. 이제 동일한 행에 트랜잭션이 시작되었을 때와 다른 값이 있습니다.

팬텀 읽기는 비슷하지만 다른 트랜잭션에서 커밋된 INSERT 및/또는 DELETES에서 읽을 때 입니다 . 거래를 시작한 이후 사라진 새로운 행이나 행이 있습니다.

RepeatableRead에서는 Non-repeatableRead를 undo영역에 저장되어 트랜잭션 id가 작은 것만 찾아서 조회하므로 해결

하지만 insert했을 때는 update된게 아니므로 개수를 조회하면 +1되어서 찾아진다.

SELECT하는 레코드에 쓰기 잠금을 걸어야 하는데, 언두 영역에는 잠금을 걸 수 없기 때문이다.

InnoDB에서 팬텀리드 해결

InnoDB 스토리지 엔진은 레코드 락갭 락을 합친 넥스트 키 락을 사용한다.

 t 테이블에 c1 = 13 , c = 17 인 두 레코드가 있다고 가정하자. 이때 SELECT c1 FROM t WHERE c1 BETWEEN 10 AND 20 FOR UPDATE 쿼리를 수행하면, 10 <= c1 <= 12, 14 <= c1 <= 1618 <= c1 <= 20 인 영역은 전부 갭 락에 의해 락이 걸려서 해당 영역에 레코드를 삽입할 수 없다. 또한 c = 13, c = 17인 영역도 레코드 락에 의해 해당 영역에 레코드를 삽입할 수 없다. 참고로 INSERT 외에 UPDATE, DELETE 쿼리도 마찬가지이다.

이러한 방식으로 InnoDB 스토리지 엔진은 넥스트 키 락을 이용하여 PHANTOM READ 문제를 해결한다.

 

● Record Lock : 각 인덱스 Record에 설정되는 Lock

● Gap Lock : 인덱스 Record 사이의 구간에 설정되는 Lock (Unique 인덱스에서 1건 데이터 변경시 Gap Lock 설정되지  않음)

 

어떻게 구현이 되었는가?(=어떤 흐름으로 동작하는가)

AOP를 통해서 활성화되고 트랜잭셔널의 advice가 메타데이터에 의해 구현이 되어있다.

implementation

그렇다면 AOP를 통해 트랜잭션을 걸때 아래 문제를 주의하자!

  1. private메서드는 AOP가 걸리지않는다. 왜냐하면 spring 2.5버전 이후부터는 default로 CGLIB을 사용하므로 상속을 통해 프록시를 구현한다. 하지만 private메서드는 상속이 불가능하기 때문에 AOP가 걸리지 않는다.
  2. 동일한 클래스 내에서 @Transanctional이 선언되지 않은 메소드에서 @Transactional이 선언된 메소드를 호출해도 트랜잭션이 적용되지 않는다.
  3. @Transactional(propagation=Propagation.REQUIRES_NEW)사용할 때, 동일한 클래스 메소드끼리 호출하면 새로 생성되지 않음. 반드시 다른 클래스 메소드를 호출해야함.

Reference

https://joont92.github.io/db/%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EA%B2%A9%EB%A6%AC-%EC%88%98%EC%A4%80-isolation-level/

https://docs.spring.io/spring-framework/docs/4.2.x/spring-framework-reference/html/transaction.html

728x90
Comments