Delphi의 async / await: 이벤트 핸들러 없는 sgcWebSockets | eSeGeCe 블로그

Delphi의 async / await: 이벤트 핸들러 없는 sgcWebSockets

· 컴포넌트

지금까지 sgcWebSockets의 모든 비동기 호출은 이벤트 핸들러를 의미했습니다. 백그라운드 스레드에서 Get을 호출하거나, Active := True로 설정하고 OnConnect를 기다렸습니다. 결과는 다른 곳, 다른 메서드에서 도착했으므로, 작업을 시작한 코드와 응답을 소비하는 코드가 서로 다른 자리에 놓였습니다. 여기에 첫 번째 요청에 의존하는 두 번째 요청, 오류 처리, 그리고 다시 숨겨야 하는 "잠시 기다려 주세요" 패널까지 더하면, 폼 전체에 흩어진 작은 상태 머신이 만들어집니다. 무엇을 하던 중이었는지 기억하기 위한 private 필드 몇 개, 마지막 호출이 아직 진행 중인지 알기 위한 플래그, 그리고 세 요청 중 어느 것이 방금 돌아왔는지 알아내야 하는 핸들러 말입니다.

sgcWebSockets 2026.7은 그 대부분을 없애는 await 스타일 API를 추가합니다. 이제 HTTP 요청, WebSocket 연결, AI 채팅 호출은 기다릴 수 있고, 콜백을 연결할 수 있으며, 취소할 수 있는 객체를 반환합니다. 그래서 코드가 실행되는 순서대로 위에서 아래로 읽힙니다. 취소도 겉치레가 아닙니다. 결과가 마침내 도착했을 때 그냥 무시하는 것이 아니라, 아래에서 돌아가는 작업 자체를 중단시킵니다.

태스크와 퓨처

새 유닛은 sgcBase_AsyncAwait입니다. 여기에는 두 개의 인터페이스가 있습니다. IsgcTask는 완료되지만 값을 만들어내지 않는 작업을 나타내고, IsgcFuture<T>T 타입의 값을 만들어내는 작업을 나타냅니다. 둘 다 같은 네 가지를 제공합니다. 작업이 끝날 때까지 블로킹하는 Await, 성공했을 때 콜백을 받는 ThenProc, 예외가 발생했을 때 콜백을 받는 OnError, 그리고 작업을 멈추는 Cancel입니다.

TsgcAsync.Run으로 직접 만들 수도 있습니다. 전달한 프로시저나 함수는 백그라운드 스레드에서 실행되고, 콜백은 메인 스레드로 돌아오므로 ThenProcOnError 안에서 UI를 다루어도 안전합니다.

uses
  sgcBase_AsyncAwait;

begin
  // the anonymous method runs on a background thread
  TsgcAsync.Run<Integer>(
    function: Integer
    begin
      Result := ExpensiveCalculation;
    end)
  .ThenProc(
    procedure(const Value: Integer)
    begin
      // main thread, safe to update the UI
      Label1.Caption := IntToStr(Value);
    end)
  .OnError(
    procedure(E: Exception)
    begin
      // main thread as well
      Label1.Caption := 'Failed: ' + E.Message;
    end);
end;

태스크는 실행되는 동안 스스로를 살려두므로, 위와 같은 실행 후 방치(fire-and-forget) 호출도 문제없습니다. 나중에 상태를 확인하고 싶다면 인터페이스를 변수에 담아두고 IsCompleted, IsCancelled 또는 State를 읽으면 됩니다. 이는 tsaPending, tsaRunning, tsaCompleted, tsaFailed, tsaCancelled 중 하나입니다. 또한 타임아웃 후 완료되고 발동 전에 취소할 수 있는 태스크를 반환하는 TsgcAsync.Delay(aMilliseconds)와, 백그라운드 태스크 안에서 코드 조각을 메인 스레드로 되돌려 보내는 TsgcAsync.RunOnMainThread도 있습니다.

HTTP 요청 기다리기

HTTP 클라이언트는 퓨처를 직접 노출하므로, 호출을 직접 감쌀 필요가 없습니다. GetFuturePostFuture는 응답 본문으로 해석되는 IsgcFuture<string>을 반환합니다.

uses
  sgcHTTP_Client, sgcBase_AsyncAwait;

var
  oHTTP: TsgcHTTPClient;
  oFuture: IsgcFuture<string>;
begin
  oHTTP := TsgcHTTPClient.Create(nil);

  // the request runs on a background thread, this line returns immediately
  oFuture := oHTTP.GetFuture('https://httpbin.org/get');

  oFuture.ThenProc(
    procedure(const Value: string)
    begin
      // main thread: Value is the response body
      Memo1.Lines.Text := Value;
    end)
  .OnError(
    procedure(E: Exception)
    begin
      ShowMessage('GET failed: ' + E.Message);
    end);
end;

이미 워커 스레드 위에 있거나 콘솔 애플리케이션을 작성 중이라면, 그냥 블로킹하고 값을 읽으면 됩니다.

var
  vBody: string;
begin
  vBody := oHTTP.GetFuture('https://httpbin.org/get').Await;
  WriteLn(vBody);
end;

Await는 백그라운드 호출에서 발생한 예외를 원래 클래스 이름과 메시지를 담은 EsgcAsyncException으로 감싸서 다시 발생시키므로, 평범한 try ... except로 감싸는 방식이 그대로 동작합니다. 퓨처가 취소된 경우에는 Await가 대신 EsgcTaskCancelled를 발생시킵니다.

이 글에서 다른 무엇보다 중요한 경고가 하나 있습니다. 메인 스레드에서 Await를 호출하면 메인 스레드가 블로킹됩니다. VCL이나 FMX 애플리케이션에서는 호출이 반환될 때까지 창이 멈춘다는 뜻입니다. 폼에서는 ThenProc를 사용하십시오. Await는 콘솔 애플리케이션, 서비스, 그리고 이미 백그라운드 스레드에서 실행 중인 코드를 위해 남겨두십시오. HTTP 클라이언트 자체는 스레드 안전하지 않으므로, 하나를 여러 태스크에서 공유하지 말고 동시 요청마다 별도의 인스턴스를 사용하십시오.

WebSocket 클라이언트 연결하기

연결한다는 것은 곧 Active := True를 설정하고, 성공했다는 OnConnect나 실패했다는 OnError를 기다리는 것을 의미했습니다. ConnectTask는 같은 일을 태스크로 제공합니다.

uses
  sgcWebSocket, sgcBase_AsyncAwait;

var
  oTask: IsgcTask;
begin
  sgcWebSocketClient1.Host := 'echo.esegece.com';
  sgcWebSocketClient1.Port := 443;
  sgcWebSocketClient1.TLS  := True;

  oTask := sgcWebSocketClient1.ConnectTask(10000); // 10 second timeout

  oTask.ThenProc(
    procedure
    begin
      // main thread: the connection is established and ready to send
      sgcWebSocketClient1.WriteData('hello');
    end)
  .OnError(
    procedure(E: Exception)
    begin
      ShowMessage('Could not connect: ' + E.Message);
    end);
end;

연결 시도는 백그라운드 스레드에서 실행되며, 타임아웃은 진짜 타임아웃입니다. 시간이 지나면 태스크는 멈춰 있는 대신 실패합니다. 태스크를 취소하면 소켓 연결이 끊어지므로, 아직 네트워크를 기다리고 있는 연결 시도가 풀립니다.

기다리지 않고 연결 대기: Connect와 LastError

모든 것에 태스크가 필요한 것은 아닙니다. 때로는 연결하고 성공 여부를 알려주는 한 줄의 코드만 있으면 되는데, 사실 대부분의 사람들이 애초에 Active := True에 기대했던 동작이 바로 그것입니다. 2026.7은 연결이 시작된 것이 아니라 실제로 수립되었을 때만 반환하는 블로킹 Connect를 추가합니다.

var
  vError: string;
begin
  // returns True only when the WebSocket connection is fully established
  if sgcWebSocketClient1.Connect(10000) then
    sgcWebSocketClient1.WriteData('hello')
  else
    // find out why, without wiring an OnError handler
    ShowMessage('Connect failed: ' + sgcWebSocketClient1.LastError);

  // or get the reason back directly from the call
  if not sgcWebSocketClient1.Connect(vError, 10000) then
    ShowMessage('Connect failed: ' + vError);

  // and a Disconnect that waits until the socket is really closed
  sgcWebSocketClient1.Disconnect(5000);
end;

이는 빠른 재연결 시 자주 문제가 되던 경쟁 조건을 해소합니다. Connect는 연결이 완전히 올라온 뒤에만 True를 반환하므로 더 이상 반쯤 준비된 연결 위에서 무언가가 실행되지 않고, Disconnect는 소켓이 실제로 닫힐 때까지 기다린 후 반환하므로 재연결 루프가 이전 연결과 겹칠 수 없습니다. LastError는 컴포넌트에 존재하며 마지막 시도가 실패한 이유를 담고 있으므로, 어떤 이벤트도 구독하지 않고 연결이 실패했는지 알 수 있습니다. 이 값은 각 연결 시도가 시작될 때 자동으로 지워지며, ClearLastError로 직접 초기화할 수도 있습니다.

ConnectAwait와 똑같이 호출한 스레드를 블로킹한다는 점에 유의하십시오. 워커 스레드나 콘솔 애플리케이션에서 사용하고, 폼에서는 ConnectTask를 사용하십시오.

AI 채팅 기다리기

AI 클라이언트도 같은 패턴을 따릅니다. ChatAsync는 백그라운드 스레드에서 프롬프트를 보내고 모델 응답으로 해석되는 IsgcFuture<string>을 반환하므로, 폼에서의 채팅 호출은 이제 이벤트 핸들러 없이 세 줄이면 됩니다.

uses
  sgcAI_Chat, sgcBase_AsyncAwait;

var
  oFuture: IsgcFuture<string>;
begin
  oFuture := sgcAIChat1.ChatAsync('Summarize this text in one sentence.');

  oFuture.ThenProc(
    procedure(const Value: string)
    begin
      // main thread: Value is the model answer
      MemoAnswer.Lines.Text := Value;
      ButtonSend.Enabled := True;
    end)
  .OnError(
    procedure(E: Exception)
    begin
      MemoAnswer.Lines.Text := 'Error: ' + E.Message;
      ButtonSend.Enabled := True;
    end);

  ButtonSend.Enabled := False; // the UI stays responsive while it runs
end;

그 퓨처를 취소하면 진행 중인 요청이 중단되므로, 대화 상자를 닫거나 중지를 누른 사용자가 결코 읽지 않을 토큰에 비용을 지불하며 백그라운드에서 LLM 호출을 계속 돌리는 일이 없습니다.

블로킹 대신 체이닝 (ThenProc / OnError)

어떤 스레드가 무엇을 실행하는지 명확히 짚고 넘어갈 필요가 있습니다. 콜백 코드가 보통 잘못되는 지점이 바로 여기이기 때문입니다. TsgcAsync.Run에 전달하는 작업과 GetFuture, ConnectTask, ChatAsync 뒤에 있는 요청은 모두 백그라운드 스레드에서 실행됩니다. ThenProcOnError 콜백은 메인 스레드로 큐에 넣어 되돌려지므로, 그 안에서 폼을 다루는 것은 안전합니다.

둘 중 하나만 발동합니다. ThenProc는 작업이 성공했을 때, OnError는 예외가 발생했을 때 실행되며, 태스크가 취소된 경우에는 둘 다 실행되지 않습니다. 순차적인 경우에는 이것으로 충분합니다. 이전 ThenProc 안에서 다음 호출을 시작하면 체인이 실행되는 순서대로 읽힙니다. 이미 백그라운드 태스크 안에 있으면서 중간에 UI를 다뤄야 한다면 TsgcAsync.RunOnMainThread를 사용하십시오.

TsgcAsync.Run(
  procedure
  var
    oHTTP: TsgcHTTPClient;
    vBody: string;
  begin
    // background thread
    oHTTP := TsgcHTTPClient.Create(nil);
    try
      TsgcAsync.RunOnMainThread(
        procedure
        begin
          StatusBar1.SimpleText := 'Downloading...';
        end);

      vBody := oHTTP.Get('https://httpbin.org/get');

      TsgcAsync.RunOnMainThread(
        procedure
        begin
          // main thread again
          Memo1.Lines.Text := vBody;
          StatusBar1.SimpleText := 'Done';
        end);
    finally
      oHTTP.Free;
    end;
  end);

진짜로 취소하는 취소

대부분의 "취소" 구현은 플래그를 세워두고 결과가 결국 도착하면 무시합니다. 소켓은 열린 채로 남고, 요청은 계속 실행되며, 스레드는 계속 바쁩니다. sgcWebSockets에서는 각 비동기 호출이 태스크에 취소 동작을 등록하므로, Cancel이 아래에 있는 작업까지 도달합니다. HTTP 퓨처를 취소하면 요청이 중단되고, ConnectTask를 취소하면 소켓 연결이 끊어지며, ChatAsync를 취소하면 AI 호출이 중단됩니다. 블로킹된 워커 스레드는 타임아웃이 만료될 때까지 앉아 있는 대신 곧바로 깨어납니다.

덕분에 Delay 태스크와 취소를 조합해 타임아웃을 손쉽게 만들 수 있습니다. 이는 라이브러리에 예제 18번으로 함께 제공되는 패턴입니다.

var
  oRequest: IsgcFuture<string>;
  oTimeout: IsgcTask;
begin
  oRequest := oHTTP.GetFuture('https://httpbin.org/delay/10');
  oTimeout := TsgcAsync.Delay(3000);

  // if the request answers first, drop the timeout
  oRequest.ThenProc(
    procedure(const Value: string)
    begin
      oTimeout.Cancel;
      Memo1.Lines.Text := Value;
    end);

  // if the timeout fires first, abort the request for real
  oTimeout.ThenProc(
    procedure
    begin
      if not oRequest.IsCompleted then
      begin
        oRequest.Cancel;
        ShowMessage('Request timed out');
      end;
    end);
end;

취소된 태스크는 ThenProcOnError도 발동하지 않습니다. StatetsaCancelled가 되고, IsCancelledTrue를 반환하며, Await에서 블로킹된 쪽은 EsgcTaskCancelled를 받습니다. TsgcAsync.Run으로 직접 태스크를 만들고 그것이 중단 가능한 무언가를 감싸고 있다면, SetCancelProc로 자신만의 중단 동작을 등록하면 Cancel이 그것을 호출합니다.

Delphi 버전 지원

async/await 유닛은 제네릭과 익명 메서드를 사용하므로 Delphi 2010 이상이 필요합니다. Delphi 7, 2007, 2009에서는 이 유닛이 빈 유닛으로 컴파일되고 async 메서드는 존재하지 않으므로, GetFuture, ConnectTask, ChatAsync를 사용할 수 없습니다. 라이브러리의 나머지 모든 기능은 이 메서드들과 나란히 존재하는 이벤트 기반 API를 포함해 예전과 똑같이 Delphi 7을 계속 지원합니다. 제거된 것은 없고 기존 코드의 동작이 바뀌는 일도 없으며, 새 호출은 추가된 것입니다.

위에서 설명한 블로킹 Connect, Disconnect, LastError는 제네릭이 아니므로 지원되는 모든 Delphi 버전에서 사용할 수 있습니다.

이 모든 것에 대한 실제 예제는 sgcBase_AsyncAwait_Examples.pas에 소스와 함께 제공되며, 태스크, 퓨처, 지연, 취소, 상태 폴링, 병렬 HTTP 요청, 그리고 위의 타임아웃 워치독을 다룹니다.

제공 범위

태스크와 퓨처는 sgcWebSockets 2026.7에서 Delphi 2010부터 Delphi 13까지, 그리고 C++Builder에서 Win32/Win64, Linux64, macOS, Android, iOS 전반에 걸쳐 사용할 수 있습니다. 블로킹 Connect, Disconnect, LastError는 Delphi 7부터 13까지 사용할 수 있습니다.

구독이 유효한 고객은 고객 영역에서 새 빌드를 내려받을 수 있습니다. 평가판 사용자는 esegece.com/products/websockets/download에서 최신 설치 파일을 받을 수 있습니다.

질문이나 피드백이 있으신가요? 아니면 콜백 기반 폼을 태스크로 옮기는 데 도움이 필요하신가요? 문의하기, 코드를 작성한 사람들에게서 직접 답변을 받으실 수 있습니다.