4. C# 비동기 소켓 종료 처리 순서 (in-flight async 와 Dispose)

난이도 상 해설 보기 →

결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커 (A)(B) 는 주목 위치 힌트다.

결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 비동기 소켓 종료 처리 순서 (in-flight async 와 Dispose)
// ----------------------------------------------------------------------------
// 시나리오:
//   - 비동기 I/O(SocketAsyncEventArgs / async-await). 한 세션에 대해 여러 개의
//     비동기 작업이 동시에 in-flight 상태일 수 있다: ReceiveAsync 완료,
//     SendAsync 완료, 그리고 종료(Close).
//   - 완료 콜백/continuation 들은 스레드 풀의 여러 스레드에서 실행된다. 같은
//     세션의 서로 다른 완료가 서로 다른 스레드에서 거의 동시에 올라올 수 있다.
//   - Close 도중에도 이미 커널에 걸려 있던 Receive 완료가 뒤늦게 올라온다.
//
// 요구사항:
//   - 종료 후 콜백이 Dispose 된 객체를 건드리면 안 된다(ObjectDisposedException 금지).
//   - 소켓/버퍼는 정확히 한 번만 해제되어야 한다(이중 Dispose 금지).
//   - 모든 in-flight I/O 가 끝난 뒤에 자원을 해제해야 한다.
//
// 과제:
//   이 코드의 잠재적 문제를 모두 찾고, 왜 문제인지 설명하고,
//   수정안과 더 나은 설계를 제시하라.
//   (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================

using System;
using System.Net.Sockets;
using System.Threading.Tasks;

public sealed class Connection : IDisposable
{
    private readonly Socket _socket;
    private readonly byte[] _recvBuf = new byte[4096];   // (A)
    private bool _closed = false;                        // (B)
    private int _pendingIo = 0;                          // in-flight 비동기 작업 수

    public Connection(Socket socket) { _socket = socket; }

    public void PostRecv()
    {
        _pendingIo++;
        // 비동기 수신 시작 (continuation 으로 OnRecvComplete)
        _ = ReceiveLoopAsync();
    }

    // (C) Receive 완료 — 스레드 풀에서 실행
    private async Task ReceiveLoopAsync()
    {
        int bytes = await _socket.ReceiveAsync(_recvBuf, SocketFlags.None);
        _pendingIo--;

        if (bytes <= 0)            // 상대가 닫음 / 에러
        {
            Close();
            return;
        }
        ProcessPacket(_recvBuf, bytes);
        PostRecv();                // 다음 수신 재등록
    }

    public async Task SendAsync(byte[] data)
    {
        _pendingIo++;
        await _socket.SendAsync(data, SocketFlags.None);
        _pendingIo--;
    }

    // (D) 종료 — 어느 스레드에서든 호출될 수 있음
    public void Close()
    {
        if (_closed) return;
        _closed = true;
        Dispose();                 // (E) 즉시 자원 해제
    }

    public void Dispose()
    {
        _socket.Dispose();         // (F) in-flight Receive/Send 가 남아있어도 즉시 Dispose
    }

    private void ProcessPacket(byte[] buf, int n) { /* ... */ }
}
내 리뷰 · C#
내 답안 · 자동 저장

작성 후 위 해설 보기에서 모범 해설과 대조하세요.