안녕하세요. 오늘도 새로운 걸 가지고 또 찾아왔습니다.
그러고보니, 요즘 친구들은 각설이를 본적은 있을까요? 갑자기 궁금하네요.
빅뱅도 모르는 친구들이 많다네요. (충격)
저번에는 MySQL의 AUTOCOMMIT에 대해 다뤘었죠. 데이터를 무사히 DB에 반영이 되고 문제가 해결되는가 했으나... 새로운 친구가 절 반겨주더라구요. 안반겨줘도 되는데 참...
// 예시 코드
// 데이터 INSERT 성공
await connection.query('INSERT INTO users (name) VALUES ("John")');
// 하지만 바로 이어진 SELECT에서 데이터를 찾지 못함
const [rows] = await connection.query('SELECT * FROM users WHERE name = "John"');
// rows = [] // 빈 배열 반환
DB에는 존재하는데... 왜 select가 안되는거지....?
커밋을 해서 DB에 잘반영이 되었는데...? DB에서 직접 조회도 잘되는데...?
처음에는 커넥션 풀의 설정의 문제라고 생각했습니다.
const pool = mysql.createPool({
host: 'localhost',
user: 'root',
password: 'password',
database: 'mydb',
waitForConnections: true, // 커넥션 풀이 가득 찼을 때 대기 여부
connectionLimit: 10, // 최대 커넥션 수 제한
queueLimit: 0, // 대기 큐 제한 (0은 무제한)
enableKeepAlive: true, // 커넥션 유지
keepAliveInitialDelay: 0 // keepAlive 시작 전 대기 시간
});
커넥션 풀의 설정을 위와 같이 변경하고 다시 테스트를 진행해보았지만... 문제는 해결되지 않았습니다. 이 커넥션 풀 설정들은 성능 최적화, 리소스 관리, 안정성 확보에는 도움이 되지만, 데이터 가시성의 문제와는 직접적인 관련이 없었습니다. (주륵)
데이터 동기화 시간이 필요할 수도 있다고 생각했습니다. 그래서! 대기 시간을 한번 추가해봤죠!
async function insertAndSelect() {
await connection.query('INSERT INTO users (name) VALUES ("John")');
// 1초 대기 추가
await new Promise(resolve => setTimeout(resolve, 1000));
const [rows] = await connection.query('SELECT * FROM users WHERE name = "John"');
}
결과는 슬프게도 역시 여전히 데이터를 찾지 못하더군요... (주륵주륵)
계속해서 원인을 파악하던 중, 트랜잭션 격리 수준이라는걸 발견하게 되었습니다.
💡 MySQL의 트랜잭션 격리 수준 4가지
- READ UNCOMMITTED
커밋되지 않은 데이터도 읽을 수 있음
데이터 정합성 문제 발생
- READ COMMITTED
커밋된 데이터만 읽을 수 있음
트랜잭션 없이도 최신 데이터 확인 가능
- REPEATABLE READ (MySQL 기본값)
트랜잭션 시작 시점의 스냅샷을 사용
데이터 일관성이 높음
- SERIALIZABLE
가장 엄격한 격리 수준
성능 저하가 발생할 수 있음
먼저, MySQL의 현재 격리 수준을 확인해보았습니다.
SELECT @@transaction_isolation;
-- 결과: REPEATABLE-READ
결과는 기본 값인 REPEATABLE-READ으로 설정이 되어있었습니다.
해당 값에 대해 좀 더 자세히 설명드리겠습니다.
💡 REPEATABLE READ의 특징
- "안전하고 일관된 데이터 읽기"를 보장하기 위해 스냅샷을 사용
- 트랜잭션이 시작되면 해당 시점의 데이터베이스 스냅샷을 생성
- 트랜잭션 내에서의 모든 읽기는 이 스냅샷을 기준으로 함
- 트랜잭션 없는 SELECT는 과거 시점의 데이터를 볼 수 있음
💡 스냅샷이란?
특정 시점의 데이터베이스 상태를 "찍어둔 것"이라고 생각하면 됨
REPEATABLE READ의 특징을 보시면, 트랜잭션이 시작되면! 트랜잭션이 시작되면!! 해당 시점의 데이터베이스 스냅샷을 생성한다고합니다. 그러면 트랜잭션을 시작하면 되겠네요!
이 메소드를 통해 트랜잭션을 시작하여 해당 시점의 데이터베이스 스냅샷을 생성하면 select가 되겠죠?! 아래 예시를 보시면 더 잘이해가 되실겁니다!!
// 1. 트랜잭션 없는 SELECT
const [rows] = await connection.query('SELECT FROM users');
// MySQL: "음... 안전하게 확인된 이전 상태를 보여줄게요"
// (마지막으로 안정적이라고 판단된 DB 상태)
// 2. 트랜잭션을 시작한 SELECT
await connection.beginTransaction();
const [rows] = await connection.query('SELECT FROM users');
// MySQL: "네! 지금 현재 시점의 모든 커밋된 데이터를 보여드릴게요"
// (현재 시점의 실제 DB 상태)
위의 문제상황에서 발생했던 코드와 비교해보면!
beginTransaction()을 통해 최신 상태의 스냅샷을 생성하여 Select문을 실행하였습니다.
// 문제 발생 코드
await connection.query('INSERT INTO users (name) VALUES ("John")');
const [rows] = await connection.query('SELECT FROM users WHERE name = "John"');
// rows = [] // 데이터를 찾지 못함
// 해결된 코드
await connection.query('INSERT INTO users (name) VALUES ("John")');
await connection.beginTransaction(); // 현재 시점의 스냅샷 생성
const [rows] = await connection.query('SELECT FROM users WHERE name = "John"');
// 방금 INSERT한 "John" 데이터가 정상적으로 조회됨!
따란!
문제가 해결되었습니다.
이렇게 SELECT 전에 beginTransaction()을 추가하는 것만으로도
방금 INSERT한 데이터를 정확하게 찾을 수 있게 되었습니다! 🎉