Computer Science/Network

The response ended prematurely μ—λŸ¬ : 응닡을 λ°›κΈ° 전에 연결이 λ‹«νžŒλ‹€λŠ” 것

ν”„λ‘œκ·Έλž˜λ¨Έ μ˜€μ›” 2026. 6. 25.

πŸš©κ°œμš”

μ™ΈλΆ€ μ„œλ²„λ‘œ HTTP μš”μ²­μ„ λ³΄λ‚΄λŠ” μ„œλΉ„μŠ€λ₯Ό μš΄μ˜ν•˜λ‹€ 보면 λŒ€λΆ€λΆ„μ˜ μ‹€νŒ¨λŠ” μ΅μˆ™ν•œ μ–Όκ΅΄μž…λ‹ˆλ‹€. νƒ€μž„μ•„μ›ƒμ΄ λ‚˜κ±°λ‚˜, 4xx·5xx μƒνƒœ μ½”λ“œκ°€ λŒμ•„μ˜€κ±°λ‚˜, 응닡 ν˜•μ‹μ΄ λ°”λ€Œμ–΄ νŒŒμ‹±μ΄ κΉ¨μ§‘λ‹ˆλ‹€.

그런데 졜근 μ—λŸ¬ λ‘œκ·Έμ—μ„œ μƒνƒœ μ½”λ“œμ‘°μ°¨ μ—†λŠ”, 처음 λ³΄λŠ” μ—λŸ¬λ₯Ό λ§Œλ‚¬μŠ΅λ‹ˆλ‹€.

The response ended prematurely. (ResponseEnded)

“응닡이 쑰기에 μ’…λ£Œλ˜μ—ˆλ‹€”λŠ” 뜻인데, νƒ€μž„μ•„μ›ƒλ„ μ•„λ‹ˆκ³  μ„œλ²„κ°€ μ—λŸ¬λ₯Ό μ‘λ‹΅ν•œ 것도 μ•„λ‹™λ‹ˆλ‹€.

κ·Έλž˜μ„œ 이번 κΈ€μ—μ„œλŠ” 이 μ—λŸ¬κ°€ HTTP μš”μ²­μ˜ μ–΄λŠ λ‹¨κ³„μ—μ„œ, μ–΄λ–€ λ°©μ‹μœΌλ‘œ λŠμ–΄μ‘Œλ‹€λŠ” λœ»μΈμ§€, 그리고 μ™œ ν•˜ν•„ POST μš”μ²­μ—μ„œ λ“œλŸ¬λ‚¬λŠ”μ§€ μ •λ¦¬ν•΄λ³΄κ² μŠ΅λ‹ˆλ‹€. μ—λŸ¬ λ©”μ‹œμ§€λŠ” .NET(HttpClient)의 κ²ƒμ΄μ§€λ§Œ, λ‚΄μš©μ˜ λŒ€λΆ€λΆ„μ€ TCP·HTTP 이야기라 λ‹€λ₯Έ μ–Έμ–΄λ₯Ό μ“°μ‹œλŠ” λΆ„λ“€κ»˜λ„ 도움이 될 것 κ°™μŠ΅λ‹ˆλ‹€.

 

 

 

1️⃣ HTTPS μš”μ²­ ν•œ 번이 κ±°μΉ˜λŠ” 단계

μ—λŸ¬λ₯Ό μ΄ν•΄ν•˜λ €λ©΄ λ¨Όμ € μš”μ²­ ν•œ 번이 μ–΄λ–€ 단계λ₯Ό κ±°μΉ˜λŠ”μ§€ μ•Œμ•„μ•Ό ν•©λ‹ˆλ‹€. https:// 둜 μ‹œμž‘ν•˜λŠ” μ£Όμ†Œμ— μš”μ²­μ„ 보내면 ν΄λΌμ΄μ–ΈνŠΈλŠ” λ‹€μŒ μˆœμ„œλ‘œ 일을 ν•©λ‹ˆλ‹€.

 

[κ·Έλ¦Ό 1] HTTPS μš”μ²­ ν•œ 번이 κ±°μΉ˜λŠ” 5단계와 이번 μ—λŸ¬μ˜ μœ„μΉ˜

 

HTTP μƒνƒœ μ½”λ“œλŠ” 5λ‹¨κ³„μ—μ„œ μ‘λ‹΅μ˜ 첫 쀄(μƒνƒœ 라인)을 λ°›μ•„μ•Όλ§Œ μ•Œ 수 μžˆμŠ΅λ‹ˆλ‹€. κ·Έ 전에 λ¬Έμ œκ°€ 생기면 μƒνƒœ μ½”λ“œλŠ” μ‘΄μž¬ν•˜μ§€ μ•Šκ³ , λŒ€μ‹  “μ–΄λŠ λ‹¨κ³„μ—μ„œ μ‹€νŒ¨ν–ˆλŠ”κ°€”κ°€ μ—λŸ¬μ˜ 정체가 λ©λ‹ˆλ‹€.

 

 

.NET 8λΆ€ν„°λŠ” 이 ꡬ뢄을 HttpRequestError λΌλŠ” μ—΄κ±°ν˜•μœΌλ‘œ μ œκ³΅ν•©λ‹ˆλ‹€. 곡식 λ¬Έμ„œμ— μ •μ˜λœ κ°’ 쀑 일뢀λ₯Ό 단계에 맞좰 보면 λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

단계 HttpRequestError 곡식 λ¬Έμ„œμ˜ μ„€λͺ…
1. DNS NameResolutionError (1) DNS 이름 확인에 μ‹€νŒ¨
2. TCP ConnectionError (2) 원격 μ—”λ“œν¬μΈνŠΈμ— μ—°κ²°ν•˜λŠ” λ™μ•ˆ 전솑 μˆ˜μ€€ 였λ₯˜ λ°œμƒ
3. TLS SecureConnectionError (3) TLS ν•Έλ“œμ…°μ΄ν¬ 쀑 였λ₯˜ λ°œμƒ
5. 응닡 InvalidResponse (9) μœ νš¨ν•˜μ§€ μ•Šκ±°λ‚˜ ν˜•μ‹μ΄ 잘λͺ»λœ 응닡을 μˆ˜μ‹ 
5. 응닡 ResponseEnded (10) 응닡이 쑰기에 μ’…λ£Œλ¨

μ΄λ²ˆμ— λ§Œλ‚œ μ—λŸ¬λŠ” ResponseEnded, 즉 1~4λ‹¨κ³„λŠ” λͺ¨λ‘ μ§€λ‚˜κ³  5단계 응닡 μˆ˜μ‹ μ—μ„œ μΌμ–΄λ‚œ μΌμž…λ‹ˆλ‹€.

 

 

 

2️⃣ λ‘œκ·Έμ™€ μ—λŸ¬μ˜ 의미

2-1. 둜그

μ™ΈλΆ€ μ‚¬μ΄νŠΈμ— 쑰회 μš”μ²­(POST)을 보내닀가 λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€. μ˜ˆμ™Έ μ²΄μΈμ—μ„œ ν•΅μ‹¬λ§Œ 좔리면 λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

 
System.Net.Http.HttpRequestException
  : An error occurred while sending the request.
 ---> System.Net.Http.HttpIOException
  : The response ended prematurely. (ResponseEnded)

   at System.Net.Http.HttpConnection.SendAsync(...)

이 μš”μ²­μ˜ νƒ€μž„μ•„μ›ƒμ€ 6μ΄ˆμ˜€λŠ”λ°, μ‹€νŒ¨κΉŒμ§€ κ±Έλ¦° μ‹œκ°„μ€ μ•½ 0.6μ΄ˆμ˜€μŠ΅λ‹ˆλ‹€. 즉 νƒ€μž„μ•„μ›ƒμ΄ μ•„λ‹™λ‹ˆλ‹€. 기닀리닀 μ§€μΉœ 것이 μ•„λ‹ˆλΌ, 금방 “끝”을 톡보받은 κ²ƒμž…λ‹ˆλ‹€.

 

2-2. SocketException 이 μ—†λ‹€λŠ” 것이 λ‹¨μ„œ

이 λ‘œκ·Έμ—μ„œ λˆˆμ— λ„λŠ” 점이 μžˆμŠ΅λ‹ˆλ‹€. λ„€νŠΈμ›Œν¬κ°€ λŠκ²Όμ„ λ•Œ ν”νžˆ λ”°λΌμ˜€λŠ” SocketException 이 μ˜ˆμ™Έ 체인 어디에도 μ—†μŠ΅λ‹ˆλ‹€. μ†ŒμΌ“ κ³„μΈ΅μ—μ„œλŠ” 아무 μ—λŸ¬λ„ λ‚˜μ§€ μ•Šμ•˜λ‹€λŠ” λœ»μž…λ‹ˆλ‹€.

.NET λŸ°νƒ€μž„ μ†ŒμŠ€(HttpConnection.cs)μ—μ„œ 이 μ˜ˆμ™Έλ₯Ό λ˜μ§€λŠ” 곳을 보면 이유λ₯Ό μ•Œ 수 μžˆμŠ΅λ‹ˆλ‹€. 응닡을 읽으렀고 μ†ŒμΌ“μ—μ„œ read λ₯Ό ν–ˆλŠ”λ° 읽은 λ°”μ΄νŠΈ μˆ˜κ°€ 0이면 이 μ˜ˆμ™Έλ₯Ό λ˜μ§‘λ‹ˆλ‹€.

 
// dotnet/runtime (v8.0.0) HttpConnection.cs μ—μ„œ 발췌
if (bytesRead == 0)
{
    throw new HttpIOException(HttpRequestError.ResponseEnded, SR.net_http_invalid_response_premature_eof);
}

μ†ŒμΌ“μ—μ„œ 0λ°”μ΄νŠΈ μ½κΈ°λŠ” μ—λŸ¬κ°€ μ•„λ‹ˆλΌ EOF, 즉 μƒλŒ€κ°€ FIN 으둜 연결을 정상 μ’…λ£Œν–ˆλ‹€λŠ” μ‹ ν˜Έμž…λ‹ˆλ‹€. TCP μž…μž₯μ—μ„œλŠ” 아무 문제 μ—†λŠ” κΉ”λ”ν•œ μ’…λ£Œμž…λ‹ˆλ‹€. ν•˜μ§€λ§Œ HTTP μž…μž₯μ—μ„œλŠ” μš”μ²­μ„ λ³΄λƒˆλŠ”λ° 응닡이 μ™„μ„±λ˜κΈ° 전에 슀트림이 λλ‚¬μœΌλ‹ˆ λΉ„μ •μƒμž…λ‹ˆλ‹€.

TCP 연결이 λλ‚˜λŠ” 방식은 두 가지인데, λ°›λŠ” μͺ½μ—μ„œ λ³΄μ΄λŠ” λͺ¨μŠ΅μ΄ μ„œλ‘œ λ‹€λ¦…λ‹ˆλ‹€.

 

 

이번 λ‘œκ·ΈλŠ” FIN μͺ½μž…λ‹ˆλ‹€. λ°˜λŒ€λ‘œ RST 둜 끊기면 SocketException 이 μ˜ˆμ™Έ 체인에 λ‚˜νƒ€λ‚˜λŠ”λ°, 이 μ°¨μ΄λŠ” λ‹€μŒ κΈ€(The SSL connection could not be established 편)μ—μ„œ μžμ„Ένžˆ λ‹€λ£Ήλ‹ˆλ‹€.

 

 

HTTP/1.1 ν‘œμ€€(RFC 9112 8절)은 이λ₯Ό λΆˆμ™„μ „ν•œ λ©”μ‹œμ§€(incomplete message) 둜 λ‹€λ£Ήλ‹ˆλ‹€. 연결이 쑰기에 λ‹«ν˜€ λΆˆμ™„μ „ν•œ 응닡을 받은 ν΄λΌμ΄μ–ΈνŠΈλŠ” κ·Έ λ©”μ‹œμ§€λ₯Ό λΆˆμ™„μ „ν•œ κ²ƒμœΌλ‘œ 기둝해야 ν•©λ‹ˆλ‹€. .NET 은 그것을 ResponseEnded λΌλŠ” μ—λŸ¬λ‘œ ν‘œν˜„ν•©λ‹ˆλ‹€.

이 μ—λŸ¬μ˜ μ •μ²΄λŠ” “μš”μ²­μ„ λ³΄λƒˆλŠ”λ°, 응닡을 λ‹€ λ°›κΈ° 전에 μƒλŒ€κ°€ 연결을 (μ •μƒμ μœΌλ‘œ) λ‹«μ•˜λ‹€” μž…λ‹ˆλ‹€.

이 μ˜ˆμ™ΈλŠ” 응닡 본문을 μ½λŠ” 도쀑에도 λ°œμƒν•  수 μžˆμ§€λ§Œ, 이번 둜그의 μŠ€νƒ νŠΈλ ˆμ΄μŠ€λŠ” HttpConnection.SendAsync, 즉 μ‘λ‹΅μ˜ μƒνƒœ 라인과 헀더λ₯Ό λ°›λŠ” κ΅¬κ°„μž…λ‹ˆλ‹€. 응닡 헀더λ₯Ό λ‹€ 받기도 전에 연결이 λ‹«νžŒ κ²ƒμž…λ‹ˆλ‹€.

 

 

 

3️⃣ μ™œ λ‹«ν˜”μ„κΉŒ

3-1. 원인 후보 β‘  : Keep-Alive μ—°κ²°μ˜ 경쟁 μƒνƒœ

HTTP/1.1 은 기본적으둜 지속 μ—°κ²°(persistent connection) 을 μ‚¬μš©ν•©λ‹ˆλ‹€. μš”μ²­λ§ˆλ‹€ TCP μ—°κ²°κ³Ό TLS ν•Έλ“œμ…°μ΄ν¬λ₯Ό μƒˆλ‘œ ν•˜λ©΄ λΉ„μ‹ΈκΈ° λ•Œλ¬Έμ—, ν•œ 번 맺은 연결을 λ‹«μ§€ μ•Šκ³  λ‹€μŒ μš”μ²­μ— μž¬μ‚¬μš©ν•©λ‹ˆλ‹€. ν΄λΌμ΄μ–ΈνŠΈλŠ” 이런 연결듀을 컀λ„₯μ…˜ 풀에 λ³΄κ΄€ν•©λ‹ˆλ‹€.

λ¬Έμ œλŠ” μ„œλ²„λ„ ν•œκ°€ν•œ 연결을 λ¬΄ν•œμ • λ“€κ³  μžˆμ§€λŠ” μ•ŠλŠ”λ‹€λŠ” μ μž…λ‹ˆλ‹€. RFC 9112 9.5μ ˆμ€ μ„œλ²„κ°€ 보톡 λΉ„ν™œμ„± 연결을 μœ μ§€ν•˜λŠ” νƒ€μž„μ•„μ›ƒ 값을 κ°€μ§€λ©°, ν΄λΌμ΄μ–ΈνŠΈ·μ„œλ²„·ν”„λ‘μ‹œλŠ” μ–Έμ œλ“ μ§€ 전솑 연결을 닫을 수 μžˆλ‹€κ³  λ§ν•©λ‹ˆλ‹€.

그리고 λ°”λ‘œ 이 상황을 예둜 λ“­λ‹ˆλ‹€. μ„œλ²„κ°€ “유휴” 연결을 λ‹«κΈ°λ‘œ κ²°μ •ν•œ 것과 같은 μ‹œμ μ— ν΄λΌμ΄μ–ΈνŠΈκ°€ μƒˆ μš”μ²­μ„ 보내기 μ‹œμž‘ν•  수 μžˆλ‹€λŠ” κ²ƒμž…λ‹ˆλ‹€. μ„œλ²„κ°€ λ³΄κΈ°μ—λŠ” 유휴 μƒνƒœμ˜ 연결을 닫은 κ²ƒμ΄μ§€λ§Œ, ν΄λΌμ΄μ–ΈνŠΈκ°€ λ³΄κΈ°μ—λŠ” μš”μ²­μ΄ μ§„ν–‰ 쀑인 연결이 λ‹«νžŒ 것이 λ©λ‹ˆλ‹€.

 

[κ·Έλ¦Ό 3] 유휴 연결을 사이에 두고 μƒˆ μš”μ²­κ³Ό FIN 이 μ—‡κ°ˆλ¦¬λŠ” 경우

 

.NET λŸ°νƒ€μž„ μ†ŒμŠ€μ—λ„ ν•΄λ‹Ή μœ„μΉ˜μ— “μ„œλ²„κ°€ 자기 μͺ½μ—μ„œ 연결을 μ’…λ£Œν–ˆκ³ , 유휴 νƒ€μž„μ•„μ›ƒ λ•Œλ¬ΈμΌ κ°€λŠ₯성이 λ†’λ‹€”λŠ” 주석이 달렀 μžˆμŠ΅λ‹ˆλ‹€(λ°”λ‘œ μ•„λž˜ 3-2의 μ½”λ“œ). λŸ°νƒ€μž„ κ°œλ°œμžλ“€λ„ 이 μƒν™©μ˜ λŒ€ν‘œμ μΈ μ›μΈμœΌλ‘œ 유휴 μ—°κ²° μ’…λ£Œλ₯Ό κΌ½κ³  μžˆλ‹€λŠ” λœ»μž…λ‹ˆλ‹€.

 

 

λ¬Όλ‘  .NET 도 손을 놓고 μžˆμ§€λŠ” μ•ŠμŠ΅λ‹ˆλ‹€. ν’€μ—μ„œ 연결을 κΊΌλ‚Ό λ•Œ κ·Έ 연결이 이미 λ‹«ν˜”λŠ”μ§€(EOF λ₯Ό λ°›μ•˜λŠ”μ§€) ν™•μΈν•˜κ³ , λ‹«νžŒ 연결은 μ“°μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. ν•˜μ§€λ§Œ ν™•μΈν•œ 직후뢀터 μš”μ²­μ΄ μ„œλ²„μ— λ„μ°©ν•˜κΈ° μ „ 사이에 μ„œλ²„κ°€ λ‹«μ•„λ²„λ¦¬λŠ” κ²½μš°λŠ” ν΄λΌμ΄μ–ΈνŠΈκ°€ μ›μ²œμ μœΌλ‘œ 막을 수 μ—†μŠ΅λ‹ˆλ‹€.

 

3-2. 그럼 μžλ™μœΌλ‘œ μž¬μ‹œλ„ν•΄ μ£Όλ©΄ λ˜μ§€ μ•Šλ‚˜?

μ‹€μ œλ‘œ ν•΄μ€λ‹ˆλ‹€. 단, 쑰건이 μžˆμŠ΅λ‹ˆλ‹€. 이번 둜그의 μŠ€νƒμ— 찍힌 SendAsync μ•ˆμ— 이런 λΆ„κΈ°κ°€ μžˆμŠ΅λ‹ˆλ‹€.

 
// dotnet/runtime (v8.0.0) HttpConnection.cs (SendAsync) μ—μ„œ 발췌
if (_readBuffer.ActiveLength == 0)
{
    // The server shutdown the connection on their end, likely because of an idle timeout.
    // If we haven't started sending the request body yet (or there is no request body),
    // then we allow the request to be retried.
    if (request.Content is null || allowExpect100ToContinue is not null)
    {
        _canRetry = true;
    }

    throw new HttpIOException(HttpRequestError.ResponseEnded, SR.net_http_invalid_response_premature_eof);
}

응닡을 ν•œ λ°”μ΄νŠΈλ„ λ°›μ§€ λͺ»ν•œ 채 연결이 λ‹«ν˜”κ³ , μš”μ²­ 본문이 μ—†κ±°λ‚˜ 아직 본문을 보내기 전이라면 μž¬μ‹œλ„ κ°€λŠ₯으둜 ν‘œμ‹œν•©λ‹ˆλ‹€. 그러면 컀λ„₯μ…˜ 풀이 μ˜ˆμ™Έλ₯Ό μ‚Όν‚€κ³  μƒˆ μ—°κ²°λ‘œ μš”μ²­μ„ λ‹€μ‹œ λ³΄λƒ…λ‹ˆλ‹€. ν˜ΈμΆœν•œ μͺ½μ€ 그런 일이 μžˆμ—ˆλŠ”μ§€λ„ λͺ¨λ¦…λ‹ˆλ‹€.

λ°˜λŒ€λ‘œ 본문이 μžˆλŠ” μš”μ²­μ€ 이미 본문을 λ³΄λƒˆμ„ 수 μžˆμœΌλ―€λ‘œ μž¬μ‹œλ„ν•˜μ§€ μ•Šκ³  μ˜ˆμ™Έλ₯Ό κ·ΈλŒ€λ‘œ μ˜¬λ¦½λ‹ˆλ‹€. μ΄λ²ˆμ— μ‹€νŒ¨ν•œ μš”μ²­μ΄ μ •ν™•νžˆ 본문이 μžˆλŠ” POST μ˜€μŠ΅λ‹ˆλ‹€. GET μš”μ²­μ—μ„œλŠ” 쑰용히 λ„˜μ–΄κ°€λ˜ 상황이, POST μ—μ„œλŠ” μ—λŸ¬λ‘œ λ“œλŸ¬λ‚œ κ²ƒμž…λ‹ˆλ‹€.

 

 

이 λ™μž‘μ€ HTTP ν‘œμ€€κ³Όλ„ λ§žλ‹Ώμ•„ μžˆμŠ΅λ‹ˆλ‹€. RFC 9110 9.2.2μ ˆμ€ λ‹€μŒκ³Ό 같이 μ •λ¦¬ν•©λ‹ˆλ‹€.

  • λ©±λ“±(idempotent) λ©”μ„œλ“œ(GET, HEAD, PUT, DELETE λ“±)λŠ” 응닡을 읽기 전에 톡신 μž₯μ• κ°€ λ‚˜λ©΄ μžλ™μœΌλ‘œ λ‹€μ‹œ 보낼 수 μžˆλ‹€.
  • λ©±λ“±ν•˜μ§€ μ•Šμ€ λ©”μ„œλ“œ(POST λ“±)λŠ” μš”μ²­μ˜ μ˜λ―Έκ°€ μ‹€μ œλ‘œλŠ” λ©±λ“±ν•˜λ‹€λŠ” 것을 μ•Œκ±°λ‚˜, μ›λž˜ μš”μ²­μ΄ μ μš©λ˜μ§€ μ•Šμ•˜μŒμ„ 확인할 μˆ˜λ‹¨μ΄ μ—†λŠ” ν•œ μžλ™ μž¬μ‹œλ„ν•˜μ§€ μ•Šμ•„μ•Ό ν•œλ‹€(SHOULD NOT).

μ΄μœ λŠ” λ‹¨μˆœν•©λ‹ˆλ‹€. ν΄λΌμ΄μ–ΈνŠΈλŠ” μ„œλ²„κ°€ μš”μ²­μ„ μ²˜λ¦¬ν–ˆλŠ”μ§€ μ•Œ 수 μ—†κΈ° λ•Œλ¬Έμž…λ‹ˆλ‹€. μš”μ²­μ΄ λ„μ°©ν•˜κΈ° 전에 λ‹«ν˜”μ„ μˆ˜λ„ 있고, 처리λ₯Ό 끝내고 응닡을 보내렀닀 μ£½μ—ˆμ„ μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€. “결제” 같은 μš”μ²­μ„ λ¬΄ν„±λŒ€κ³  λ‹€μ‹œ 보내면 두 번 처리될 수 μžˆμŠ΅λ‹ˆλ‹€.

 

3-3. 원인 후보 β‘‘ : μ„œλ²„κ°€ μš”μ²­μ„ 보고 닫은 경우

유휴 μ—°κ²° 경쟁이 μœ μΌν•œ 원인은 μ•„λ‹™λ‹ˆλ‹€. 응닡을 보내기 전에 연결을 λ‹«λŠ” 상황이면 무엇이든 같은 μ—λŸ¬κ°€ λ©λ‹ˆλ‹€.

  • μ„œλ²„ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ΄ μš”μ²­μ„ μ²˜λ¦¬ν•˜λ‹€ 비정상 μ’…λ£Œλ˜κ±°λ‚˜ μž¬μ‹œμž‘λœ 경우
  • μ„œλ²„ μ•žλ‹¨μ˜ ν”„λ‘μ‹œ·λ‘œλ“œλ°ΈλŸ°μ„œκ°€ λ°±μ—”λ“œ 문제둜 응닡 없이 연결을 닫은 경우
  • μ„œλ²„λ‚˜ λ³΄μ•ˆ μž₯λΉ„κ°€ μš”μ²­ λ‚΄μš©μ„ 보고 응닡 없이 연결을 닫아버린 경우

ν΄λΌμ΄μ–ΈνŠΈ μ˜ˆμ™Έλ§ŒμœΌλ‘œλŠ” 이 연결이 ν’€μ—μ„œ μž¬μ‚¬μš©λœ κ²ƒμ΄μ—ˆλŠ”μ§€ μ•Œ 수 μ—†μœΌλ―€λ‘œ, λ°œμƒ νŒ¨ν„΄μœΌλ‘œ κ΅¬λΆ„ν•©λ‹ˆλ‹€.

  • 간헐적이고, ν•œλ™μ•ˆ μš”μ²­μ΄ μ—†λ‹€κ°€ 보낸 μš”μ²­μ—μ„œ 주둜 λ‚œλ‹€ → 유휴 μ—°κ²° 경쟁
  • νŠΉμ • μš”μ²­μ—μ„œ 맀번 λ‚œλ‹€ → μ„œλ²„κ°€ κ·Έ μš”μ²­μ„ μ²˜λ¦¬ν•˜μ§€ λͺ»ν•˜κ±°λ‚˜ κ±°λΆ€ν•˜λŠ” 것
  • νŠΉμ • μ‹œκ°„λŒ€μ— λͺ°λ €μ„œ λ‚œλ‹€ → μ„œλ²„ 배포·μž¬μ‹œμž‘·μž₯μ• 

 

ν•΄λ‹Ή μ—λŸ¬λŠ” λ‘œκ·Έμ—μ„œ 집계해 λ΄€μ„λ•Œ 맀우 κ°„ν—μ μœΌλ‘œ 일어났기 λ•Œλ¬Έμ— 일단은 "유휴 μ—°κ²° 경쟁"둜 κ²°λ‘  μ§€μ—ˆμŠ΅λ‹ˆλ‹€.

 

 

4️⃣ λŒ€μ‘ 방법

β‘  ν΄λΌμ΄μ–ΈνŠΈμ˜ 유휴 νƒ€μž„μ•„μ›ƒμ„ μ„œλ²„λ³΄λ‹€ 짧게

경쟁 μƒνƒœλŠ” “μ„œλ²„κ°€ λ¨Όμ € λ‹«λŠ”” κ΅¬μ‘°μ—μ„œ μƒκΉλ‹ˆλ‹€. ν΄λΌμ΄μ–ΈνŠΈκ°€ μ„œλ²„λ³΄λ‹€ λ¨Όμ € 유휴 연결을 버리면 창이 크게 μ€„μ–΄λ“­λ‹ˆλ‹€. .NET μ—μ„œλŠ” SocketsHttpHandler 의 두 속성이 κ΄€λ ¨λ©λ‹ˆλ‹€.

속성 의미 κΈ°λ³Έκ°’
PooledConnectionIdleTimeout 연결이 ν’€μ—μ„œ 유휴 μƒνƒœλ‘œ μžˆμ–΄λ„ μž¬μ‚¬μš© κ°€λŠ₯ν•˜λ‹€κ³  λ³΄λŠ” μ΅œλŒ€ μ‹œκ°„ .NET 6 이상 1λΆ„
(.NET Core, .NET 5 λŠ” 2λΆ„)
PooledConnectionLifetime 연결이 수립된 μ‹œμ λΆ€ν„° ν’€μ—μ„œ μž¬μ‚¬μš© κ°€λŠ₯ν•˜λ‹€κ³  λ³΄λŠ” μ΅œλŒ€ μ‹œκ°„ λ¬΄μ œν•œ
 
var handler = new SocketsHttpHandler
{
    // μƒλŒ€ μ„œλ²„μ˜ keep-alive νƒ€μž„μ•„μ›ƒλ³΄λ‹€ 짧게
    PooledConnectionIdleTimeout = TimeSpan.FromSeconds(30),
    // 연결을 주기적으둜 μƒˆλ‘œ λ§Ίμ–΄ DNS λ“± λ„€νŠΈμ›Œν¬ 변화도 반영
    PooledConnectionLifetime = TimeSpan.FromMinutes(2),
};
var client = new HttpClient(handler);

μœ„ μˆ«μžλŠ” μ˜ˆμ‹œμΌ 뿐이고, μƒλŒ€ μ„œλ²„μ˜ 유휴 νƒ€μž„μ•„μ›ƒμ— 맞좰 μ •ν•΄μ•Ό ν•©λ‹ˆλ‹€. 참고둜 PooledConnectionLifetime 은 Microsoft 의 HttpClient κ°€μ΄λ“œλΌμΈμ—μ„œ DNS 변경을 λ°˜μ˜ν•˜κΈ° μœ„ν•΄ 섀정을 ꢌμž₯ν•˜λŠ” 값이기도 ν•©λ‹ˆλ‹€. HttpClient λŠ” 연결을 λ§Œλ“€ λ•Œλ§Œ DNS λ₯Ό μ‘°νšŒν•˜κΈ° λ•Œλ¬Έμž…λ‹ˆλ‹€.

 

 

β‘‘ μž¬μ‹œλ„λŠ” “λ‹€μ‹œ 보내도 λ˜λŠ” μš”μ²­μΈκ°€”λ₯Ό νŒλ‹¨ν•œ 뒀에

μ„€μ •μœΌλ‘œ 창을 쀄여도 0 이 λ˜μ§€λŠ” μ•ŠμŠ΅λ‹ˆλ‹€. RFC 9112(9.3.1절)도 κ΅¬ν˜„μ²΄λŠ” 예고 μ—†λŠ” μ—°κ²° μ’…λ£Œλ‘œλΆ€ν„° 볡ꡬ할 ν•„μš”λ₯Ό μ˜ˆμƒν•΄μ•Ό ν•œλ‹€κ³  λ§ν•©λ‹ˆλ‹€. κ²°κ΅­ μž¬μ‹œλ„κ°€ ν•„μš”ν•©λ‹ˆλ‹€.

 

 

λŸ°νƒ€μž„μ΄ POST λ₯Ό μžλ™ μž¬μ‹œλ„ν•˜μ§€ μ•ŠλŠ” μ΄μœ λŠ” κ·Έ μš”μ²­μ΄ μ•ˆμ „ν•œμ§€ λŸ°νƒ€μž„μ€ λͺ¨λ₯΄κΈ° λ•Œλ¬Έμž…λ‹ˆλ‹€. ν•˜μ§€λ§Œ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ€ μ••λ‹ˆλ‹€. RFC 9110 도 μš”μ²­μ΄ ν•΄λ‹Ή λ¦¬μ†ŒμŠ€μ— λŒ€ν•΄ μ•ˆμ „ν•˜λ‹€λŠ” 것을 (μ„€κ³„λ‚˜ 섀정을 톡해) μ•Œκ³  μžˆλ‹€λ©΄ POST λ₯Ό μžλ™μœΌλ‘œ λ°˜λ³΅ν•  수 μžˆλ‹€κ³  적고 μžˆμŠ΅λ‹ˆλ‹€.

λ©”μ„œλ“œλŠ” POST μ§€λ§Œ μ‹€μ œλ‘œλŠ” 쑰회만 ν•˜λŠ” μš”μ²­μ΄λΌλ©΄ μž¬μ‹œλ„ν•΄λ„ λ©λ‹ˆλ‹€. λ°˜λŒ€λ‘œ μƒνƒœλ₯Ό λ°”κΎΈλŠ” μš”μ²­μ΄λΌλ©΄ λ©±λ“±ν‚€ 같은 μž₯치 μ—†μ΄λŠ” μž¬μ‹œλ„ν•˜λ©΄ μ•ˆ λ©λ‹ˆλ‹€.

 
try
{
    return await client.SendAsync(request, cancellationToken);
}
catch (HttpRequestException e) when (IsResponseEnded(e) && isSafeToRetry)
{
    // μƒˆ μ—°κ²°λ‘œ λ‹€μ‹œ μ‹œλ„ (μš”μ²­ κ°μ²΄λŠ” μƒˆλ‘œ λ§Œλ“€μ–΄μ•Ό 함)
}

static bool IsResponseEnded(HttpRequestException e) =>
    e.HttpRequestError == HttpRequestError.ResponseEnded ||
    e.InnerException is HttpIOException { HttpRequestError: HttpRequestError.ResponseEnded };

μ—λŸ¬ λ©”μ‹œμ§€ λ¬Έμžμ—΄μ„ λΉ„κ΅ν•˜λŠ” λŒ€μ‹  HttpRequestError κ°’μœΌλ‘œ λΆ„κΈ°ν•  수 μžˆλ‹€λŠ” 점이 .NET 8 μ΄ν›„μ˜ μž₯μ μž…λ‹ˆλ‹€. μž¬μ‹œλ„ 섀계 μžμ²΄λŠ” 이전에 μ“΄ 글듀을 μ°Έκ³ ν•΄ μ£Όμ„Έμš”.

 

 

 

5️⃣ κ²°κ³Ό 및 배운 점

1. μƒνƒœ μ½”λ“œκ°€ μ—†λŠ” μ—λŸ¬λŠ” “단계”둜 λΆ„λ₯˜ν•œλ‹€

  • DNS, TCP, TLS, μš”μ²­, 응닡 쀑 μ–΄λ””μ—μ„œ λŠκ²ΌλŠ”μ§€κ°€ κ³§ 원인 ν›„λ³΄μ˜ λ²”μœ„μž…λ‹ˆλ‹€.

2. μ˜ˆμ™Έ 체인에 무엇이 “μ—†λŠ”μ§€”도 λ‹¨μ„œλ‹€

  • SocketException 이 μ—†λ‹€λŠ” 것은 μ†ŒμΌ“ μ—λŸ¬ 없이 0λ°”μ΄νŠΈ 읽기(EOF)둜 λλ‚¬λ‹€λŠ” 뜻이고, μƒλŒ€κ°€ FIN 으둜 λ‹«μ•˜λ‹€λŠ” λœ»μž…λ‹ˆλ‹€.

3. 연결은 μ–Έμ œλ“  끊길 수 있고, 그것은 ν‘œμ€€μ΄ ν—ˆμš©ν•œ λ™μž‘μ΄λ‹€

  • Keep-Alive μ—°κ²°μ˜ 경쟁 μƒνƒœλŠ” 버그가 μ•„λ‹ˆλΌ ꡬ쑰적으둜 μ‘΄μž¬ν•˜λŠ” ν‹ˆμž…λ‹ˆλ‹€. ν΄λΌμ΄μ–ΈνŠΈλŠ” 이λ₯Ό μ „μ œλ‘œ 섀계해야 ν•©λ‹ˆλ‹€.

4. μž¬μ‹œλ„μ˜ 기쀀은 “μ‹€νŒ¨ν–ˆλŠ”κ°€”κ°€ μ•„λ‹ˆλΌ “λ‹€μ‹œ 보내도 λ˜λŠ”κ°€”λ‹€

  • λŸ°νƒ€μž„μ΄ GET 은 쑰용히 μž¬μ‹œλ„ν•˜κ³  본문이 μžˆλŠ” POST λŠ” κ·ΈλŒ€λ‘œ μ˜ˆμ™Έλ₯Ό μ˜¬λ¦¬λŠ” μ΄μœ κ°€ 여기에 μžˆμŠ΅λ‹ˆλ‹€.

5. μ—°κ²° μ„€μ •λ§Œ 보지 말고 μ‹€μ œλ‘œ 보낸 μš”μ²­λ„ λ³Έλ‹€

  • μš”μ²­μ„ 받은 뒀에 λ‹«ν˜”λ‹€λ©΄, μƒλŒ€κ°€ κ·Έ μš”μ²­μ˜ 무엇을 보고 λ‹«μ•˜λŠ”μ§€λ„ 원인 ν›„λ³΄μž…λ‹ˆλ‹€.

 

이 글이 λΉ„μŠ·ν•œ μ—λŸ¬λ₯Ό 처음 λ§Œλ‚œ λΆ„λ“€μ—κ²Œ 도움이 λ˜μ—ˆμœΌλ©΄ ν•©λ‹ˆλ‹€.

 

 

 

참고 자료

λŒ“κΈ€