API 5초 지연의 진짜 원인: 포트 고갈과 TCP 패킷 유실 메커니즘 분석

1. 서론

지난 포스팅에서 우리는 SimpleClientHttpRequestFactory 사용 시 Connection Pool 부재로 인해 발생하는 포트 고갈 현상과 그로 인한 장애에 대해 알아보았습니다. 하지만 여기서 한 가지 더 깊은 의문이 생깁니다. “포트가 부족하다면 즉시 에러가 나야지, 왜 하필 ‘5초’라는 애매한 시간 동안 멈춰 있다가 성공하거나 실패하는 걸까?”

많은 개발자가 이 대기 시간을 단순히 “빈 포트가 날 때까지 줄 서서 기다린 시간”으로 오해하곤 합니다. 하지만 운영체제(OS)의 네트워크 스택은 그렇게 친절하지 않습니다. 오늘 다룰 내용은 단순한 자원 부족을 넘어, 네트워크 패킷이 소멸하고 다시 살아나는 TCP 프로토콜의 심연에 관한 이야기입니다. 서버의 CPU와 메모리가 텅텅 비어있음에도 불구하고 API가 느려지는 미스터리한 현상, 그 뒤에 숨겨진 TCP 재전송(Retransmission)과 패킷 유실(Packet Drop) 메커니즘을 낱낱이 파헤쳐 보겠습니다.


2. 본론

포트 부족 시 OS의 동작 – 대기인가, 즉시 실패인가?

먼저 가장 많이 하는 오해부터 바로잡아야 합니다. OS의 가용 포트가 0개가 되면 애플리케이션은 대기(Queueing)하지 않습니다. 즉시 실패(Fail Fast)합니다.

1. 포트 100% 고갈 시의 현상

만약 리눅스 서버에서 가용할 수 있는 임시 포트(Ephemeral Port) 28,000개가 모두 사용 중이라면, 자바 애플리케이션은 연결을 시도하는 즉시 java.net.BindException: Address already in use 또는 No buffer space available 에러를 뱉어냅니다. 이는 은행 창구에서 “번호표 마감되었습니다”라고 안내받고 바로 쫓겨나는 것과 같습니다. 기다릴 기회조차 주지 않습니다.

2. 5초 지연의 진짜 의미 (아수라장 효과)

그렇다면 우리가 겪은 5초 지연은 무엇일까요? 포트가 완전히 0개가 된 상태가 아니라, “거의 고갈되어 OS가 극한의 과부하에 걸린 아수라장 상태“였기 때문입니다. 이때 발생한 5초는 포트를 받기 위해 대기한 시간이 아니라, 연결 요청(Handshake)을 시도했으나 무시당해서 다시 시도하느라 걸린 시간입니다. 즉, 정상적인 처리가 아니라 비정상적인 재전송(Retransmission)이 발생하고 있었던 것입니다.

서버를 마비시키는 TCP 재전송(Retransmission) 메커니즘

이 5초라는 시간은 우연히 발생한 것이 아닙니다. TCP 프로토콜의 표준 규약에 따라 수학적으로 계산된 시간입니다.

1. 패킷 유실 (Packet Drop) 발생

TPS 300 상황에서 SimpleClient를 사용하면 초당 300번의 연결과 해제가 반복됩니다. 이때 OS 커널은 수만 개의 TIME_WAIT 소켓을 관리하느라 과부하에 걸리고, SYN Backlog(연결 대기열)가 꽉 차게 됩니다.

  • 정상 상황: Client가 SYN을 보내면 Server는 SYN+ACK로 응답합니다.
  • 장애 상황: 대기열이 꽉 찬 OS는 들어오는 SYN 패킷을 처리하지 못하고 조용히 버립니다(Drop). “잠깐 기다려”라는 응답조차 주지 않고 무시해 버리는 것입니다.

2. 지수적 후퇴 (Exponential Backoff)와 RTO

요청을 보낸 내 서버(Client)는 상대방이 패킷을 버렸는지 알 길이 없습니다. 그래서 응답이 올 때까지 기다리다가, 일정 시간이 지나면 다시 보냅니다. 이때 대기 시간(RTO)은 재시도할 때마다 2배씩 늘어납니다.

  • 0초 (최초 시도): 연결 요청(SYN) 전송 → OS가 바빠서 Drop
  • +1초 (1차 재전송): “1초 동안 답이 없네? 다시 보내자.” → Drop
  • +3초 (2차 재전송): “또 없네? 이번엔 2초 더 기다려보자.” (누적 1+2=3초)
  • +7초 (3차 재전송): “이번엔 4초 더…” (누적 1+2+4=7초)

사용자가 겪은 약 5초의 지연은 2차 재전송(3초) 이후 연결에 성공했거나, 처리 시간까지 포함되어 발생한 필연적인 결과였습니다. 패킷 하나를 놓칠 때마다 지연 시간은 기하급수적으로 늘어납니다.

하드웨어 스펙과 논리적 자원의 한계 (CPU/RAM vs Port)

“서버 사양을 높이면 해결되지 않을까?”라는 질문을 많이 받습니다. 결론은 “해결되지 않는다“입니다. 이는 물리적 체력의 문제가 아니라 논리적 티켓의 문제이기 때문입니다.

1. 물리적 자원 (CPU/RAM)

  • 서버의 메모리가 32GB라도, TIME_WAIT 소켓 3만 개가 차지하는 메모리는 고작 90MB 수준입니다.
  • CPU 역시 바쁘긴 하겠지만, 단순히 소켓 상태를 관리하는 것만으로 최신 서버를 다운(Shutdown)시킬 만큼의 부하를 주기는 어렵습니다.
  • 즉, 서버 하드웨어는 멀쩡하게 살아있습니다.

2. 논리적 자원 (Port)

  • 문제는 포트(Port)입니다. 리눅스 OS가 외부 통신용으로 내어줄 수 있는 포트는 약 28,000개로 고정되어 있습니다.
  • 아무리 CPU가 빠른 슈퍼컴퓨터라도, 나가는 문(Port)이 28,000개로 한정되어 있고 그 문이 모두 잠겨있다면(TIME_WAIT) 통신은 불가능합니다.
  • 이를 “식물인간 상태“라고 비유할 수 있습니다. 생명 유지 장치(CPU/RAM)는 돌아가지만, 외부와 소통(Network)은 끊긴 상태입니다.

따라서 하드웨어 스케일업(Scale-up)보다는, Connection Pool을 도입하여 한 번 맺은 연결(포트)을 끊지 않고 재사용하는 소프트웨어적 튜닝만이 유일한 해결책입니다. Connection Pool을 쓰면 재전송 도박을 할 필요 없이 열려있는 문으로 드나들기만 하면 되므로, 5초 지연 문제는 원천적으로 차단됩니다.


3. 결론

이번 심층 분석을 통해 API 5초 지연의 진짜 범인은 단순한 자원 부족이 아닌, TCP 패킷 유실로 인한 재전송 대기 시간임이 밝혀졌습니다.

OS 레벨에서의 포트 고갈은 대기열을 만들어주는 자비로운 동작이 아니라, 들어오는 요청을 가차 없이 버리고(Drop) 즉시 에러를 뱉는 냉혹한 환경입니다. 우리가 겪은 지연은 그 냉혹한 환경 속에서 살아남기 위해 TCP 프로토콜이 처절하게 재시도(Retransmission)를 반복했던 흔적입니다.

이제 우리는 왜 Connection Pool이 필수적인지 명확히 알게 되었습니다. 그것은 단순히 성능을 높이는 옵션이 아니라, 한정된 자원(Port)과 가혹한 네트워크 환경(Packet Drop) 속에서 안정적인 서비스를 유지하기 위한 유일한 생존 수단입니다. 이 글이 여러분의 시스템을 더욱 견고하게 만드는 통찰이 되기를 바랍니다.

댓글 남기기