트랜잭션 경쟁 조건: 풀에서 BEGIN이 모든 것을 깨는 이유
Source: Dev.to
위에 제공된 링크 외에 번역할 텍스트가 포함되어 있지 않습니다. 번역을 원하는 본문을 제공해 주시면 한국어로 번역해 드리겠습니다.
Problem
// ❌ Dangerous: Transaction on pool
async function transferFunds(from, to, amount) {
await pool.query('BEGIN');
await pool.query('UPDATE accounts SET balance = balance - $1 WHERE id = $2', [
amount,
from,
]);
await pool.query('UPDATE accounts SET balance = balance + $1 WHERE id = $2', [
amount,
to,
]);
await pool.query('COMMIT');
}
PostgreSQL 풀은 클라이언트 연결들의 집합입니다. 각 pool.query()는 서로 다른 클라이언트를 사용할 수 있습니다.
- 요청 1:
pool.query('BEGIN')→ 클라이언트 A - 요청 1:
pool.query('UPDATE…')→ 클라이언트 B (다름!) - 요청 2:
pool.query('BEGIN')→ 클라이언트 A (재사용!)
이제 트랜잭션이 여러 클라이언트에 걸쳐 분산되어 데이터 불일치가 발생합니다.
안전한 접근 방식
// ✅ Safe: Get dedicated client, use it for entire transaction
async function transferFunds(from, to, amount) {
const client = await pool.connect();
try {
await client.query('BEGIN');
await client.query(
'UPDATE accounts SET balance = balance - $1 WHERE id = $2',
[amount, from],
);
await client.query(
'UPDATE accounts SET balance = balance + $1 WHERE id = $2',
[amount, to],
);
await client.query('COMMIT');
} catch (e) {
await client.query('ROLLBACK');
throw e;
} finally {
client.release();
}
}
BEGIN, 모든 쿼리, 그리고 COMMIT에 동일한 클라이언트를 사용하면 트랜잭션 무결성이 보장됩니다.
풀에서 직접 실행할 경우 실패하는 경우
// ❌ pool.query('BEGIN') → Error
// ❌ pool.query('COMMIT') → Error
// ❌ pool.query('ROLLBACK') → Error
// ❌ pool.query('SAVEPOINT') → Error
정상 동작하는 경우
// ✅ client.query('BEGIN') → OK
// ✅ pool.query('SELECT…') → OK (no transaction)
린팅 규칙: no-transaction-on-pool
ESLint 플러그인을 설치합니다:
npm install --save-dev eslint-plugin-pg
설정 방법:
import pg from 'eslint-plugin-pg';
export default [pg.configs.recommended];
이 규칙은 모든 오용을 감지합니다:
src/transfer.ts
3:9 error 🔒 CWE-362 | Transaction command on pool - use pool.connect() for transactions
Fix: const client = await pool.connect(); client.query('BEGIN');
재사용 가능한 트랜잭션 래퍼
// ✅ Reusable transaction wrapper
async function withTransaction(callback) {
const client = await pool.connect();
try {
await client.query('BEGIN');
const result = await callback(client);
await client.query('COMMIT');
return result;
} catch (e) {
await client.query('ROLLBACK');
throw e;
} finally {
client.release();
}
}
// Usage
await withTransaction(async (client) => {
await client.query(
'UPDATE accounts SET balance = balance - $1 WHERE id = $2',
[amount, from],
);
await client.query(
'UPDATE accounts SET balance = balance + $1 WHERE id = $2',
[amount, to],
);
});
사용 시나리오
| 시나리오 | 권장 API |
|---|---|
| 단일 쿼리 | pool.query() |
| 여러 독립 쿼리 | pool.query() |
트랜잭션 (BEGIN/COMMIT) | pool.connect() → client.query() |
| 장기 실행 세션 | pool.connect() → client.query() |
설치 및 리소스
npm install --save-dev eslint-plugin-pg
import pg from 'eslint-plugin-pg';
export default [pg.configs.recommended];
- npm 패키지:
eslint-plugin-pg - 규칙 문서: no-transaction-on-pool
- GitHub: https://github.com/your-repo/eslint-plugin-pg
경쟁 상태가 데이터를 손상시키지 않도록 하세요. ⭐️ 업데이트를 위해 GitHub에서 저장소에 ⭐️ 별표를 달아 주세요.