트랜잭션이란?
연산
- Commit : DB에 반영됨. 트랜잭션의 성공을 알림
- Rollback : 문제 상황 발생 시 다시 원래 상태로 복귀
- undo 영역
- 커밋되기 전의 SQL들을 담는 공간이다.
- Consistent Read를 제공한다.
- redo 영역
- 트랜잭션을 재실행하기 위함.
-> 문제상황 발생 시 redo 영역을 통해 체크포인트로 이동 뒤 undo 영역으로 커밋되기 전 상황들을 롤백한다.
트랜잭션의 특성
ACID
A : 원자성 -> 한 트랜잭션은 All or Nothing. 전체 다 발생하던지 발생하면 안된다. 멱개의 연산만 실행되는 것은 용납할 수 없다.
C : Consistency -> 일관성. DB의 규칙에 맞게 데이터가 저장되어야 한다.
I : Independency -> 독립성. 각 트랜잭션은 독립적으로 진행되야 한다.
D : D -> 지속성. 데이터는 트랜잭션이 끝난 뒤에도 지속되야 한다.
트랜잭션 병행 처리 시 발생되는 문제점
- Dirty Read : 커밋되지 않은 데이터를 읽을 수 있는 것
- UnRepeatable Read : 한 트랜잭션에서 여러번의 SQL을 날렸을 때 값이 달라지는 것
- Phantom Read : 한 트랜잭션 내에서 두 번의 SQL을 날렸을 때 중간에 값이 생성되는 것
Consistent Read
- 커밋된 후 특정 시점의 DB를 스냅샷으로 찍어서 보관하는 것
- InnoDB는 즉시 갱신 회복 기법으로 커밋되지 않은 데이터를 DB에 반영하는 데 어떻게 이게 가능하냐?
- InnoDB가 실행할 수 있는 방법: 모든 쿼리 후 결과를(커밋되지 않은 것들도)
트랜잭션 격리수준
-
Read_uncommitted : 다른 트랜잭션에서 커밋되지 않은 데이터도 읽을 수 있는 격리 수준. InnoDB에서는 즉시 갱신 회복 요법을 사용해서 커밋전에도 DB를 수정하기 때문에 DB값을 읽어오면 Dirty Read가 발생 가능하다.
Consistent Read가 적용X
Dirty read + Repeatable Read + Phantom Read모든 현상이 발생
-
Read_committed : 커밋된 데이터만 읽을 수 있는 격리수준. -> Dirty Read가 방지됨
UnRepeatable Read는 실행됨
-> Consistent Read가 select문이 발생할 때마다 적용되기 때문이다.
따라서 다른 트랜잭션에서 update/delete 후 commit을 날리면 해당 값이 DB에 저장되고, select문이 commit된 데이터를 모두 들고오기 때문에 값이 다르게 보여지는 결과가 나타난다.
-
Repeatable Read : 트랜잭션의 아이디를 비교하여 현재 SQL문보다 앞선 데이터 값만 읽어오는 격리수준
UnRepeatable Read가 방지된다 -> Consistent Read가 커밋 후 처음 select를 했을 당시를 남겨두기 때문에 commit전까지는 해당 스냅샷 결과만 보여지게 된다.
Phantom Read는 방지가 안된다 -> select for update를 날릴 때 해당 레코드에만 lock이 걸리고 다른곳에는 안걸리는데, 따라서 insert는 가능하기 때문이다.
하지만 innoDB는 next key lock = gap lock + record lock을 함께 걸어버리기 때문에 phantom read를 방지할 수 있다.
또한 InnoDB의 Repeatable Read수준에는 select for share/update, update, delete시 넥스트 키 락을 걸어버린다 -> 하지만 update, delete는 공유 락이기 때문에 다른 트랜잭션에서 해당 필드를 조회는 가능하다.
-
Serializable : select 문을 select for share로 처리하여 어떠한 트랜잭션도 값을 변경할 수 없는 격리수준. 성능이 매우 떨어진다.
트랜잭션 전파레벨
- Mandatory : 부모가 무조건 존재해야 함. 아니면 에러
- Never : 트랜잭션이 존재하면 안됨
- Not-supported : 트랜잭션이 존재해도 생성X
- Requires_new : 새로운 트랜잭션을 생성. 하지만 자녀의 에러는 부모까지 전파된다.
- 꼭 named lock을 사용할 때 해당 전파레벨로 처리해야 한다.
- 왜냐하면 하나의 트랜잭션을 사용할 시 나올 때 바로 lock이 해제가 되는데, 이후 commit이 발생하기 때문이다.
- 따라서 꼭 commit 후에 lock을 해제해야 하므로 새롭게 트랜잭션을 생성한다.
- Required : 부모 트랜잭션이 존재하면 해당 트랜잭션에 사용, 없으면 새로 생성
- nested : 병행해서 생성. 만약에 자식 트랜잭션 문제 시 자식만 롤백. 아니면 부모까지 롤백.
- Supported : 부모 트랜잭션이 있으면 사용. 없으면 생성X
병행 트랜잭션 처리
- locking 기법
- optimistic lock
- 버전 체크를 통해 해당 버전과 일치하면 데이터 변경, 불일치 시 데이터 변경X
- 성능 좋음. 충돌 발생 시 개발자가 직접 에러 처리해야 함.
- 충돌이 적게 발생하는 환경에서 유리
- pesimistic lock
- select for share, select for update -> 공유 락, 배타 락
- 성능 저하. 충돌 발생 시 자동 롤백
- 충돌이 다수 발생하는 환경에서 유리
@Transactional 동작원리
- 해당 클래스에 대한 트랜잭션 기능이 적용된 프록시 객체가 생성된다.
- 해당 어노테이션이 붙은 메서드가 호출될 경우 트랜잭션 시작, rollback, commit을 수행한다.
- 정상여부는 default로 runtimeException을 제외하고 판단됨
@Transactional 사용 주의점
- private는 @Transactional이 적용되지 않는다.
- proxy형태로 동작하기 때문에 외부에서 접근이 가능한 메서드만 설정가능
- 같은 클래스 내 여러 @Transactional 메서드 호출
- 롤백을 해도 Auto_increment로 증가된 id는 다시 감소하지 않는다.
- 트랜잭션 범위 밖에서 동작하기 때문
- 왜? - 동시성 문제
- rollback의 문제때문에 한 사용자가 다른 사용자의 가입을 기다려야 하는 상황이 발생할 수 있다.
프록시 패턴 *
- 원래 객체를 감싸고 있는 객체
- 프록시 구현체
- JDK Proxy(Interface Based)
- AOP를 적용하여 구현된 클래스의 인터페이스를 프록시 객체로 구현하여 코드를 끼워넣는 방식
- 자바에서 기본적으로 제공
- CGLib Proxy(Subclass Based)
- Spring Boot가 default로 사용
- 인터페이스를 구현하지 않고 해당 구현체를 상속받는 것으로 문제를 해결