지원 티켓에 답변한 수년 후 같은 몇 가지 문제가 "내 WebSocket 연결이 이상하게 작동해요" 보고서의 대다수를 차지해요. 라이브러리의 버그는 없어요 — 모두 존재한다는 것을 알면 5분 만에 수정되는 구성 실수예요. 이 게시물은 증상, 원인 및 각각을 해결하는 한 줄짜리와 함께 가장 일반적인 열 가지를 나열해요.
이 목록에서 하나의 실수만 읽는다면 #2로 만드세요. OnMessage 이벤트를 차단하는 것은 다른 모든 카테고리를 합친 것보다 더 많은 "라이브러리가 느려요" 티켓에 책임이 있으며, 수백 개의 동시 클라이언트를 넘어 확장될 때까지 보이지 않아요. 늦은 밤 프로덕션 디버깅 세션을 피하고 출시하기 전에 수정하세요.
실수 1: WatchDog 재연결 비활성화
증상: 클라이언트가 시작 시 연결되고, 몇 시간 동안 잘 실행되다가 Wi-Fi 신호 단절, VPN 재연결 또는 서버 재시작 후 메시지 수신이 자동으로 중지돼요.
원인: WatchDog 속성이 기본적으로 꺼져 있어요. 라이브러리는 자동 재연결을 원한다고 가정하지 않아요 — 일부 앱은 빠른 실패 동작을 원해요. 대부분은 그렇지 않아요.
oClient.WatchDog.Enabled := True; oClient.WatchDog.Interval := 10; // try every 10 seconds oClient.WatchDog.Attempts := 0; // 0 = unlimited
설정하고 잊어버리세요. 재연결이 얼마나 자주 발사되는지 현장에서 볼 수 있도록 OnDisconnect 로깅과 결합하세요. 사용자가 네트워크 간에 로밍하는 모바일 또는 노트북 배포의 경우 다음 간격 틱을 기다리는 대신 즉각적인 재연결을 트리거하기 위해 OS 네트워크 변경 알림(.NET의 SystemEvents.NetworkAvailabilityChanged, Windows-Delphi의 WMI 경로)을 연결하세요.
실수 2: OnMessage 이벤트 차단
증상: 부하 하에서 처리량이 무너져요. 서버가 "막힌" 느낌이에요. CPU는 낮지만 메시지가 쌓여요.
원인: OnMessage는 I/O 워커 스레드에서 실행돼요. 내부에서 느린 데이터베이스, 외부 HTTP API 또는 장기 실행 파서를 호출하면 해당 워커를 공유하는 다른 모든 연결을 굶주리게 해요.
// BAD: blocks the worker
procedure TForm1.ServerMessage(Connection: TsgcWSConnection; const Text: string);
begin
oDb.Query('INSERT INTO events VALUES(?)', [Text]); // 50 ms
oHttp.Get('https://billing/track?event=' + Text); // 200 ms
end;
// GOOD: hand off to a worker queue
procedure TForm1.ServerMessage(Connection: TsgcWSConnection; const Text: string);
begin
oWorkQueue.Push(TWorkItem.Create(Connection.Guid, Text));
end;
실수 3: HeartBeat 무시
증상: 유령 연결이 쌓여요. 클라이언트가 죽었다는 것을 알고 있어도 Connections.Count가 계속 증가해요.
원인: 애플리케이션 계층 하트비트가 없으면 OS TCP 킵얼라이브 타이머는 ~2시간 후에 발사돼요. 그때쯤이면 메모리를 먹는 수만 개의 좀비 소켓이 있어요.
oServer.HeartBeat.Enabled := True; oServer.HeartBeat.Interval := 30; oServer.HeartBeat.Timeout := 90;
실수 4: 불일치하는 TLS 버전
증상: 클라이언트가 "handshake failed" 또는 "SSL_ERROR_SYSCALL"을 받아요. 연결이 개발 박스에서는 작동하지만 기업 프록시 뒤의 프로덕션에서는 실패해요.
원인: 클라이언트 SSL 옵션은 하위 호환성을 위해 TLS 1.0–1.2로 기본 설정돼요; 최신 서버는 TLS 1.2/1.3만 요구해요. 또는 OpenSSL DLL이 오래되었어요.
oClient.TLSOptions.Version := tlsTLSv1_2; // or tlsTLSv1_3 oClient.TLSOptions.OpenSSL_Options.LibPath := ExtractFilePath(ParamStr(0)); // Bundle the latest libcrypto-3.dll / libssl-3.dll with your installer.
실수 5: 단편화된 메시지 처리 안 함
증상: 서버가 큰 JSON 문서의 첫 부분을 받고, 파서가 실패하고, 연결이 닫혀요.
원인: 기본적으로 sgcWebSockets는 단편을 재조립하고 완전한 페이로드로만 OnMessage를 발사해요. 그러나 메모리 이유로 ReadOptions.FragmentMode := frgPartial을 설정했다면 재조립해야 해요.
// Default (recommended for most apps)
oServer.ReadOptions.FragmentMode := frgComplete;
// If you opted into partial delivery, reassemble manually
procedure TForm1.OnFragment(Connection: TsgcWSConnection;
const aPartial: TBytes; aIsFinal: Boolean);
begin
Buffers[Connection.Guid].Append(aPartial);
if aIsFinal then
Process(Buffers[Connection.Guid].ToBytes);
end;
실수 6: GUI 스레드에서 동기 API 사용
증상: 연결 시 또는 느린 링크에서 WriteData를 호출할 때 UI가 몇 초 동안 멈춰요.
원인: 메인 스레드의 차단 호출. VCL/FMX 앱에서는 항상 비동기 패턴을 사용하거나 워커 스레드에서 호출하세요.
// BAD: blocks the UI
oClient.Active := True;
// GOOD: connect asynchronously
TTask.Run(procedure
begin
oClient.Active := True;
end);
// Or use the event-driven API
oClient.OnConnect := DoConnected;
oClient.Connect; // returns immediately
실수 7: 서브 프로토콜 선택 잊기
증상: 연결은 성공하지만 피어가 모든 프레임을 거부하거나 예상한 것과 다른 형식으로 응답해요.
원인: 많은 WebSocket 서버(MQTT-over-WS, STOMP, GraphQL-WS, Phoenix)는 핸드셰이크에 특정 서브 프로토콜을 요구해요. 없으면 서버가 다른 프로토콜로 기본 설정되거나 떨어뜨려요.
// MQTT over WebSocket
oClient.Specifications.Hixie76 := False;
oClient.Specifications.RFC6455 := True;
oClient.HeadersRequest.Add('Sec-WebSocket-Protocol: mqttv3.1');
// GraphQL over WS
oClient.HeadersRequest.Add('Sec-WebSocket-Protocol: graphql-ws');
실수 8: 버퍼 크기가 너무 작음 (또는 너무 큼)
증상: 많은 작은 메시지를 푸시하는 서버의 높은 CPU, 또는 큰 메시지를 푸시하는 서버의 메모리 부족.
원인: 기본 송수신 버퍼는 일반적인 사용에 맞게 크기가 조정되어 있어요. 트래픽 모양에 맞게 튜닝하세요.
// Small messages (chat, telemetry): smaller buffers reduce per-conn memory oServer.IOHandler.RecvBufferSize := 4096; oServer.IOHandler.SendBufferSize := 4096; // Big messages (file transfer, media): larger buffers reduce syscalls oServer.IOHandler.RecvBufferSize := 65536; oServer.IOHandler.SendBufferSize := 65536;
실수 9: 공용 서버에서 Origin 검사 없음
증상: 보안 감사가 "모든 웹사이트가 사용자의 브라우저를 통해 WebSocket에 연결할 수 있음"을 표시해요.
원인: WebSocket 프로토콜은 동일 출처를 적용하지 않아요. 서버가 Origin 헤더를 검증해야 해요.
procedure TForm1.ServerBeforeConnect(Connection: TsgcWSConnection;
var Continue: Boolean);
var
vOrigin: string;
begin
vOrigin := Connection.Headers.Values['Origin'];
Continue := (vOrigin = 'https://app.example.com')
or (vOrigin = 'https://admin.example.com');
end;
실수 10: OnMessage에 민감한 데이터 로깅
증상: 감사관이 로그 파일에서 API 키, JWT 또는 PII를 발견해요. 규정 준수 문제.
원인: OnMessage의 쉬운 LogMemo.Lines.Add(Text)는 모든 페이로드를 영원히 디스크에 쓰는 거예요.
procedure TForm1.ServerMessage(Connection: TsgcWSConnection; const Text: string);
begin
// Hash / redact before logging
LogMemo.Lines.Add(Format('[%s] %d bytes (sha=%s)',
[Connection.Guid, Length(Text), ShortHash(Text)]));
Process(Text);
end;
보너스: History.txt 안 읽기
모든 릴리스는 2013년 이후의 모든 변경 사항, 수정 사항 및 중단 노트를 나열하는 history.txt와 함께 제공돼요. 각 업그레이드 후 그것을 훑어보는 데 5분을 보내면 나중에 "왜 작동이 멈췄나" 디버깅 시간을 절약할 수 있어요.
보너스 2: 프로젝트 간 컴포넌트 버전 혼합
Delphi 개발자는 때때로 더 새로운 sgcWebSockets 릴리스의 단일 .pas를 이전 프로젝트에 복사해요. "이 파일만요". 이것은 작동할 때까지 작동해요 — 파일이 두 릴리스 전에 변경된 타입에 의존하고 링커가 신비롭게 실패하거나 더 나쁘게는 링크되지만 런타임에 충돌해요. 항상 전체 라이브러리를 함께 업그레이드하세요. 한 파일을 복사하여 절약한 30초는 다운스트림에서 무언가가 깨질 때 네 시간의 디버깅 가치가 없어요.
보너스 3: WebSocket을 fire-and-forget으로 취급
WebSocket은 메시지 큐가 아니에요. TCP를 통한 양방향 바이트 스트림이에요. 네트워크가 메시지 중간에 끊어지면 프레임이 손실되고 자동으로 다시 전달되지 않아요. 비즈니스 중요 메시지의 경우 자체 승인 프로토콜을 추가해야 해요 — 일반적으로 메시지당 UUID, 수신자로부터의 명시적 ACK, 타임아웃 후 발신자의 재전송. 이 계층을 건너뛰는 것은 "사용자가 입력 중" 알림에는 괜찮지만 "사용자가 카트에 대해 지불함"에는 치명적이에요.
보너스 4: 메모가 메모리를 누출하도록 두기
증상: 디버그 폼의 진단 메모가 몇 시간의 트래픽 후 클라이언트를 OOM시키는 첫 번째 것이에요. sgcWebSockets를 비난해요; sgcWebSockets는 무죄예요.
원인: TMemo는 추가된 모든 라인을 유지해요. 초당 100라인이면 시간당 360,000라인이에요. 각 라인은 문자열을 할당해요. VCL은 모든 WM_PAINT에서 수천 개의 보이지 않는 라인을 렌더링해요. 라이브러리가 잘못된 것이 없는데 프로세스가 멈춰요.
// Cap the diagnostic memo
procedure TForm1.LogLine(const aText: string);
const
cMaxLines = 500;
begin
TThread.Queue(nil, procedure
begin
while Memo1.Lines.Count > cMaxLines do
Memo1.Lines.Delete(0);
Memo1.Lines.Add(aText);
end);
end;
더 나은 방법: LoggerPro를 통해 롤링 파일에 로깅하고 시각적 디버깅을 위해 마지막 200줄만 메모에 미러링하세요. 프로덕션 코드는 절대 네트워크 스레드에서 UI 컨트롤에 쓰면 안 돼요.
보너스 5: Application.Terminate 전에 서버 닫지 않기
증상: 앱 종료 시 프로세스가 30초 동안 멈추거나 연결이 우아하지 않게 분해되었기 때문에 OS가 클라이언트 로그에 처리되지 않은 예외를 보고해요.
원인: 서버 소멸자는 각 연결에 닫기 프레임을 보내고 OS가 청취 포트를 해제하기를 기다려요. oServer.Active := False 전에 Application.Terminate를 호출하면 연결이 핸드셰이크 중간에 죽고 OS 포트는 TIME_WAIT에 남아 빠른 재시작을 차단해요.
procedure TForm1.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
begin
if oServer.Active then
begin
oServer.Broadcast('{"event":"shutdown","reconnect_after":5}');
Sleep(200); // let frames flush
oServer.Active := False; // graceful close
end;
end;
콘솔 서버의 경우 Windows에서 SetConsoleCtrlHandler를 후크하거나 Linux에서 SIGTERM을 후크하고 동일한 종료 시퀀스를 실행하세요. 이것을 서비스 매니저의 HUP/재시작 루프와 페어링하면 연결 손실 없는 배포가 가능해요.
패턴 뒤의 패턴
이러한 실수의 대부분은 공통된 뿌리가 있어요: 네트워크가 신뢰할 수 있다고 가정. 그렇지 않아요. 반 열린 TCP 연결이 발생해요. 모바일 네트워크가 떨어져요. 기업 프록시가 TLS를 깨요. Wi-Fi가 로밍해요. 서버가 재시작해요. 클라우드 로드 밸런서가 60초 후 유휴 연결을 종료해요. 이러한 조건에서 살아남지 못하는 WebSocket 앱은 완성되지 않은 것이에요 — 해피 패스 데모예요. 좋은 소식: 라이브러리는 이러한 시나리오 각각에 대한 컨트롤을 노출해요. 나쁜 소식: 대부분은 하위 호환성을 위해 기본적으로 꺼져 있어요. 문서의 WatchDog, HeartBeat, Reconnect 및 TLS 옵션 페이지를 읽는 데 두 시간을 보내는 것이 가장 저렴한 보험이에요.
2차 패턴: I/O 스레드를 존중하세요. 1밀리초 이상 걸리는 모든 것 — 데이터베이스 쿼리, 파일 I/O, 외부 HTTP 호출, 긴 문자열의 정규식, 100 KB 페이로드의 JSON 파싱 — 은 OnMessage가 아닌 워커 스레드에 속해요. 이 규칙을 절대적으로 만드세요. 주니어 개발자는 3개월 후에 이를 위반할 거예요; "OnMessage에서 차단 없음"을 포함하는 코드 리뷰 체크리스트가 출시되기 전에 잡아요.
다음 단계
고트래픽 서버를 튜닝하고 있다면 다음으로 sgcWebSockets 성능 튜닝을 읽으세요. 라이브러리가 처음이에요? 시작하기 허브에서 시작하여 5분 안에 첫 WebSocket을 연결하세요.