본문 바로가기
Q&A2026년 8월 17일2분 읽기

특정 UPDATE만 계속 멈춰있는데 락이 안 풀려요 (idle in transaction)

t
tx_seoyeon
조회 434댓글 3

일부 주문 건 UPDATE가 응답 없이 계속 멈춰 있어서 pg_stat_activity를 봤더니 이런 상태였어요.

SELECT pid, state, wait_event_type, xact_start, query
FROM pg_stat_activity WHERE state <> 'idle';

  pid  |        state        | wait_event_type |     xact_start
-------+---------------------+-----------------+---------------------
 20144 | idle in transaction |                 | 00:01:12
 20330 | active              | Lock            | 00:07:40

20330번이 Lock을 기다리며 멈춰 있고, 20144번은 idle in transaction 상태로 6분 넘게 트랜잭션을 열어둔 채 아무것도 안 하고 있어요. 20144가 뭔가 락을 쥐고 안 놓는 것 같은데, 이 세션은 왜 트랜잭션을 계속 열고 있는 거고 어떻게 막아야 하나요?

댓글 3

김김영삼2개월 전
20144가 범인이 맞습니다. idle in transaction은 'BEGIN 하고 어떤 문장(십중팔구 UPDATE나 SELECT FOR UPDATE)까지 실행해서 락은 이미 잡았는데, COMMIT/ROLLBACK을 안 하고 멍 때리고 있는' 상태예요. 락은 트랜잭션이 끝나야 풀리니까, 20144가 커밋을 안 하는 한 그 행 락은 계속 잡혀 있고 20330은 영원히 기다립니다.

왜 트랜잭션을 열어둔 채 노는지가 핵심인데, 거의 항상 앱 코드 문제예요. 트랜잭션을 시작한 뒤 그 안에서 느린 작업(외부 API 호출, 파일 처리, await 걸린 무거운 로직)을 하고 있거나, 커밋을 깜빡했거나, ORM이 커넥션을 트랜잭션 중간에 놓쳤거나입니다. DB 문제가 아니라 애플리케이션이 트랜잭션을 너무 오래 붙잡고 있는 거죠.

급한 불은 SELECT pg_terminate_backend(20144); 로 그 세션을 끊으면 락이 풀리면서 20330이 진행됩니다. 다만 이건 응급처치고, 재발을 막으려면 두 가지를 하세요. 첫째, 트랜잭션 안에서는 절대 외부 I/O(HTTP 호출, 슬립 등)를 하지 마세요. 트랜잭션은 짧게 열고 짧게 닫아야 합니다. 둘째, idle_in_transaction_session_timeout을 걸어두면(예: 30s) 이런 좀비 트랜잭션을 DB가 알아서 끊어줘서 서비스 전체가 물리는 걸 막아줍니다.

혹시 트랜잭션 안에서 뭘 하고 계신지 코드 흐름 알려주시면 더 정확히 짚어드릴게요.
ttx_seoyeon(guest)2개월 전
말씀 듣고 보니 짚이는 게 있어요. 주문 상태를 바꾸는 로직을 prisma의 인터랙티브 트랜잭션(prisma.$transaction(async (tx) => {...}))으로 감싸놨는데, 그 콜백 안에서 결제사 API를 호출하고 응답을 기다린 뒤에 다음 UPDATE를 합니다. 결제 API가 느릴 때가 있는데, 혹시 이게 원인일까요? 그리고 timeout은 어느 값들을 걸어야 하나요?
김김영삼2개월 전
네, 그게 정확히 원인입니다. 인터랙티브 트랜잭션은 콜백이 다 끝날 때까지 트랜잭션을 열어둔 채로 있어요. 콜백 안에서 결제 API 응답을 기다리는 동안 내내 트랜잭션이 살아 있고, 첫 UPDATE로 잡은 행 락과 커넥션 하나를 그 시간만큼 통째로 붙잡습니다. 결제사가 몇 초씩 느려지면 그 세월 동안 다른 주문 UPDATE가 다 막히는 거죠. 지금 보신 idle in transaction이 딱 이겁니다.

외부 호출은 트랜잭션 밖으로 빼야 해요. 흐름을 바꿔서, 결제 API를 먼저 호출해 결과를 받고 그 다음에 짧은 트랜잭션으로 DB만 갱신하거나, 정합성이 중요하면 outbox 패턴 + 멱등키로 나눠 처리하는 식이 안전합니다. 트랜잭션 안에는 순수 DB 작업만 두세요.

timeout은 두 층으로 겁니다. DB 쪽은 idle_in_transaction_session_timeout(예: 30s)으로 좀비를 잘라내고, 앱 쪽은 prisma $transaction 옵션의 timeout(콜백 최대 실행시간)과 maxWait(커넥션 대기시간)를 현실적인 값으로 잡아두면 외부 호출이 늘어져도 트랜잭션이 무한정 안 살아 있습니다. 다만 근본은 어디까지나 외부 호출을 트랜잭션 밖으로 빼는 거예요. timeout은 사고를 줄여줄 뿐 구조를 고쳐주진 않습니다.
Ctrl+Enter로 등록