트랜잭션 경쟁 조건: 풀에서 BEGIN이 모든 것을 깨는 이유

발행: (2026년 1월 1일 오전 06:38 GMT+9)
4 min read
원문: Dev.to

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];

경쟁 상태가 데이터를 손상시키지 않도록 하세요. ⭐️ 업데이트를 위해 GitHub에서 저장소에 ⭐️ 별표를 달아 주세요.

Back to Blog

관련 글

더 보기 »