TIL

Sequelize 트랜잭션 깜빡하고 안 닫으면 생기는 일

하얀잔디 2026. 9. 3. 16:07

Node JS 에서 나타났던 상황이지만 Java 등 모든 WAS서버에서 나올수 있는 상황

 

상황

 

DB 커넥션 풀이 꽉 차서 API가 504 뜨고, 완전 가벼운 API도 덩달아 느려지고, SequelizeConnectionAcquireTimeoutError까지 터짐. 원인 파보니 트랜잭션 하나를 안 닫고 있었음.


문제 코드


이미 있으면 그거 리턴, 없으면 새로 생성 패턴에서 나왔던 실수임.

const findOrCreateRoom = async (userId: string) => {
    const transaction = await sequelize.transaction() // 커넥션 하나 확보

    try {
        const existing = await Room.findOne({ where: { userId } })
        if (existing) {
            return existing   // ⚠️ 여기서 그냥 끝. commit도 rollback도 안 함
        }

        const created = await Room.create({ userId }, { transaction })
        await transaction.commit()
        return created
    } catch (err) {
        await transaction.rollback()
        throw err
    }
}


existing이 있는 경우(=제일 자주 타는 경로)마다 트랜잭션을 열어놓고 return 해버림. 커넥션이 풀로 안 돌아가고, DB에는 idle in transaction 세션으로 계속 쌓임. 이 함수 호출될 때마다 하나씩 새서, 풀(예: max 10개) 금방 바닥남. → 이후 요청들은 커넥션 못 받아서 줄줄이 타임아웃.

 


배운 것



1. 트랜잭션 열었으면 모든 return 경로가 commit/rollback으로 끝나야 함

if (existing) {
    await transaction.commit()  // 또는 rollback — 아무튼 닫아야 함
    return existing
}


2. { transaction } 안 넘긴 쿼리는 그 트랜잭션이랑 상관없이 따로 커넥션 잡아씀

// 이러면 findOne이 위 transaction과 별개로 커넥션 하나 더 쓴다
const existing = await Room.findOne({ where: { userId } })

// 이래야 같은 커넥션 재사용
const existing = await Room.findOne({ where: { userId }, transaction })


(단, Sequelize.useCLS()로 CLS 세팅해두면 자동으로 물려서 안 넘겨도 됨 — 안 쓰는 프로젝트라면 매번 명시해야 함)


3. commit()은 실패해도 트랜잭션을 끝내버림 그래서 catch에서 무조건 rollback() 부르면, 이미 끝난 트랜잭션에 롤백 시도하다가 새 에러(Transaction cannot be rolled back because it has been finished with state: commit)

터지면서 원래 에러가 로그에서 사라짐. commit 시도했으면 플래그 세워서 롤백 스킵하는 게 안전함.

 

let done = false
try {
    // ...
    done = true
    await transaction.commit()
} catch (err) {
    if (!done) await transaction.rollback()
    throw err
}


4. 동시 요청이 트랜잭션을 나눠쓰는 건 아님 const transaction = await sequelize.transaction()은 함수 호출마다 새로 생기는 지역변수라 요청끼리 안 섞임. 대신 동시에 열 수 있는 트랜잭션(=커넥션) 개수가 pool 크기로 제한되는 게 진짜 문제임. 그 이상 몰리면 뒤 요청은 커넥션 기다리다 타임아웃.
한 줄 요약

트랜잭션은 연 사람이 반드시 닫아야 함.


return

으로 일찍 빠져나가는 경로일수록 깜빡하기 쉬우니까 특히 조심.