[java] Transaction과 격리 레벨에 대해서
해당 게시글은 "프로그래머스 데브코스 4기"의 팀 내 스터디 TL 과정으로 노션에 직접 작성한 글입니다.
트랜잭션(Transaction)이란?
- 데이터베이스의 상태를 변화시키기 위해서 수행하는
작업의 단위
작업의 단위가 CRUD 문인가에 대한 의문이 들 수 있다고 생각한다.
게시판에서 작업의 단위를 나눈 예시를 들어보자!
- 게시판 사용자가 게시글을 작성하고 업로드 버튼을 눌러 업로드
- 다시 게시판으로 돌아와 게시판 목록에서 자신이 업로드한 게시글을 확인
1번은 Insert 질의
2번은 Select 질의
그럼, 작업의 단위가 Insert와 Select으로 2개인가요? 라고 말할 수 있겠지만
작업의 단위는 Insert와 Select 질의를 합친 행위로 1개
라는 개념으로 이해해야 한다
트랜잭션 특징 (ACID)
원자성 (Atomicity) A
트랜잭션이 DB 모두! 반영되거나 아니면, 전혀! 반영되지 않아야 하는 특징이다
All or Nothing
원자성이 중요한 이유는 뭘까? 왜 써야할까?
사람A와 사람B가 인터넷 뱅킹으로 10,000원을 송금하는 예시를 들어보자!
사람A의 잔액은 20,000
사람B의 잔액은 50,000이라고 하자
이하 A, B로 부르겠다
먼저, A가 자신의 인터넷 뱅킹 어플로 B에게 10,000원을 송금하려고 한다
그럼 A의 잔액은 20,000 - 10,000 = 10,000이 되고
B에게 10,00을 보내려는 순간… !!에러!!
가 나버렸다.
이때 원자성이 지켜지지 않아 All or Nothing이 적용되지 않기 때문에..
A는 10,000원을 공중분해 하였고 B는 10,000원을 받지 못하는 상황이 된 것이다.
반대로, 만약 원자성이 지켜지는 상태였다면?
A의 잔액이 -10,000이 되고 B에게 송금하는 그때 에러가 난다면
바로 이때까지 진행했던 모든 연산들을 취소(Rollback)하는 것이다.
A는 다시 20,000원을 가지게 되는 것이고 B는 10,000원을 받지 않는 것이다.
이렇게.. 중요한 원자성을 지키는 것이 트랜잭션의 첫번째 특징이다.
일관성 (Consistency) C
사람은 일관적이어야 한다는 말을 아시는감? 트랜잭션도 똑같다
트랜잭션 실행 이전, 이후든 DB의 상태는 이전과 같이 유효해야 한다
즉, 트랜잭션 실행 이후에도 DB에서 약속한 제약과 규약을 계속해서 만족해야 한다
예로, KOREA 은행에서 A씨가 새로운 신규 계좌를 개설했다는 걸 가정해보자.
이때 계좌의 규칙중 하나는 “모든 고객은 반드시 계좌에 인감/사인을 필요로 한다”
A가 신규 계좌를 개설하면서 일관성이 지켜지지 않는 상태라면?
- A의 계좌를 개설하면서 다른 고객의 인감/사인이 사라진다
- A가 계좌를 개설하면서 인감/사인을 제출하지 않았다
와 같은 상황들이 발생할 수 있다
그럼 쟤는 되고 왜 나는 안돼!?!?
와 같은 상황이 발생할 수 있기에
트랜잭션의 두번째 특징인 일관성 또한 잘 지켜줘야 한다.
독립성 (Isolation) I
모든 작업(트랜잭션)은 독립적이어야 한다.
여러개의 작업(트랜잭션)을 동시에 수행해도 각 결과는 따로 나와야 하는 것이다.
A는 잔액이 10,000원 있다
B에게 8,000원 C에게 8,000원을 동시에 송금하는 상황이다
독립성이 지켜지지 않는다면?
10,000 - 8,000 - 8,000 = -6,000
갑자기 A는 대출을 받게 되는 상황이 되는 것이다
반대로, 독립성을 지키는 상황이라면?
B와 C에게 동시에 송금을 하는 상황이어도 “차례대로 연속으로” 송금을 하는 것이다.
A→B (10,000 - 8,000 = 2,000) 송금완료
A→C (2,000 - 8,000 = ?) 송금실패
지속성 (Durability) D
지속성은 우리 인터넷 계좌에 오류가 발생했다고 우리 계좌 정보가 날아가는걸 막아주는 특징이다.
지속성이 지켜지지 않는다면?
은행 어플에서 오류가 나고 동시에 은행 기록들이 전부 사라질 수 있게 되는 것이다..
A는 B한테 만원을 송금한 기록이 어플의 오류로 인해 송금 기록이 사라지면 A만 억울하게 되는 것이다.
즉, 어떤 일이 있어도 작업(트랜잭션)의 기록(로그)가 남아있어야 하는 특징
이 지속성이다.
이러한 드래곤볼들이 모여 트랜잭션의 특징을 ACID
라고 표현하는 것이다
그럼 트랜잭션 격리는 무엇인가?
그전에 “격리(isolation)”의 뜻은?
- 다른 것과 통하지 못하도록 사이를 막거나 떼어놓는 것
트랜잭션 격리도 똑같다. 다들 코로나 격리가 생각나실거다
서로에게 코로나 바이러스가 영향을 미치지 않도록 격리하는 상황이었다
코로나 격리에 수준이 있어 단계별로 적용했던 것이 기억나시는지!
마찬가지로 트랜잭션의 격리도 수준
이 있어 단계별 적용이 가능하다는 것이다.
트랜잭션 격리 수준
- 여러 트랜잭션이 동시에 변경을 수행할 때 성능과 안정성, 일관성 및 재현성 간의 규형을 미세하게 조정하는 설정
- 데이터 베이스 ACID 성질 중
Isolation
즉, 트랜잭션이 동시에 실행되게 되면 다른 트랜잭션에게 얼마나 간섭할지를 단계로 정하는 것이다!
여기서 간섭이란 것은 특정 트랜잭션이 다른 트랜잭션에 변경을 하거나 조회하는 데이터를 볼 수 있게 할지 말지 결정하는 의미이다.
트랜잭션 격리 방법
아래로 갈수록 격리 수준이 높아지는 단계
0레벨의 격리 수준이 가장 낮고 3레벨의 격리 수준이 가장 높다
동시에, 격리 수준이 높아질수록 성능이 떨어지게 된다
“0레벨” READ UNCOMMITTED
- 다른 트랜잭션에서 커밋되지 않은 내용 참조 가능
- 문제가 많은 격리 수준이기 때문에 사용을 권고하지 않음
- Dirty 🤮 READ 발생
“1레벨” READ COMMITTED (커밋된 읽기)
다른 트랜잭션에서 커밋된 내용만 참조 가능 (Oracle)
- Non-Repeatable💫 READ 발생
- 실제 테이블 값을 가져오는 것이 아니라
Undo 영역
에 백업된 레코드에서 가져옴Undo 영역
: 데이터를 저장하는 버퍼 기능- 트랜잭션에서 연산(커밋, 롤백)이 이루어지지 않아도 질의문에 의해 수정이 생기면 수정되기 이전 값이 저장되는 영역
“2레벨” REPEATEABLE READ (반복 읽기 가능)
트랜잭션 진입하기 이전
에 커밋된 내용만 참조 가능- MySQL의 InnoDB엔진 default 격리 레벨
- MySQL에서는 트랜잭션마다 트랜잭션 ID를 부여하여 트랜잭션 ID보다 작은 트랜잭션 번호에서 변경한 것만 읽는다
- 트랜잭션이 완료될 때 까지 Select문이 사용하는 모든 데이터에 Shaked Lack이 걸린다
- 따라서 트랜잭션이 범위 내에서 조회한 데이터 내용이 항상 일관됨을 보장
- 다만, Phantom 👻 READ가 발생
“3레벨” SERIALIZABLE
- 단순하면서 엄격한 격리 수준
- But, 성능 저하가 크게 옴 ⇒ DB에서 거의 사용하지 않는다
- 트랜잭션에 진입하게 되면 락(Lock)을 걸어 다른 트랜잭션에 대한 접근 금지 명령
- 트랜잭션들이 동시에 실행되지 않고 차례대로 실행되는 것처럼 동작
- Phantom👻 Read가 발생하지 않는다
트랜잭션 격리가 필요한 이유?
왜 굳이 수준별 트랜잭션 격리를 사용해야 하는지 의문이 들지 않는지요?
이유는 아래와 같은 3가지 현상이 발생하기 때문이다!
Dirty🤮 Read
- 커밋되지 않은 수정중인 데이터를 다른 트랜잭션에서 읽을 수 있도록 허용할 때 발생하는 현상
Non-Repeatable 💫 Read
- 한 트랜잭션에서 같은 쿼리를 두 번 실행할 때 그 사이에 다른 트랜잭션 값을 수정 또는 삭제하면서 두 쿼리의 결과가 상이하게 나타나 일관성이 깨진 현상
- Non이 붙지 않은 Repeatable Read의 뜻은 뭔가?
- 하나의 트랜잭션 내에서 동일한 조회를 수행했을 때 항상 같은 결과를 반환
- 즉, 반대로 해당 현상은 동일한 조회를 수행해도 다른 결과가 나오는 현상이다
참고
✅ Dirty Read와 Non-Repeatable Read의 차이가 뭔지 잘 모르겠어요.
- Dirty Read는 `커밋되지 않은 값`을 읽어서 문제가 발생하는 것
- Non-Repeatable Read는 읽었던 값이 `다른 트랜잭션에 의해 다른 값으로 커밋되어 다른 값으로 변경` 되어서 발생하는 문
Phantom Read 👻
- 한 트랜잭션 안에서 일정 범위의 레코드를 두 번 이상 읽었을 때 첫번째 쿼리에선 없던 레코드(👻유령👻 Phantom)가 두번째 쿼리에서 나타나는 현상
- 즉, 보였다가 안보였다가 유령처럼 레코드가 나타나는 것!
- 트랜잭션 도중 새로운 레코드 삽입을 허용하기 때문에 나타나는 현상
- 즉,
Insert
에 대해서만 발생하는 문제 - Select와 Delete에 대해서는 발생하지 않는다
- 즉,
- 이를 해결하기 위해서는
Write Lock(쓰기 잠금)
을 걸어야 한다
- T2가 select 했을땐 id1만 보였는데 다음에 또 select하니 id2가 보이는 현상
정리
마무리
트랜잭션은 DB의 데이터를 일관되고 안정적으로 유지하기 위해서 꼭 사용해야 하는 기능이다.
여러개의 연산을 트랜잭션이라는 하나의 단위로 묶어 데이터를 안정적으로 다룰 수 있게된다.
만약, 서버 운영중에 모아둔 황금같은 데이터들이 꼬이고 없어진다면 너무나 슬프지 않을까?
그러니 트랜잭션이라는 개념과 기능을 잘 활용해서 이와같은 불상사가 일어나지 않도록 활용하자.
[부록] JPA의 @Transactional
Transaction을 어떻게 활용할건데?에 대해서 생각해 보았더니 이럴수가!
활용 방법을 적지 않은 것 같다!
그래서 우리가 가장 쉽고 간편하게 활용할 수 있는 방안은 바로 !!!
JPA의 @Transactional
임이 떠올랐다.
그 중 비즈니스 로직이 담겨있는 서비스 레이어에서 트랜잭션 처리를 많이 한다
이유는 레포지토리 레이어에서 읽어온 데이터들을 관리하는 공간이기 때문이다
@Transactional(readOnly = true)
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
@Override
public UserDTO.UserSimpleInfoResponse updateUser(Long userId, UserDTO.UpdateUserRequest request) {
Users user = userRepository.findById(userId).orElseThrow(() -> new EntityNotFoundException("존재하지 않는 사용자 입니다."));
user.setNickname(request.getNickname());
user.setProfileImage(request.getProfileImage());
return new UserDTO.UserSimpleInfoResponse(user);
}
}
[트랜잭션 주의사항]
트랜잭션은 꼭 필요한 최소한의 범위로 수행해야 한다.
왜냐하면 일반적으로 데이터베이스 커넥션은 갯수가 제한적이기 때문에 각 트랜잭션에서 커넥션을 소유하는 시간이 길어진다면 그 이후에 사용 가능한 커넥션의 갯수가 줄어들고 어느 순간 다른 트랜잭션이 수행될 때 커넥션이 부족하여 커넥션을 받기 위해 지연되는 상황이 발생할 수 있기 때문이다.
지금까지 트랜잭션의 정의와 특징을 살펴보았고 또한, 여러 개의 트랜잭션이 동시에 실행되면서 발생하게 되는 문제들과 이를 해결하기 위한 트랜잭션 격리 레벨을 알아보았다.
항상 @Transactional을 붙이는 이유가 궁금했는데 해당 내용을 정리하게 되면서 그 이유에 대해 한 발자국 더 가까이 다가가게 된 것 같다
여기까지 읽어주신 여러분들도, 그리고 나도
Reference