5. 예외 경로의 커넥션/트랜잭션 누수 (풀 고갈 / 잠금 장기 보유)
난이도 하내 리뷰 · C#
내 리뷰 · C++
해설 · C#
해설 — 예외 경로의 커넥션/트랜잭션 누수 (풀 고갈 / 잠금 장기 보유)
난이도: 하
요약
SaveCharacter 는 커넥션 대여(A)·트랜잭션 시작(B) 후 곧장 UPDATE 들을 실행하고, 반납(E)과 커밋(D)을 정상 흐름에만 둔다. 중간 Execute(C)가 예외를 던지면 (D)(E)에 도달하지 못해, 트랜잭션이 롤백되지 않고 커넥션이 풀로 반납되지 않는다. 이런 저장이 반복되면 풀의 커넥션이 하나씩 새어나가 결국 풀 고갈 → 이후 모든 Rent() 가 대기/타임아웃하며 저장 전체가 멈춘다. 더해 열린 채 버려진 트랜잭션이 행 잠금을 계속 쥐고 있어 다른 트랜잭션이 막히고 데드락이 번진다.
문제점
1. (자원 누수) 예외 시 커넥션 미반납 — (A)(E)
- 증상:
Execute예외 →_pool.Return(conn)미실행. 커넥션이 "대여 중" 상태로 영영 남는다. - 재현조건: 중간 UPDATE 가 데드락 피해자/타임아웃/제약위반으로 throw. 간헐적이어도 누적된다.
- 근본원인: 정리(반납)가
try/finally/using으로 보장되지 않고 정상 경로에만 있다.
2. (트랜잭션 누수 / 잠금 보유) 예외 시 미롤백 — (B)(D)
- 증상: 커밋도 롤백도 안 된 트랜잭션이 열린 채 남아 행/인덱스 잠금을 유지. 다른 저장이 그 행을 못 건드려 블로킹·데드락 확산.
- 근본원인: 실패 시
tx.Rollback()을 부르는 경로가 없다.
3. (연쇄 장애) 풀 고갈 → 가용성 붕괴
- 누수가 쌓이면 풀이 비어
Rent()가 타임아웃. 일부 저장의 일시적 오류가 전체 저장 불능으로 증폭된다(부분 장애의 전면화).
수정안
using/try-finally 로 커밋 여부와 무관하게 항상 롤백·반납을 보장한다.
public void SaveCharacter(Character c)
{
IDbConnection conn = _pool.Rent();
try
{
IDbTransaction tx = conn.BeginTransaction();
try
{
conn.Execute(tx, "UPDATE stats ...", c.Stats);
conn.Execute(tx, "UPDATE inventory ...", c.Inventory);
conn.Execute(tx, "UPDATE quests ...", c.Quests);
tx.Commit();
}
catch
{
tx.Rollback(); // 실패 시 잠금 즉시 해제
throw;
}
}
finally
{
_pool.Return(conn); // 성공/실패/예외 모두 반납
}
}
- 더 깔끔하게는
IDisposable을 구현해using var conn = _pool.Rent();(Dispose 가 반납),using var tx = ...(Dispose 가 미커밋 시 롤백) 패턴으로 정리한다. - 커넥션을 풀에 되돌릴 때 트랜잭션이 깨끗한 상태인지(미완료 트랜잭션 없음) 검증하거나, 의심되면 커넥션을 폐기·재생성한다.
더 나은 설계
- 풀 대여/반납과 트랜잭션 커밋/롤백을 RAII/
using으로 캡슐화한 단위 작업(UnitOfWork) 으로 감싸 호출부에서 실수할 여지를 없앤다. Rent()에 타임아웃을 두고, 풀 사용률·대여 시간·누수 의심(오래 점유된 커넥션)을 메트릭으로 관측해 누수를 조기 발견.- 일시적 오류(데드락/타임아웃)는 짧은 백오프 후 재시도하되, 재시도도 같은 정리 보장 안에서 수행.
면접 포인트
- 빌린 자원(커넥션·트랜잭션·락)의 해제는 정상 경로가 아니라
finally/using/RAII 에 둬야 예외에도 보장된다. - 열린 채 버려진 트랜잭션은 잠금을 계속 쥐어 국소 오류를 시스템 전역 블로킹으로 키운다 — 실패 시 즉시 롤백이 중요.
- 커넥션 풀 누수는 부분 장애를 전면 장애로 증폭한다. 정리 보장 + 풀 관측(대여 시간/사용률)으로 막는다.
해설 · C++
해설 — 예외 경로의 커넥션/트랜잭션 누수 (풀 고갈 / 잠금 장기 보유)
난이도: 하
요약
saveCharacter 는 커넥션 대여(A)·트랜잭션 시작(B) 후 UPDATE 들을 실행하고, 반납(E)·커밋(D)을 정상 흐름에만 둔다. 중간 execute(C)가 예외를 던지면 스택이 풀리며 (D)(E)를 건너뛰어, 트랜잭션이 롤백되지 않고 커넥션이 풀로 돌아가지 않는다. C++ 에는 GC 가 없으니 이 누수는 회수되지 않는다. 반복되면 풀이 말라 이후 rent() 가 무한 대기/타임아웃하고, 버려진 트랜잭션이 행 잠금을 계속 쥐어 다른 저장을 막고 데드락을 확산시킨다.
문제점
1. (자원 누수) 예외 시 커넥션 미반납 — (A)(E)
- 증상:
execute예외 →giveBack(conn)미실행. 커넥션이 "대여 중"으로 영구 점유. - 재현조건: 중간 UPDATE 가 데드락 피해자/타임아웃/제약위반으로 throw.
- 근본원인: 정리를 RAII 가 아니라 정상 경로 코드에 의존. 예외 시 호출 자체가 스킵된다.
2. (트랜잭션 누수 / 잠금 보유) 예외 시 미롤백 — (B)(D)
- 증상: 열린 트랜잭션이 잠금을 쥔 채 남아 다른 저장을 블로킹.
- 근본원인: 실패 시
tx->rollback()경로 부재.
3. (연쇄 장애) 풀 고갈로 가용성 붕괴
- 누수 누적 → 풀 고갈 →
rent()타임아웃 → 저장 전면 불능. 일시 오류가 전역 장애로 증폭.
수정안
RAII 가드로 스코프 종료(정상/예외 무관) 시 롤백·반납을 자동화한다.
class PooledConn {
public:
PooledConn(ConnectionPool& p, DbConnection* c) : pool_(p), conn_(c) {}
~PooledConn() { if (conn_) pool_.giveBack(conn_); } // 스코프 종료 시 항상 반납
DbConnection* operator->() { return conn_; }
DbConnection* get() { return conn_; }
private:
ConnectionPool& pool_; DbConnection* conn_;
};
class TxGuard {
public:
explicit TxGuard(DbTransaction* tx) : tx_(tx) {}
~TxGuard() { if (!committed_ && tx_) tx_->rollback(); } // 미커밋이면 롤백
void commit() { tx_->commit(); committed_ = true; }
private:
DbTransaction* tx_; bool committed_ = false;
};
void CharacterRepository::saveCharacter(const Character& c) {
PooledConn conn(pool_, pool_.rent()); // 소멸 시 반납 보장
TxGuard tx(conn->beginTransaction()); // 소멸 시 미커밋이면 롤백
conn->execute(tx_raw(tx), "UPDATE stats ...", c);
conn->execute(tx_raw(tx), "UPDATE inventory ...", c);
conn->execute(tx_raw(tx), "UPDATE quests ...", c);
tx.commit(); // 성공해야 commit, 아니면 ~TxGuard 가 롤백
}
- 핵심은 정리를 소멸자(RAII) 에 두는 것. 예외로 스택이 풀려도
~PooledConn/~TxGuard가 반드시 실행된다. - 반납 전 커넥션 상태를 점검하거나, 의심되면 폐기·재생성.
더 나은 설계
- 대여/반납·커밋/롤백을 UnitOfWork RAII 타입으로 캡슐화해 호출부가 정리를 잊을 수 없게 한다.
rent()타임아웃 + 풀 사용률·대여 시간·장기 점유를 메트릭으로 관측해 누수 조기 발견.- 일시 오류는 백오프 후 재시도하되 같은 RAII 정리 안에서 수행.
면접 포인트
- C++ 자원 정리의 정답은 RAII 다 — 예외 안전성(최소 basic guarantee)은 소멸자 기반 정리로 얻는다.
try/catch수동 정리는 누락되기 쉽다. - 버려진 트랜잭션은 잠금을 쥐어 국소 오류를 전역 블로킹으로 키운다. 실패 시 즉시 롤백이 핵심.
- 커넥션 풀 누수는 부분 장애를 전면 장애로 증폭한다 — RAII 정리 + 풀 관측으로 차단한다.