sgcWebSockets 사용 시 일반적인 10가지 실수 (및 수정 방법)

· 컴포넌트

지원 티켓에 답변한 수년 후 같은 몇 가지 문제가 "내 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을 연결하세요.