[면접 대비] 스프링과 트랜잭션

Swim Lee·2021년 5월 12일
2

기술면접 대비

목록 보기
6/13

트랜잭션

트랜잭션 매커니즘

출처 : https://www.youtube.com/watch?v=ImvYNlF_saE

트랜잭션이 왜 필요한가?

데이터베이스의 상태를 변화시키기 위해 수행하는 작업의 단위

상태를 변화시킨다는 것 ➡ SQL 질의어를 통해 DB에 접근한다는 것
작업 단위 ➡ 많은 SQL 명령문들을 사람이 정하는 기준에 따라 정하는 것

트랜잭션 없이 계좌이체를 결제를 한다면?

  • 구매자의 계좌에서 돈이 출금된다.
  • 판매자의 계좌에 돈이 입금된다.
UPDATE acounts
SET balance = balance - 10000
WHERE user = '구매자';

UPDATE accounts
SET balance = balance + 10000;
WHERE user = '판매자';

여기서 오류가 발생한다면?
어떤 오류가있고, 어떻게 처리할지 이야기해보자

예상되는 오류

  • 구매자의 계좌에서 돈이 출금된 뒤, DB가 다운된다.
  • 구매자의 계좌에서 돈이 출금되지 않았는데, 판매자에게 돈이 입금된다.
  • 출금도 입금도 되지 않는다.

처리방법

어중간한 상태로 두면 안된다. 전부 없었던 일로 해주자! (초기상태로 되돌리기)

이 일을 해주는 것이 바로 트랜잭션의 역할 (작업의 단위 all or nothing을 지정해주는 것)

트랜잭션 동작 방식

DB 구조

실행될 쿼리

위에서 나온 쿼리에 트랜잭션을 건 경우

BEGIN TRAN
UPDATE acounts
SET balance = balance - 10000
WHERE user = '구매자';

UPDATE accounts
SET balance = balance + 10000;
WHERE user = '판매자';
COMMIT TRAN

  • 해당 쿼리가 쿼리 처리기를 통해 통과가 된다.
  • 해당 쿼리를 처리하기 위해서는 구매자의 데이터가 필요하기 때문에 데이터 캐시에 구매자의 데이터를 요청한다.
  • 데이터 캐시에 요청한 데이터가 없다.

  • 데이터 파일에서 필요한 데이터를 데이터 캐쉬로 가져오고,
  • 데이터 캐쉬는 필요한 데이터가 업데이트된다.
  • 가져온 데이터를 업데이트하기전에 먼저해야할 일은, 로그를 기록하는 것
  • 로그의 종류에는 REDO와 UNDO가 있음
    • REDO : 변경 후(업데이트 후)의 값을 기록
    • UNDO : 변경 전(업데이트 전)의 값을 기록
  • 참고로 accounts.balance가 맞고 구매자는 alias임 ㅋㅋ
  • REDO의 경우 트랜잭션이 시작되었다 명시를하고, 그 데이터가 어떻게 변경될 것인지 기록
  • UNDO는 해당 데이터값이 원래 무엇이었는지 기록
  • 로그를 기록한 후, 데이터 캐시를 업데이트한다.
  • 이후 다음 데이터도 데이터 캐시에 없으므로 데이터 파일에서 요청을 해서 캐시로 가져온 후, 업데이트 전 후에 로그를 기록하고 (업데이터 전에는 UNDO, 업데이트 후에는 REDO) 데이터를 업데이트한다.



  • 트랜잭션이 완료가 되면은 REDO로그에는 트랜잭션이 COMMIT되었다는 기록을 한다.
  • 만약 해당 트랜잭션에 오류가 발생하여 개발자가 ROLL BACK 명령을 내리는 경우
  • UNDO로그로 데이터 복원
  • 왜냐하면 지금까지 데이터 캐시에서 데이터를 업데이트해서 변경된 상태로 있을 것이고, UNDO로그로 다시 원래값으로 복원한다.
  • 데이터의 일관성 지킬 수 있음
  • ROLLBACK 명령을 명시적으로 내리는게 아니라 예상치 못한 오류 발생하는 경우 (DB 전원이 꺼져버린다든가?)
  • REDO 로그를 이용해서 데이터를 다시 원래상태로 복원한다. (일관성있게 만들어준다.)
  • 내 사견을 첨부하자면, 트랜잭션이 완료된 경우에는 REDO로그를 통해서 복원해야하고, 트랜잭션이 수행되는 도중에 발생하면 UNDO를 통해 복원하는 것

트랜잭션 관리를 위한 DBMS의 전략

DBMS 구조

  • 크게 2가지
    • Query Processor (질의 처리기)
    • Storage System (저장 시스템)
  • 입출력 단위 : 고정 길이의 page 단위로 disk에 읽거나 쓴다 (read / write)
  • 저장공간 : 비휘발성 저장장치인 disk에 저장, 일부분을 Main Memory에 저장

Page Buffer Manager of Buffer Manager

DMBS의 Storage System (저장 시스템)에 속하는 모듈 중 하나로, Main Memory에 유지하는 페이지를 관리하는 모듈

Buffer 관리 정책에 따라 UNDO 복구와 REDO 복구가 요구되거나, 그렇지 않게 되므로, 트랜잭션 관리에 매우 중요한 결정을 가져온다.

UNDO

필요한 이유

오퍼레이션 수행중에 수정된 page들이 Buffer 교체 알고리즘에 따라 디스크에 출력될 수 있음 (여기서 출력이란 memory에서 disk로 output연산이 되는 것을 말함)

Buffer 교체는 트랜잭션과는 무관하게 전적으로 Buffer의 상태에 따라서 결정된다.일관성 관점에 봤을 때는 임의의 방식으로 일어나게 된다.

즉, 아직 완료되지 않은 트랜잭션이 수정한 페이지들도 디스크에 출력될 수 있다. 따라서 만약 해당 트랜잭션이 어떤 이유든 정상적으로 종료될 수 없게 되면 트랜잭션이 변경한 (디스크에 출력된, 혹은 메모리 버퍼의) 페이지들은 원상 복구되어야 한다.

이러한 복구를 UNDO라고 한다.

만약 버퍼 관리자가 트랜잭션 종료 전에는 어떤 경우에도 수정된 페이지들을 디스크에 쓰지 않는다면, UNDO 연산은 메모리 버퍼에 되는 식으로 매우 간단해질 수 있다.

하지만, 이 부분은 매력적이지만 매우 큰 메모리 버퍼가 필요하다는 문제점을 가지고 있다.

버퍼 쓰기 정책

수정된 페이지를 디스크에 쓰는 시점을 기준으로 아래의 두 개의 정책으로 나눌 수 있다.

  • steal : 수정된 페이지를 언제든지 디스크에 쓸 수 있는 정책
  • not steal : 수정된 페이지들을 EOF(End of Transaction)까지는 버퍼에 유지하는 정책 (버퍼에 유지한다는 것은 그전까지는 버퍼에서 디스크로 출력하지 않고 가지고 있는다는 뜻)

steal 정책은 수정된 페이지가 어떠한 시점에도 디스크에 써질 수 있기 때문에 필연적으로 UNDO 로깅과 복구를 수반한다.

거의 모든 DBMS가 채택하는 버퍼관리 정책이다.

REDO

UNDO의 반대개념. 커밋한 트랜잭션의 수정은 어떠한 경우에도 유지(durability)되어야한다. 이미 커밋한 트랜잭션의 수정을 재반영하는 복구작업을 REDO라고 한다.

REDO 복구역시 버퍼관리정책에 영향을 받는다.

Buffer 관리 정책

트랜잭션이 종료되는 시점에 해당 트랜잭션이 수정한 페이지들을 디스크에도 쓸 것인가 여부로 두 가지 정책이 구분.

  • force : 수정했던 모든 페이지를 트랜잭션 커밋 시점에 디스크에 반영
  • not force : 수정했던 페이지를 트랜잭션 커밋시점에 디스크에 반영하지 않는 정책

여기서 주의할점

not force 정책이 수정했던 페이지(데이터)를 디스크에 반영하지 않느다는 점이지, 커밋 시점에 어떠한 것도 쓰지 않는다는 것은 아니다. 어떤 일들을 했었다고 하는 로그는 기록한다.

force 정책을 따르면 트랜잭션이 커밋되면 수정되었던 페이지들이 이미 디스크상의 데이터베이스에 반영되었으므로 REDO 복구가 필요없어진다.

반면 not force 정책을 따른다면 커밋한 트랜잭션의 내용이 디스크상의 데이터베이스상에 반영되어 있지 않을 수 있기 때문에 반드시 REDO 복구가 필요하다.

사실 force 정책을 따르더라도 데이터베이스 백업으로부터의 복구, 즉 미디어(media)복구 시에는 REDO 복구가 요구된다.

따라서 거의 모든 DBMS가 채택하는 정책은 not force이다.

정리

DBMS는 버퍼 관리 정책으로 steal (언제든지 디스크에 변경사항이 출력될 수 있다) 과 not force (커밋시점에 디스크에 변경사항 기록을 강제하지 않는다.) 정책을 채택하고 있어, 이로 인해 REDO와 UNDO 복구가 모두 필요하다.

출처 : https://d2.naver.com/helloworld/407507

스프링 트랜잭션 전파 타입

출처 :
https://www.youtube.com/watch?v=e9PC0sroCzc
https://jsonobject.tistory.com/467
https://n1tjrgns.tistory.com/266

트랜잭션이 시작하거나 참여하는 방법에 관한 설정
트랜잭션의 경계에서 트랜잭션이 어떻게 동작할 것인가

public class ServiceA {

	private ServiceB serviceB;
    
    ...
    
    @Transactional
    public void a() {
    	service.b();
    }
}

public class ServiceB {
	@Transactional
    public void b() {...}
}

위의 코드에서처럼 트랜잭션이 처리되는 과정안에서 또 다른 트랜잭션이 처리되는 경우가 있다.

부모 트랜잭션이 있냐 없냐에 따라 타입별로 트랜잭션의 경계를 설정할 수 있다.

@Transactional을 클래스 또는 메서드 레벨에 명시하면 해당 메서드 호출시 지정된 트랜잭션이 작동하게 된다. 해당 클래스의 Bean이 다른 클래스의 Bean에서 호출될 때만 @Transactional을 인지하고 작동하게 된다.
(같은 Bean 내에서 @Transactional이 명시된 다른 메서드를 호출해도 작동하지 않는다.)

스프링 프레임워크는 내부적으로 AOP를 통해서 해당 애노테이션을 인지하여 프록시를 생성해서 트랜잭션을 자동 관리한다.

스프링에서 제공하는 전파타입은 총 7가지

Propagation.REQUIRED

@Transactional(propagation = Propagation.REQUIRED)
public void doSomething() { ... }
  • 특정 메서드의 트랜잭션이 Propagation.REQUIRED로 설정된 경우 동작방식
  • 기본적으로 해당 메서드를 호출한 곳에서 별도의 트랜잭션이 설정되어있지 않았다면, 트랜잭션을 새로시작! (새로운 연결을 생성하고 실행)
  • 만약 호출한 곳에서 이미 트랜잭션이 설정되어있다면, 기존의 트랜잭션 내에서 로직 실행 (동일한 연결 안에서 실행)
  • 예외가 발생하면 롤백이 되고, 호출한 곳에도 롤백이 전파된다.
  • 기본값이므로 생략 가능
  • 만약 해당 메서드가 호출한 곳과 별도의 스레드라면?
  • 답은 전파레벨과 상관없이 무조건 별도의 트랜잭션을 생성하여 해당 메서드를 실행
  • 스프링은 내부적으로 트랜잭션의 정보를 ThreadLocal 변수에 저장하기 때문에 다른 스레드로 트랜잭션이 전파되지 않는다!!

Propagation.REQUIRES_NEW

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void doSomething() { ... }
  • Propagation.REQUIRES_NEW의 경우
  • 매번 새로운 트랜잭션을 시작한다. (새로운 연결(DB 커넥션)을 생성하고 실행한다.)
  • 만약 호출한 곳에서 이미 트랜잭션이 설정되어있다면(기존의 연결이 존재한다면), 기존의 트랜잭션은 메서드가 종료할 때까지 잠시 대기 상태로 두고 자신의 트랜잭션을 실행한다.
  • 새로운 트랜잭션 안에서 예외가 발생해도, 호출한 곳에는 롤백이 전파되지 않는다.
  • 즉 두개의 트랜잭션은 완전히 독립적인 별개의 단위로 작동한다.

Propagation.NESTED

@Transactional(propagation = Propagation.NESTED)
public void doSomething() { ... }
  • Propagation.NESTED의 경우
  • 기본적으로 앞서 설명한 Propagation.REQUIRED와 동일하게 작동
  • 중요한 차이점은 SAVEPOINT를 지정한 시점까지 부분 롤백이 가능하다는 것! (아직 커밋되지 않은 상태에서 SAVEPOINT를 지정할 수 있고 해당 포인트까지 롤백이 가능한 DB의 기능)
  • 유의할 점은 데이터베이스가 SAVEPOINT 기능을 지원해야한다. (대표적으로 ORACLE)
  • 이미 진행중인 트랜잭션이 있다면 중첩 트랜잭션을 시작
  • 해당 메소드가 부모 트랜잭션에서 진행될 경우 별개로 커밋되거나 롤백될 수 있다.
  • 둘러싼 부모 트랜잭션이 없을 경우 Propagation.REQUIRED와 동일하게 동작

Propagation.MANDATORY

@Transactional(propagation = Propagation.MANDATORY)
public void doSomeThing() {...}
  • 부모 트랜잭션 내에서 실행되며, 부모 트랜잭션이 없을 경우 Exception 발생

Propagation.SUPPORT

@Transactional(propagation = Propagation.SUPPORT)
public void doSomeThing() {...}
  • 부모 트랜잭션이 존재하면 부모 트랜잭션으로 동작하고, 없을 경우 non-transactional하게 동작

Propagation.NOT_SUPPORT

@Transactional(propagation = Propagation.NOT_SUPPORT)
public void doSomeThing() {...}
  • non-transactinal로 실행되며, 부모 트랜잭션이 존재하면 부모 트랜잭션을 일시정지하고 수행한다.

Propagation.NEVER

@Transactional(propagation = Propagation.NEVER)
public void doSomeThing() {...}
  • non-transactinal로 실행되며, 부모 트랜잭션이 존재하면 Exception이 발생

추가

  • no-rollback-for : 롤백하지 않을 익셉션 타입
  • rollback-for : 롤백할 익셉션 타입

정리

https://developyo.tistory.com/250 : 이게 제일 좋은 듯 (나중에 보기)

  • REQUIRE : 부모 트랜잭션(자신을 호출한 메소드의 Transaction)이 존재한다면 그에 포함되어 동작. 부모 트랜잭션이 존재하지 않을 경우 새로 트랜잭션 생성(default 속성)
  • SUPPORTS : 부모 트랜잭션이 존재하면 그에 포함되어 동작. 부모 트랜잭션이 없다면 트랜잭션 없이 동작.
  • MANDATORY : 부모 트랜잭션이 존재하면 그에 포함되어 동작. 부모 트랜잭션이 없다면 예외 발생시킨다.
  • REQUIRES_NEW : 부모 트랜잭션 존재 여부와 상관없이 트랜잭션을 새로 생성
  • NOT_SUPPORTED : 트랜잭션을 사용하지 않는다. 부모 트랜잭션이 존재하면 보류시킨다.
  • NEVER : 트랜잭션을 사용하지 않도록 강제한다. 부모 트랜잭션이 존재할 경우 예외를 발생시킨다.
  • NESTED : 부모 트랜잭션이 존재하면 부모 트랜잭션 안에 트랜잭션을 만든다. 부모트랜잭션의 커밋과 롤백에 영향을 받지만 자신의 커밋과 롤백은 부모 트랜잭션에 영향을 주지 않는다. 하지만 부모 트랜잭션의 롤백은 자신에게 반영된다.

※ NESTED 사용의 예 :
메인 작업을 진행하며 이와 관련된 로그를 DB에 저장해야 한다.
로그를 저장하는 작업이 실패하더라도 메인 작업의 트랜잭션은 롤백하지 않는다.
하지만 메인 작업이 실패할 경우 로그 또한 저장하지 않아야 한다(롤백 되어야 한다).
Propagation 과 관련된 부분은 이곳을 참고

profile
백엔드 꿈나무 🐥

0개의 댓글