- Node.js/TypeScript 기반 백엔드에서 대규모 실시간 업데이트를 처리해야 하는 상황이었음
- PostgreSQL을 백엔드로 사용하여 수백 개의 워커 노드가 새로운 작업을 지속적으로 확인하고, 에이전트가 실행 및 채팅 상태 업데이트를 받아야 함
- 웹소켓에 대한 탐구로 시작했지만, 놀랍도록 효과적인 '구식' 솔루션으로 귀결
→ "Postgres를 사용한 HTTP Long Polling"
문제 상황: 대규모 실시간 업데이트
-
워커 노드 업데이트 :
- Node.js/Golang/C# SDK를 실행하는 수백 개의 워커 노드가 있음
- 새로운 작업이 제공되는 즉시 이를 알아야 했기 때문에 Postgres 데이터베이스를 다운시키지 않는 쿼리 전략이 필요
-
에이전트 상태 동기화 :
- 에이전트는 실행 및 채팅 상태에 대한 실시간 업데이트가 필요했고, 이를 효율적으로 스트리밍해야 함
롱 폴링과 WebSocket 비교
- 숏 폴링은 시간표에 따라 엄격하게 출발하는 기차와 같아서 승객이 있는지 여부에 관계없이 정해진 간격으로 출발함
- 롱 폴링은 서버가 응답을 기다리다 데이터가 생기면 바로 반환하고, 일정 시간이 지나면 타임아웃으로 응답을 돌려줌
- 즉, “기다리다가 데이터가 생기면 출발”하는 기차와 같음. 특정 시간(TTL)내에 승객이 나타나지 않을때만 비어 있는 상태로 출발
- 데이터(승객)가 있을 때는 즉시 출발하고 없을 때는 리소스를 효율적으로 사용할 수 있는 두 가지 장점을 모두 제공
- WebSocket은 연결을 상시 유지해 양방향으로 데이터를 주고받는 방식임
- 조직 환경, 인프라, 파이어월 문제 등으로 WebSocket 구성보다 롱 폴링이 더 단순하고 호환성 높음
롱 폴링 구현 세부 내용
-
getJobStatusSync 함수가 중요한 역할을 담당함
-
jobId, owner, ttl 등의 파라미터를 받아 특정 작업 상태를 특정 시간 동안 반복 조회함
- 다음 조건 중 하나가 충족될 때까지 반복 조회를 수행함
- 작업 상태가 success 또는 failure가 됨
-
ttl(타임아웃) 경과
- 500ms 간격으로 데이터베이스를 조회하고, 결과가 확정되지 않았으면 기다렸다가 다시 조회함
- 타임아웃 초과 시 에러를 던지고, 성공 시 결과를 반환함
데이터베이스 최적화
- Postgres에 적절한 인덱스를 두어 조회 비용을 최소화함
- 예: CREATE INDEX idx_jobs_status ON jobs(id, cluster_id);
롱 폴링의 이점
-
모니터링 유지 용이성 : 기존 HTTP 기반 로깅, 모니터링 스택을 그대로 활용 가능함
-
인증 단순성 : 새 인증 방식을 구현할 필요 없이 기존 HTTP 인증을 그대로 사용 가능함
-
인프라 호환성 : 파이어월이나 로드 밸런서에 별도 설정이 필요 없고, 일반 HTTP 트래픽으로 취급됨
-
운영 단순성 : 서버 재시작 시에도 연결 상태를 별도로 처리할 필요가 없고, 디버깅이 용이함
-
클라이언트 구현 간편성 : 표준 HTTP 요청-응답 구조에 재시도 로직만 추가하면 동작 가능함
ElectricSQL과의 비교
- ElectricSQL은 Postgres 데이터를 프론트엔드와 동기화하는 솔루션임
- WebSocket 대신 HTTP를 쓰면서도 실시간성을 보장해주는 구조를 갖추고 있음
- 실제로 실시간 업데이트를 처리하기 위해 극단적인 제어나 낮은 수준의 구조가 필요하지 않은 경우 ElectricSQL을 권장
우리가 Raw Long Polling을 선택한 이유
- 메시지 전달 메커니즘은 단순한 구현 세부사항이 아니라 제품의 핵심 요소
- 핵심 기능을 타사 라이브러리에 의존할 수 없음 (아무리 우수한 라이브러리라도)
- 요구사항
-
핵심 제품 제어 : 메시지 전달 메커니즘을 완전히 제어해야 함. 인프라 수준이 아니라 제품 자체임
-
외부 의존성 제거 : 셀프 호스팅을 단순화하기 위해 외부 의존성을 최소화
-
저수준 제어 : 폴링 메커니즘 및 연결 관리를 직접 제어
-
최대 제어 가능성 : 동적 폴링 간격 구현 등 세부사항을 세밀하게 조정할 수 있어야 함
-
코드 단순성 : 사용자들이 코드베이스를 쉽게 이해하고 수정할 수 있도록 간단하게 설계
- 결론적으로 간단한 HTTP Long Polling 구현을 선택함으로써 직접 제어와 단순성을 확보
롱 폴링 구현 시 주의사항
-
TTL 설정 : 서버 쪽에서 반드시 최대 TTL을 강제하고, 클라이언트가 요청한 TTL이 이를 넘지 않도록 처리함
-
인프라 타임아웃 고려 : 로드 밸런서, 엣지 서버, 프록시 등의 타임아웃 설정보다 충분히 짧은 TTL이어야 함
-
DB 폴링 간격 : 500ms 정도로 딜레이를 주어 DB 부하를 줄임
-
백오프 전략(옵션) : 점진적으로 폴링 간격을 늘리는 방식으로 시스템 자원을 더 효율적으로 사용 가능함
WebSocket을 고려해야 할 상황
- WebSocket 자체가 잘못된 것은 아니며, 다른 측면에서는 유용함
- 상태가 많은 연결을 모니터링하고, 복잡한 이벤트를 상시 주고받아야 하는 경우
- 인증, 인프라, 관측 문제를 해결할 리소스와 시간이 충분한 경우
- 운영 및 로깅, 재연결 처리, 인증 메커니즘 등을 직접 구축해야 하는 복잡성이 존재함
WebSockets: 또 다른 선택지에 대한 이야기
- Long Polling이 우리의 요구에 적합했지만, WebSockets도 충분히 고려할 가치가 있음
- WebSockets 자체가 나쁜 것은 아니며, 많은 주의와 관리가 필요할 뿐
- WebSockets의 주요 과제와 해결 방향
-
가시성 : WebSockets는 상태 기반이므로, 지속적인 연결에 대한 로깅과 모니터링 추가 필요
-
인증 : WebSocket 연결을 위한 새로운 인증 메커니즘 구현 필요
-
인프라 : WebSocket을 지원하기 위해 로드 밸런서, 방화벽 등의 인프라를 적절히 구성해야 함
-
운영 관리 : WebSocket 연결 및 재연결 관리. 연결 타임아웃 및 오류 처리
-
클라이언트 구현 : 클라이언트 측 WebSocket 라이브러리 구현. 재연결 및 상태 관리 기능 포함