성능 최적화 19편 - 트랜잭션 격리 레벨(Isolation Level) 이해하고 튜닝하기
6편에서는 여러 서버가 동시에 같은 자원에 접근할 때 발생하는 문제를 분산 락으로 해결하는 방법을 다뤘습니다. 그런데 락을 걸지 않아도, 데이터베이스 자체에는 동시 트랜잭션 간의 간섭을 제어하는 격리 레벨(Isolation Level)이라는 장치가 이미 존재합니다. 이번 글에서는 격리 레벨이 무엇이고, 실무에서 어떻게 선택해야 하는지를 정리합니다.
1. 격리 레벨이 필요한 이유
여러 트랜잭션이 동시에 같은 데이터를 조회하고 수정할 때, 격리 수준을 어떻게 설정하느냐에 따라 서로 다른 부작용이 발생할 수 있습니다. 대표적으로 세 가지가 있습니다.
Dirty Read - 다른 트랜잭션이 아직 커밋하지 않은 데이터를 읽어버리는 현상
트랜잭션 A: 재고를 100 -> 50으로 변경 (아직 커밋 안 함)
트랜잭션 B: 재고를 조회 -> 50을 읽음
트랜잭션 A: 롤백 (재고는 다시 100)
-> 트랜잭션 B는 존재한 적 없는 값(50)을 기준으로 이미 로직을 처리함
Non-Repeatable Read - 같은 트랜잭션 안에서 같은 행을 두 번 조회했는데 값이 달라지는 현상
트랜잭션 A: 재고 조회 -> 100
트랜잭션 B: 재고를 100 -> 80으로 변경 후 커밋
트랜잭션 A: 같은 트랜잭션 안에서 재고 다시 조회 -> 80
-> 트랜잭션 A 입장에서는 한 트랜잭션 내에서 값이 바뀌어버림
Phantom Read - 같은 조건으로 두 번 조회했는데, 없던 행이 새로 나타나는 현상
트랜잭션 A: "status = PENDING" 조건으로 조회 -> 5건
트랜잭션 B: 새로운 주문(PENDING)을 추가 후 커밋
트랜잭션 A: 같은 조건으로 다시 조회 -> 6건
-> 없던 행이 갑자기 나타남
2. 네 가지 격리 레벨
SQL 표준은 이 부작용들을 어디까지 허용할지에 따라 네 단계의 격리 레벨을 정의합니다.
| 격리 레벨 | Dirty Read | Non-Repeatable Read | Phantom Read | 특징 |
|---|---|---|---|---|
| READ UNCOMMITTED | 발생 가능 | 발생 가능 | 발생 가능 | 가장 느슨함, 실무에서 거의 안 씀 |
| READ COMMITTED | 방지 | 발생 가능 | 발생 가능 | 대부분 DB의 기본값 |
| REPEATABLE READ | 방지 | 방지 | 발생 가능(DB별 차이) | MySQL(InnoDB)의 기본값 |
| SERIALIZABLE | 방지 | 방지 | 방지 | 가장 엄격, 성능 저하 큼 |
격리 수준이 엄격해질수록 데이터 정합성은 높아지지만, 그만큼 락을 더 오래, 더 넓게 걸게 되어 동시성(처리량)은 떨어집니다. 정합성과 성능은 트레이드오프 관계에 있습니다.
3. Spring에서 격리 레벨 지정하기
@Transactional(isolation = Isolation.READ_COMMITTED)
public void decreaseStock(Long productId, int quantity) {
Product product = productRepository.findById(productId).orElseThrow();
product.decrease(quantity);
}
@Transactional(isolation = Isolation.REPEATABLE_READ)
public OrderSummary getOrderSummary(Long orderId) {
// 같은 트랜잭션 안에서 여러 번 조회해도 일관된 값을 보장해야 하는 로직
Order order = orderRepository.findById(orderId).orElseThrow();
List<OrderItem> items = orderItemRepository.findByOrderId(orderId);
return OrderSummary.of(order, items);
}
별도로 지정하지 않으면 데이터베이스의 기본 격리 레벨을 따릅니다. MySQL(InnoDB)은 REPEATABLE READ, PostgreSQL과 SQL Server는 READ COMMITTED가 기본값이라는 점을 알아두는 것이 좋습니다.
4. 실무에서 격리 레벨을 낮게 유지해도 되는 경우
일반적인 CRUD API의 대부분은 기본 격리 레벨(READ COMMITTED)로도 충분합니다. 굳이 격리 레벨을 올려야 하는 경우는 다음과 같이 한 트랜잭션 안에서 같은 데이터를 여러 번 참조하며 정합성이 계속 유지되어야 하는 로직에 한정됩니다.
- 집계 리포트를 생성하는 동안, 중간에 데이터가 바뀌면 안 되는 배치 작업
- 하나의 트랜잭션 안에서 같은 값을 여러 번 읽고 계산에 사용하는 로직
반대로 조회 위주의 API에 무조건 SERIALIZABLE을 적용하면, 불필요하게 락 대기가 늘어나 오히려 전체 처리량이 떨어질 수 있습니다.
5. 격리 레벨만으로 해결 안 되는 경우 - 락과의 조합
Non-Repeatable Read를 막는 것과, "내가 이 값을 확인하고 수정하는 그 순간까지 아무도 못 건드리게 막는 것"은 다른 문제입니다. 재고 차감처럼 조회한 값을 기준으로 계산해서 다시 저장하는 로직은, 격리 레벨을 올리는 것만으로는 충분하지 않을 때가 많습니다.
@Transactional(isolation = Isolation.READ_COMMITTED)
public void decreaseStock(Long productId, int quantity) {
Product product = productRepository.findById(productId).orElseThrow(); // 재고 100 조회
// 이 시점과 저장 시점 사이에 다른 트랜잭션이 끼어들 수 있음
product.decrease(quantity);
}
이런 경우엔 6편에서 다룬 SELECT FOR UPDATE(비관적 락)로 조회 시점에 행 자체를 잠그거나, 버전 컬럼을 이용한 낙관적 락(@Version)을 함께 사용하는 것이 더 확실한 해결책입니다.
@Entity
public class Product {
@Id
private Long id;
private int stock;
@Version
private Long version; // 낙관적 락 - 동시 수정 시 버전 충돌로 예외 발생
}
낙관적 락은 실제 충돌이 드문 상황에서 락 대기 없이 동시성을 높게 유지하면서도, 충돌이 발생하면 예외를 던져 애플리케이션이 재시도하도록 만드는 방식입니다.
6. 격리 레벨 vs 락, 언제 무엇을 쓸까
| 상황 | 적합한 방식 |
|---|---|
| 조회 시 부정확한(아직 커밋 안 된) 데이터만 안 보면 되는 경우 | READ COMMITTED (대부분 기본값으로 충분) |
| 하나의 트랜잭션 안에서 같은 값을 여러 번 참조해야 하는 로직 | REPEATABLE READ |
| 조회한 값을 기준으로 반드시 정확하게 수정해야 하는 로직 | 비관적 락(FOR UPDATE) 또는 낙관적 락(@Version) |
| 충돌이 드물고, 락 대기 없이 높은 동시성이 필요한 경우 | 낙관적 락(@Version) |
7. 정리
- 격리 레벨은 동시 트랜잭션 간 간섭(Dirty Read, Non-Repeatable Read, Phantom Read)을 어디까지 허용할지 정하는 설정이다
- 격리 수준이 높아질수록 정합성은 높아지지만 동시성(처리량)은 떨어지는 트레이드오프 관계에 있다
- 대부분의 API는 기본 격리 레벨로 충분하며, 무조건 높은 레벨을 적용하면 오히려 성능 저하로 이어질 수 있다
- "조회 후 수정" 패턴처럼 정확한 값 기준 처리가 필요하다면, 격리 레벨보다 비관적 락이나 낙관적 락이 더 확실한 해결책이다
6편의 분산 락이 "여러 서버 간의 동시 접근"을 다뤘다면, 이번 편의 격리 레벨은 그보다 더 근본적인 "데이터베이스 자체가 동시 트랜잭션을 어떻게 다루는가"의 문제입니다. 두 개념을 함께 이해하고 있어야, 동시성 문제를 마주쳤을 때 어느 층위에서 해결해야 할지 정확히 판단할 수 있습니다.
'성능 최적화' 카테고리의 다른 글
| 성능 최적화 STEP 21 - Read Replica (1) | 2026.07.20 |
|---|---|
| 성능 최적화 STEP 20 - 네트워크 비용 줄이기 (0) | 2026.07.15 |
| 성능 최적화 STEP 18 - 번들 사이즈 분석과 축소 (0) | 2026.07.14 |
| 성능 최적화 STEP 17 - 이미지 최적화 (0) | 2026.07.14 |
| 성능 최적화 STEP 16 - 초기 로딩 속도 개선하기 (1) | 2026.07.13 |