트랜잭션 안에서 에러를 삼키면 COMMIT 이 조용히 ROLLBACK 된다 (node-postgres)
실패해도 되는 조회를 try/catch 로 삼켰더니 앞선 UPDATE 까지 사라졌습니다. aborted 트랜잭션의 COMMIT 이 에러 없이 ROLLBACK 이 되는 이유와 SAVEPOINT 로 막는 방법입니다.
파일을 휴지통으로 옮기는 상태 전이를 UPDATE 하는 트랜잭션 안에서, "읽어 두면 좋지만 실패해도 전이는 막지 않는" 조회를 하나 더 했습니다. try/catch 로 감싸고 실패하면 null 을 돌려주게 했습니다.
그런데 이 조회가 실패하는 순간, 앞서 한 UPDATE 까지 같이 사라졌습니다.
try/catch 로 감쌌는데 왜 사라질까요
PostgreSQL 은 트랜잭션 안에서 문장 하나가 실패하면 트랜잭션 전체가 aborted 상태가 됩니다. 이후 문장은 전부 current transaction is aborted, commands ignored until end of transaction block 으로 거부됩니다.
더 나쁜 건 마지막 COMMIT 입니다. aborted 트랜잭션에 COMMIT 을 보내면 서버는 에러 없이 ROLLBACK 을 수행하고, 명령 태그로 ROLLBACK 을 돌려줍니다. node-postgres 의 client.query("COMMIT") 은 예외를 던지지 않습니다.
그래서 앞선 UPDATE 는 통째로 사라지는데 애플리케이션은 성공 경로로 계속 갑니다. 응답도 성공이고, 검색 색인이나 감사 로그, 이벤트 같은 후속 부수효과는 "전이됨"으로 실행됩니다.
실패해도 막지 않겠다는 의도와 정반대로, 실패가 본 작업을 조용히 취소한 셈입니다.
가두거나 던지거나
방법은 둘 중 하나입니다.
SAVEPOINT로 감쌉니다. 실패해도 되는 문장만 부분 롤백합니다.- 삼키지 말고 던집니다. 트랜잭션 전체를 실패시켜 호출자가 알게 합니다.
그리고 트랜잭션 헬퍼에서 COMMIT 결과의 command 를 확인해 ROLLBACK 이면 throw 하게 하면, 같은 종류의 조용한 유실을 한 곳에서 막을 수 있습니다.
// 나쁜 예: 삼키면 트랜잭션이 aborted 가 되고, COMMIT 이 ROLLBACK 이 된다
await client.query("BEGIN");
await client.query("UPDATE items SET status = 'trashed' WHERE id = $1", [id]);
let extra = null;
try { extra = (await client.query("SELECT ... possibly failing ...")).rows; } catch { /* ignore */ }
const r = await client.query("COMMIT"); // 예외 없음, r.command === "ROLLBACK"
// 좋은 예 1: SAVEPOINT 로 실패를 가둔다
await client.query("SAVEPOINT optional_read");
try {
extra = (await client.query("SELECT ...")).rows;
await client.query("RELEASE SAVEPOINT optional_read");
} catch {
await client.query("ROLLBACK TO SAVEPOINT optional_read");
extra = null;
}
// 좋은 예 2: 헬퍼에서 조용한 롤백을 잡는다
const res = await client.query("COMMIT");
if (res.command === "ROLLBACK") throw new Error("transaction was aborted; COMMIT rolled back");에러 없이 성공하는 실패가 제일 찾기 어렵습니다. 트랜잭션 헬퍼에 COMMIT 검사를 넣어 두는 건 싸고 확실한 방어였습니다.
참고
- PostgreSQL SAVEPOINT: https://www.postgresql.org/docs/current/sql-savepoint.html
- PostgreSQL COMMIT: https://www.postgresql.org/docs/current/sql-commit.html
- PostgreSQL Transactions tutorial: https://www.postgresql.org/docs/current/tutorial-transactions.html