우당탕탕
MySQL 트랜잭션 격리수준 선택하며 겪은 실수와 비교 사례 본문
MySQL에서 트랜잭션 격리수준을 설정하다가 의외로 실수를 많이 했어요. 특히 데이터 정합성은 물론 성능에도 영향을 주다 보니 한 번 꼼꼼히 살펴보고 적용할 필요가 있었거든요.
이 글에서는 제가 직접 겪은 트랜잭션 격리수준 관련 문제 사례를 중심으로, 타사례들과 비교하며 어떤 선택이 현명한지 정리해봤습니다. 쿼리 실행 결과도 같이 첨부해서 이해하기 쉽게 풀었어요.
개발 환경 / 버전 정보
제가 쓰던 서버는 MySQL 8.0.28이고, 트랜잭션은 주로 InnoDB 스토리지 엔진에서 수행했습니다. 애플리케이션은 Spring Boot 2.7 환경에서 JDBC를 통해 접근했어요.
MySQL 트랜잭션 격리수준 실수했던 사례 정리 관련 정보
제가 실수했던 격리수준 설정 방법
사실 처음에는 트랜잭션 격리수준을 그냥 기본값인 REPEATABLE READ로 두고 작업했습니다. 그런데 동시성 높은 환경에서 데이터가 꼬이거나 락 경합이 심한 문제가 있었어요.
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
당시 저는 이 레벨이 가장 무난하고 안정적이라 생각했는데, 실제로는 팬텀 리드 현상이 엄격하게 차단되는 대신 락 대기가 많이 발생해서 쿼리 병목이 생기더라고요.
또, 트랜잭션 내에서 같은 쿼리를 여러 번 실행할 때 결과가 변경되지 않아야 하는데, 특정 업데이트 케이스에서 의도치 않게 중복 처리되는 문제가 있었어요. 나중에 확인해 보니 실제로는 트랜잭션 시작 시점에만 스냅샷을 잡고 진행하는 구조여서 그랬더라고요.
타사례: READ COMMITTED 적용 후 경험한 변화
비슷한 환경에서 일하는 동료는 READ COMMITTED 수준을 적용해서 동시성 문제를 줄였다고 하더라고요. 저도 따라 해봤는데 이 부분이 확실히 달랐어요.
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
READ COMMITTED는 매 쿼리마다 최신 커밋된 데이터를 읽기 때문에 락 대기가 줄고 트랜잭션 충돌 횟수가 눈에 띄게 줄었어요. 다만 가끔 동일 트랜잭션 내에서 이전 쿼리 결과와 다른 결과가 나오기도 해서 약간 데이터 일관성이 흔들리는 느낌이 있었습니다.
아래 예시는 같은 트랜잭션 안에서 두 번 조회할 때 데이터가 바뀌는 상황입니다.
-- 트랜잭션 시작
START TRANSACTION;
SELECT stock FROM products WHERE id = 101; -- 100개
-- 다른 트랜잭션에서 재고 90개로 업데이트 커밋
SELECT stock FROM products WHERE id = 101; -- 90개
COMMIT;
이렇게 변하는 게 필요한 경우도 있어서 트레이드오프가 있더라고요.
MySQL 트랜잭션 격리수준 실수했던 사례 정리 관련 정보
실행 결과 비교: REPEATABLE READ vs READ COMMITTED
제가 테스트한 간단한 시나리오 결과를 표로 정리했어요. 트랜잭션 내 중복 읽기와 동시 업데이트 상황에서 어떻게 달라지는지 한눈에 보입니다.
| 격리수준 | 중복 읽기 결과 | 동시업데이트 반영 여부 | 락 대기 및 성능 |
|---|---|---|---|
| REPEATABLE READ | 고정된 결과 유지 | 반영 안 됨 | 락 대기 심함, 성능 저하 |
| READ COMMITTED | 변경될 수 있음 | 바로 반영 | 락 대기 적음, 성능 우수 |
표를 보면 REPEATABLE READ는 안정성은 좋은데 동시에 다수 트랜잭션이 있을 때 병목이 되는 게 문제였고, READ COMMITTED는 가볍지만 데이터 일관성 부분에서 약간씩 틀어질 수 있다는 걸 알 수 있죠.
제가 썼던 코드 예시로 비교해보기
다음은 JDBC에서 트랜잭션 격리수준을 바꾸고 쿼리를 실행하는 간단한 예시입니다. 직접 테스트해 보면서 성능과 결과를 눈으로 확인했어요.
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
public class TransactionIsolationTest {
public static void main(String[] args) throws Exception {
String url = "jdbc:mysql://localhost:3306/testdb";
String user = "root";
String password = "password";
try (Connection conn = DriverManager.getConnection(url, user, password)) {
// 격리수준 설정(REPEATABLE READ / READ COMMITTED 중 하나)
conn.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
conn.setAutoCommit(false);
// 첫 조회
PreparedStatement selectStmt = conn.prepareStatement("SELECT stock FROM products WHERE id = ?");
selectStmt.setInt(1, 101);
ResultSet rs1 = selectStmt.executeQuery();
if (rs1.next()) {
System.out.println("첫 조회 재고: " + rs1.getInt("stock"));
}
// 여기서 동시 트랜잭션에서 stock이 바뀌었다고 가정
Thread.sleep(5000); // 대기 시간 (다른 세션에서 재고 변경 커밋)
// 두 번째 조회
ResultSet rs2 = selectStmt.executeQuery();
if (rs2.next()) {
System.out.println("두 번째 조회 재고: " + rs2.getInt("stock"));
}
conn.commit();
}
}
}
이 코드를 READ COMMITTED로 바꾸면 두 번째 조회 때 바뀐 재고가 출력됩니다. 반대로 REPEATABLE READ일 땐 첫 조회 결과와 동일하게 나와요.
MySQL 트랜잭션 격리수준 실수했던 사례 정리 관련 정보
여기서 많이 틀리는 부분: 팬텀 리드와 갱신 갇힘
많이들 헷갈려하시는 게 팬텀 리드인데요, 제가 처음에 이 개념을 잘 몰라서 REPEATABLE READ가 모든 문제를 막아준다고 착각했었어요.
근데 실제로 REPEATABLE READ는 팬텀 리드를 막기 위해 갱신 갇힘(Next-Key Lock)을 사용해요. 이게 의도는 좋지만, 범위 락이 걸리다 보니 락이 불필요하게 확대되어 성능 저하가 발생하더라고요.
또 READ COMMITTED는 팬텀 리드를 허용해서 성능은 좋은데 데이터 일관성 측면에서 위험할 수 있다는 점을 감안해야 합니다.
심화: 다른 상품(Amazon Aurora)과 MySQL 비교
저희 회사는 클라우드 이전 과정에서 Amazon Aurora를 써본 적도 있는데, 이게 MySQL 호환 모드지만 트랜잭션 격리수준 처리 방식이 조금 다르더라고요.
Aurora는 기본적으로 READ COMMITTED 기반이면서도 내부 최적화를 통해 락 대기 문제를 줄여주긴 하는데, 복잡한 트랜잭션에서 일관성 문제는 MySQL과 비슷하게 주의해야 해요.
| 항목 | MySQL InnoDB | Amazon Aurora MySQL 호환 |
|---|---|---|
| 기본 격리수준 | REPEATABLE READ | READ COMMITTED 기반 최적화 |
| 락 전략 | Next-Key Lock 사용 | 락 대기 감소 위한 MVCC 최적화 |
| 성능 | 동시성 낮을 때 안정적 | 고동시성 환경에 최적화 |
| 일관성 보장 | 엄격함 (팬텀 리드 차단) | 약간 느슨함 |
자주 물어보시는 것들
Q. 트랜잭션 격리수준을 너무 높여도 문제인가요?
A. 네, REPEATABLE READ 이상은 락 경합이 커져서 병목이 생길 수 있어요. 데이터 안정성은 올라가지만 동시에 처리량은 떨어질 수 있으니 상황에 맞게 조절하는 게 중요합니다.
Q. 애플리케이션에서 격리수준을 변경하려면?
A. 대부분 JDBC 연결단에서 Connection#setTransactionIsolation 메서드로 설정할 수 있어요. 또는 MySQL 쿼리로 세션 단위 SET SESSION TRANSACTION ISOLATION LEVEL ...도 가능하고요.
Q. 트랜잭션 격리수준과 MVCC는 어떻게 조화되나요?
A. MVCC는 읽기 락 없이 여러 버전을 관리해서 동시성 향상에 기여하는데, 격리수준에 따라 어떤 버전을 읽을지 결정해요. 예를 들어 REPEATABLE READ에서는 트랜잭션 시작 시점 스냅샷을 고정해서 읽습니다.
마무리하자면, MySQL에서 트랜잭션 격리수준을 단순히 기본값에 맡기기보다 직접 상황에 맞게 테스트하면서 선택하시면 데이터 안정성과 성능 두 마리 토끼를 잡기 훨씬 수월합니다. 다른 상품과 비교해보면서 어떤 특성이 우리 서비스에 맞는지도 판단해 보시면 좋겠어요.
📌 함께 보면 좋은 글
'Database' 카테고리의 다른 글
| MySQL 슬로우 쿼리 잡으면서 알게 된 원인들과 해결 절차 (0) | 2026.07.03 |
|---|---|
| Elasticsearch 검색 도입하면서 겪은 시행착오와 단계별 세팅 방법 (0) | 2026.06.19 |
| MySQL 8.0 업그레이드 후 깨진 쿼리, 저는 이렇게 해결했어요 (0) | 2026.06.19 |
| PostgreSQL JSONB 컬럼 직접 써보며 느낀 비용과 성능 차이 이야기 (0) | 2026.06.17 |
| 2026년 달라진 MySQL 인덱스 활용과 실행계획 보는 법 (0) | 2026.06.14 |
