모든 거래소는 요청 제한을 공표하고, 모든 거래소는 그것을 강제합니다. Binance는 제한에 걸린 REST 호출에 HTTP 429와 함께 반드시 준수해야 하는 Retry-After 헤더를 응답하고, 초당 5개를 넘는 메시지를 보내는 WebSocket 연결은 끊어 버립니다. 이 두 가지 측면에 대한 고객 보고가 며칠 사이에 각각 하나씩 도착했고, 그 둘을 합쳐 제대로 고칠 만한 문제 세 가지가 드러났습니다.
세 가지 모두 이제 수정되었습니다. 그중 하나는 Binance보다 훨씬 넓은 범위에 영향을 미치고 있었습니다.
실패한 요청은 헤더를 버리고 있었습니다
미리 준비된 API 클라이언트 중 하나를 통해 보낸 요청이 HTTP 오류 상태로 실패하면, 컴포넌트는 상태 코드와 상태 텍스트, 본문을 담은 예외를 발생시켰습니다. 응답 헤더는 사라졌습니다. 숨겨진 것이 아니라 사라진 것입니다. 예외가 아직 스택을 타고 올라가는 동안 HTTP 객체가 해제되었고, 헤더도 함께 없어졌습니다.
그래서 Binance 규격에 맞는 백오프를 이 컴포넌트 위에 구현하는 것이 불가능했습니다. Retry-After는 정확히 얼마나 기다려야 하는지 알려 주며, 이를 무시하면 429에서 HTTP 418과 일시적인 IP 차단으로 상황이 악화됩니다. X-MBX-USED-WEIGHT-1M 같이 Binance가 반환하는 요청 제한 카운터도 마찬가지로 접근할 수 없었기 때문에, 애플리케이션이 한계에 부딪히기 전에 스스로 속도를 조절할 수도 없었습니다.
이제 발생하는 예외는 EsgcHTTPAPIProtocolException이며, 응답 헤더를 함께 담고 있습니다. 이전에 발생하던 예외를 상속하므로 기존 핸들러는 그대로 예외를 잡아내고, 새 정보를 사용하고 싶지 않다면 바꿀 것이 아무것도 없습니다.
try
vResponse := oBinance.GetAggregateTrades('BTCUSDT');
except
on E: EsgcHTTPAPIProtocolException do
begin
if E.ErrorCode = 429 then
Sleep(E.RetryAfterMs);
vWeight := E.GetHeader('X-MBX-USED-WEIGHT-1M');
// E.ResponseHeaders holds the complete list
end;
end;
RetryAfterMs는 지연 시간을 밀리초 단위로 반환하며, 이 헤더가 가질 수 있는 두 가지 형식인 단순한 초 단위 숫자와 HTTP 날짜를 모두 인식합니다. 헤더가 없으면 -1을 반환합니다. 이 수정은 Get뿐 아니라 여덟 개의 요청 메서드 전부에 적용되며, Post와 Query가 이미 받고 있던 ResponseHeaders 매개변수는 요청이 실패할 때, 다시 말해 정확히 그 정보가 필요한 순간에 채워집니다.
함께 알아 두면 좋은 점은, 2026.7.0부터 HTTP 클라이언트가 대기까지 대신 처리할 수 있다는 것입니다.
oBinance.RetryOptions.Enabled := True;
oBinance.RetryOptions.MaxRetries := 3;
// HonorRetryAfter is already True by default
열두 프레임 대신 한 프레임
Binance WebSocket 프로토콜은 하나의 SUBSCRIBE 프레임에 스트림 목록을 담는 것을 허용합니다. 컴포넌트는 이를 사용하지 않고 있었습니다. 모든 Subscribe* 헬퍼가 정확히 하나의 스트림을 담은 프레임을 즉시 하나씩 보냈기 때문에, 두 개의 채널에 걸친 여섯 심볼이라는 평범한 관심 목록만으로도 몇 밀리초 사이에 열두 개의 프레임이 전송되었습니다. Binance는 문서에 나온 그대로 연결을 끊었습니다.
이제 목록 전체를 한 프레임으로 보내는 배치 메서드가 있습니다.
oBinance.SubscribeStreams(['btcusdt@aggTrade', 'btcusdt@depth',
'ethusdt@aggTrade', 'ethusdt@depth']);
스트림이 수백 개라도 프레임 하나는 8 KB 요청 제한보다 훨씬 아래에 머무르므로, 실무에서 관심 목록은 언제나 한 프레임입니다. UnsubscribeStreams는 그 반대 동작입니다.
아무도 우회할 수 없었던 부분
가장 중요한 문제이면서, 두 보고 모두 완전히 짚어내지는 못한 부분입니다.
모든 거래소 API는 재연결 후 복원할 수 있도록 구독한 채널을 보관합니다. 그 재전송은 스트림당 하나씩 프레임을 연달아, 속도 조절 없이, 겨우 몇 밀리초 된 연결 위로 쏟아냈습니다. 현실적인 크기의 관심 목록이라면 새로 맺은 연결이 곧바로 끊겼습니다. 그러면 WatchDog이 다시 연결하고, 같은 폭주를 재전송하고, 또 끊겼습니다. 일시적인 연결 끊김 한 번이 영원한 재연결 루프로 바뀌었습니다.
이 폭주는 컴포넌트 내부에서 생성되기 때문에 애플리케이션이 피할 방법이 없었습니다. 헬퍼 대신 직접 프레임을 작성해도 소용없었습니다. 재전송은 여러분이 호출하는 것이 아니기 때문입니다.
라이브러리의 나머지 부분을 점검한 결과 같은 재전송 루프가 17개 거래소 API에 복사되어 있었습니다. Binance, Bitfinex, Bitget, Bitmex, Bitstamp, Bybit, Cex, CexPlus, Coinbase, CryptoCom, Deribit, GateIO, Huobi, Kraken, Kucoin, MEXC, OKX입니다. 각각 자기 거래소의 제한에 걸리고 있었습니다.
이제 WebSocket API 클라이언트에 Throttle 옵션이 있습니다.
oBinance.Throttle.Enabled := True; // paces your own Subscribe calls
oBinance.Throttle.MaxMessages := 4; // default, leaves room for PING/PONG
oBinance.Throttle.IntervalMs := 1000; // default
기본값은 의도적으로 비대칭이므로 설명이 필요합니다. Throttle.Enabled는 False이므로, 여러분이 요청하지 않는 한 애플리케이션이 하는 호출의 타이밍은 달라지지 않습니다. 하지만 Throttle.PaceResubscribe는 True이므로, 재연결 재전송은 별도 설정 없이도 속도가 조절됩니다.
이유는 단순합니다. 여러분은 자신의 구독 루프를 직접 제어하고 배치로 묶을 수 있으므로, 그 지점의 속도 조절은 여러분의 결정입니다. 재전송은 제어할 수 없으므로 대신 조절해 드립니다. 재연결 루프 문제는 코드 변경 없이 기존의 모든 애플리케이션에서 해결됩니다. Binance에서는 재전송도 결합된 프레임으로 보내므로, 열두 개 스트림의 관심 목록이 이제 열두 프레임이 아니라 한 프레임으로 재전송됩니다. 이전 동작을 원한다면 PaceResubscribe를 False로 설정하세요.
여러 거래소를 사용한다면 알아 둘 점이 하나 있습니다. 초당 4개 메시지라는 기본 예산은 문서상 제한이 5인 Binance에 맞춘 값입니다. OKX는 3으로 문서화되어 있으므로 OKX 클라이언트의 기본값은 3입니다. MaxMessages를 올리기 전에 사용하는 거래소의 제한을 확인하세요.
업그레이드
그대로 교체하면 됩니다. 새 예외는 이전에 발생하던 예외를 상속하고, 배치 메서드는 추가된 기능이며, Throttle은 재연결을 망가뜨리던 재전송을 제외하면 기본적으로 꺼져 있습니다. 최신 버전은 sgcWebSockets 다운로드 페이지에서 받으세요.
이 두 가지 모두 시간을 들여 소스를 읽고, 문제를 재현하고, 정확하게 정리해 준 고객들에게서 나왔습니다. 세 번째이자 가장 심각한 버그를 찾아낸 것도 그 덕분입니다. 컴포넌트의 동작 중 설명할 수 없는 것이 있다면 알려 주세요.
질문이나 의견, 마이그레이션 도움이 필요하신가요? 문의하기, 코드를 작성한 사람들에게서 직접 답변을 받으실 수 있습니다.
