← 문제로

26. 동시 처리 한도 제한기의 허가 반납 누락 (permit leak → throttle 고갈)

난이도 중
내 리뷰 · C#
해설 · C#

해설 — 동시 처리 한도 제한기의 허가 반납 누락 (permit leak → throttle 고갈)

난이도: 중

요약

WaitAsync 로 허가를 얻은 뒤 Release정상 경로(D)에서 한 번만 호출된다. 그런데 (B) 사전 점검 실패의 조기 반환과 (C) op()예외는 (D)를 건너뛴다 → 허가가 반납되지 않는다(permit leak). 거절/실패가 반복되면 내부 카운트가 0까지 줄어들고, 이후 WaitAsync영원히 깨어나지 못해 throttle 전체가 멈춘다(자원 고갈로 인한 사실상 데드락).

문제점

1. (자원 누수) 조기 반환이 허가를 반납하지 않음 — (A)(B)

  • 증상: IsHealthy()==false 마다 허가 1개가 영구 소실.
  • 재현조건: 외부 시스템이 잠시 불안정해 (B)가 자주 실패 → 매번 1개씩 누수 → N번이면 전부 소진.

2. (자원 누수) 예외가 허가를 반납하지 않음 — (C)(D)

  • 증상: op() 가 던지면 스택이 (D)를 지나치며 풀려 허가가 새고, 예외는 호출자에게 전파된다.
  • 근본원인: 획득과 반납이 try/finally 로 짝지어지지 않았다. "성공했을 때만 반납"은 자원 관리의 전형적 오류.

3. (잠재) 과다 반납 위험

  • 누수를 막겠다고 여러 경로에 Release 를 흩뿌리면 이번엔 이중 반납으로 카운트가 max 를 넘겨 SemaphoreFullException 이 나거나 동시 한도가 깨진다. 반납은 정확히 한 번이어야 한다.

수정안

획득 직후 try 에 들어가고 반납은 finally 에서 딱 한 번 한다.

public async Task<Outcome> RunAsync(Func<Task> op)
{
    await _sem.WaitAsync();                 // 획득은 try 밖(획득 실패 시 finally 가 반납하면 안 되므로)
    try
    {
        if (!IsHealthy())
            return Outcome.Rejected;        // finally 가 반납 → 누수 없음
        await op();                         // 예외가 나도 finally 가 반납
        return Outcome.Ok;
    }
    finally
    {
        _sem.Release();                     // 모든 경로에서 정확히 한 번
    }
}
  • 취소(CancellationToken)를 쓰면 WaitAsync(ct) 가 던질 수 있는데, 이때는 허가를 얻지 못한 상태이므로 위처럼 획득을 try 밖에 두는 것이 중요하다(얻지도 않은 허가를 반납하면 과다 반납).
  • 더 안전하게는 "획득→반납"을 IDisposable 핸들로 감싸 using 으로 강제한다:
public async Task<IDisposable> AcquireAsync(CancellationToken ct = default)
{
    await _sem.WaitAsync(ct);
    return new Releaser(_sem);              // Dispose() 에서 Release() 1회
}
// 사용: using (await throttle.AcquireAsync()) { await op(); }

더 나은 설계

  • 자원의 수명을 스코프에 묶는다: try/finally 또는 using(IDisposable Releaser)으로 "얻으면 반드시 돌려준다"를 컴파일러/구문이 보장하게 한다.
  • 획득 자체에 타임아웃을 둬(WaitAsync(timeout)) 고갈 시 무한 대기 대신 빠른 실패·관측이 되게 한다.
  • 한도를 넘는 부하는 거절/대기열 정책을 명시(예: 큐 길이 상한)해 백프레셔를 설계.

면접 포인트

  1. 락/세마포어/커넥션 같은 자원은 모든 종료 경로(정상·조기반환·예외)에서 반납돼야 한다 → try/finally·using.
  2. 획득과 반납은 이며 반납은 정확히 한 번 — 누수도 과다 반납도 모두 버그.
  3. 획득 실패(예외/취소) 시엔 반납하면 안 되므로, 획득을 try 밖에 두는 패턴을 안다.