이번 글에서는 트랜잭션의 격리수준에 대해서 집중적으로 작성했습니다.
스프링(Spring)에서는 트랜잭션의 격리 수준(Isolation Level) 을 설정하여 다른 트랜잭션과 얼마나 독립적으로 실행될지를 결정할 수 있습니다. 올바른 격리 수준을 선택하면 성능과 데이터 일관성 사이에서 균형을 맞출 수 있습니다.
스프링에서 트랜잭션의 격리 수준을 설정할 때는 @Transactional(isolation = Isolation.XXX)
을 사용합니다.
1. READ UNCOMMITTED (가장 낮은 격리 수준)
아직 커밋되지 않은 데이터도 읽을 수 있는 상태
문제점: Dirty Read(더티 리드) 발생 가능
사용 예시: 데이터 일관성이 덜 중요하고 성능이 중요한 경우
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void readUncommittedExample() {
accountRepository.findBalanceById(1);
}
예제(Dirty Read 발생 가능)
- 트랜잭션 A가 계좌 잔액을
1000원 → 500원
으로 변경했지만 아직COMMIT
하지 않음 - 트랜잭션 B가 계좌를 조회하면 500원이 보임 (실제로는 커밋이 안 됐는데도)
- 이후 트랜잭션 A가
ROLLBACK
하면 원래1000원
으로 돌아가는데, B는500원
을 잘못 읽었음
Read Uncommitted는 일반적으로 잘 사용되지 않는 설정이다. 정확성이 불필요한 조회만 한다거나, 정말 성능이 중요한게 아니라면 고려하지 말자.
2. READ COMMITTED (기본적인 격리 수준)
커밋된 데이터만 읽을 수 있도록 보장
문제점: Non-Repeatable Read(반복 불가능한 읽기) 발생 가능
사용 예시: 일반적인 애플리케이션에서 사용 (스프링 JPA 기본값)
@Transactional(isolation = Isolation.READ_COMMITTED)
public void readCommittedExample() {
accountRepository.findBalanceById(1);
}
예제(Non-Repeatable Read 발생 가능)
- 트랜잭션 A가 계좌 잔액을 조회했을 때 1000원
- 트랜잭션 B가 1000원 → 500원으로 변경 후 COMMIT
- 트랜잭션 A가 다시 계좌를 조회하면 500원이 되어 있음 (이전과 값이 다름)
같은 데이터를 여러 번 조회할 때 값이 변할 수 있습니다. 이런 문제는 반복적으로 데이터를 조회하면서 수정을 하거나, 새로 입력하는 동작을 할 때 문제가 발생할 수 있으니 주의를 해야합니다. 제가 공부하면서 읽은 다른 블로그에서는 은행의 잔금조회와 잦은 송금이 동시에 발생하는 상황을 예로 들어주었습니다.
3. REPEATABLE READ (MySQL 기본 격리 수준)
트랜잭션이 시작될 때 조회한 데이터는 트랜잭션이 끝날 때까지 동일하게 유지됨
문제점: Phantom Read(팬텀 리드) 발생 가능
사용 예시: 온라인 뱅킹 같은 데이터 정합성이 중요한 경우
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void repeatableReadExample() {
accountRepository.findBalanceById(1);
}
예제(Phantom Read 발생 가능)
- 트랜잭션 A가
SELECT * FROM accounts WHERE balance > 1000;
실행 - 트랜잭션 B가 새로운 계좌를 추가하고
COMMIT
- 트랜잭션 A가 같은
SELECT
실행 시 새로운 행이 포함됨 (데이터 추가됨)
트랜잭션 내에서 동일한 데이터 조회 시 항상 같은 값이 보장됩니다. 값이 보장되는데 팬텀리드가 왜 생기느냐? 행이 추가되거나, 삭제되는 경우에 발생하는 것이 팬텀리드입니다. 같은 데이터를 조회하면 동일한 값을 보여주지만 예제와 같이 조회했을 때, 새로운 행이 포함되거나, 기존에 있던 행이 사라지는 경우가 발생합니다. 이를 팬텀리드라고 하는 것입니다.
4. SERIALIZABLE (가장 높은 격리 수준, 성능 저하 가능성 있음)
모든 트랜잭션을 순차적으로 실행 → 완벽한 데이터 일관성을 보장하지만 성능 저하 가능
문제점: 성능 저하 (트랜잭션이 직렬화되어 실행되기 때문)
사용 예시: 재고 관리, 금융 시스템과 같이 데이터 정합성이 최우선인 경우
@Transactional(isolation = Isolation.SERIALIZABLE)
public void serializableExample() {
accountRepository.findBalanceById(1);
}
예제(완벽한 데이터 정합성 보장)
- 동시에 실행되는 트랜잭션이 없도록 강제
- 하나의 트랜잭션이 끝날 때까지 다른 트랜잭션이 대기
- Dirty Read, Non-Repeatable Read, Phantom Read 모두 방지됨
성능 저하가 크므로 대량의 동시 요청이 있는 환경에서는 비효율적입니다.
트랜잭션 격리 수준 비교
격리 수준 | Dirty Read | Non-Repeatable Read | Phantom Read | 성능 | 사용 예시 |
---|---|---|---|---|---|
READ UNCOMMITTED | O | O | O | 🔥 빠름 | 거의 사용 안 함 |
READ COMMITTED | ❌ | O | O | ✅ 보통 | 기본값 (일반적인 애플리케이션) |
REPEATABLE READ | ❌ | ❌ | O | ⚠️ 약간 느림 | 금융 거래, 주문 시스템 |
SERIALIZABLE | ❌ | ❌ | ❌ | ⛔️ 매우 느림 | 은행 시스템, 중요 데이터 |
일반적으로 READ COMMITED
(기본값) 또는 REPEATABLE READ
를 사용합니다.
데이터 정합성이 중요한 경우 SERIALIZABLE
을 고려하되, 성능 저하를 주의해야 합니다.
스프링에서 트랜잭션의 격리 수준을 어떻게 조정하느냐에 따라 성능과 데이터 일관성이 크게 달라질 수 있습니다.
- 성능이 중요하다면
READ COMMITTED
(기본값) 사용하고 - 데이터 정합성이 중요하다면
REPEATABLE READ
또는SERIALIZABLE
고려해야 합니다. READ UNCOMMITTED
는 일반적으로 사용하지 않습니다.
각 애플리케이션의 요구사항에 맞는 격리 수준을 선택하여 안정적인 트랜잭션 관리를 수행할 수 있도록 조치하면 될 것 같습니다.
이 분이 그림도 그리고 더욱 잘 설명해주셨으니 더욱 궁금하시다면 방문해보시기 바랍니다. 참고로 MySQL기준입니다.
https://mangkyu.tistory.com/299
'개발자 공부 > Database' 카테고리의 다른 글
MySQL 2 (0) | 2023.11.01 |
---|---|
RDBS 및 MySQL 학습 (0) | 2023.10.31 |