websockets

6 개의 포스트

gitlab

Codex와 GitLab으로 버그 수정 (새 탭에서 열림)

Codex는 터미널에서 코드를 분석하고 수정·테스트하는 데 강력하지만, 실제 배포에는 이슈 관리, 머지 리퀘스트, CI/CD, 코드 리뷰와 승인 과정이 필요하다. 글은 GitLab과 Codex를 연계해 Rust WebSocket 버그를 수정하고, GitLab MCP로 이슈와 개발 맥락을 반영하며, GitLab Duo Agent Platform의 외부 에이전트로 리뷰 피드백까지 처리하는 흐름을 소개한다. 핵심 결론은 코딩 에이전트의 빠른 구현 능력과 GitLab의 소프트웨어 생명주기 관리 기능을 결합해야 코드 작성부터 운영 배포까지 연결할 수 있다는 것이다. ## Codex와 GitLab을 결합하는 전체 워크플로 - Codex는 저장소 안에서 코드를 읽고, 수정안을 만들고, 명령을 실행하고, 테스트까지 수행한다. - 그러나 코드 작성만으로는 소프트웨어가 배포되지 않는다. - GitLab 이슈 - 머지 리퀘스트 - CI/CD 파이프라인 - 보안 스캔 - 코드 리뷰 - 최종 사람의 승인 등이 필요하다. - 글에서는 Tanuki IoT Platform 프로젝트의 Rust metrics backend를 대상으로 세 가지 활용 사례를 제시한다. - 로컬 Codex로 Rust WebSocket 버그 수정 - GitLab MCP로 이슈 요구사항과 개발 맥락을 Codex에 제공 - GitLab Duo Agent Platform에서 Codex를 외부 에이전트로 사용해 MR 리뷰 피드백 처리 ## 실습 환경과 프로젝트 구조 - 필요한 환경: - 터미널에서 실행 가능한 Codex - 이슈가 포함된 GitLab 프로젝트 - 선택적으로 GitLab MCP 서버와 GitLab Duo Agent Platform - Rust 컴파일러와 Cargo - 프로젝트를 GitLab에 가져온 뒤 로컬에 clone하고 저장소 루트에서 `codex`를 실행한다. - 주요 대상은 `backend/` 아래의 Rust metrics store다. - 센서는 REST API로 측정값을 전송한다. - 대시보드는 WebSocket 스트림으로 실시간 데이터를 받는다. - `AGENTS.md`를 통해 Codex에 Rust 도구 체인, 빌드 명령, 테스트 방법, 코드 품질 기준을 알려줄 수 있다. ## WebSocket 메트릭 필터 버그 재현 - 백엔드는 REST API에서는 메트릭 필터링을 지원하지만, WebSocket 스트림에서는 필터가 제대로 적용되지 않는 문제가 있었다. - 서버 실행: ```bash PORT=9090 cargo run --manifest-path backend/rust-metrics-store/Cargo.toml ``` - 특정 센서와 메트릭을 구독: ```bash websocat 'ws://localhost:9090/ws?sensor=arduino-iot-collector&metric=temperature_celsius' ``` - 같은 센서에 서로 다른 메트릭을 전송한다. ```bash curl -s -X POST http://localhost:9090/api/metrics \ -H 'Content-Type: application/json' \ -d '{"sensor":"arduino-iot-collector","metric":"temperature_celsius","value":23.5}' curl -s -X POST http://localhost:9090/api/metrics \ -H 'Content-Type: application/json' \ -d '{"sensor":"arduino-iot-collector","metric":"humidity_percent","value":61.2}' ``` - 기대 결과는 `temperature_celsius`만 수신하는 것이다. - 실제로는 `humidity_percent`도 스트림에 나타나므로, `/ws`가 `metric` 쿼리 파라미터를 무시하고 있음을 확인할 수 있다. ## 로컬 Codex를 이용한 버그 수정 - Codex에 다음과 같이 작업을 요청한다. ```text I need help with a backend change to add metric filtering to /ws so live streams can be narrowed to one metric. ``` - Codex는 저장소와 `AGENTS.md`를 분석해 다음 작업을 수행한다. - `/ws` 핸들러의 기존 sensor 필터 로직 조사 - 선택적 `metric` 쿼리 파라미터 지원 추가 - 센서와 메트릭 조합에 따른 필터링 구현 - 관련 테스트 추가 - `README.md`와 `AGENTS.md` 등 문서 갱신 - 변경 후 포맷팅, 테스트, 빌드를 실행하고 최종 diff를 검토한다. - 이후 Codex에 브랜치 생성, 커밋, 원격 저장소 push를 맡길 수 있다. ## GitLab 머지 리퀘스트와 CI/CD 검증 - 코드가 MR에 올라가면 GitLab이 이후 생명주기를 담당한다. - 파이프라인에서 다음 검증이 수행된다. - 빌드와 테스트 - 보안 스캔 - Rust 코드 스타일 및 품질 검사 - GitLab Duo Code Review - 배포 후에는 동일한 로컬 테스트를 다시 실행해 sensor와 metric을 모두 지정했을 때 요청한 메트릭만 전달되는지 확인한다. - 검증 결과와 로컬 테스트 내용을 MR에 댓글로 남겨 리뷰 맥락을 공유한다. ## GitLab MCP로 이슈와 요구사항 연결 - 로컬 저장소만 보는 Codex는 GitLab에 있는 다음 정보를 알 수 없다. - 버그 이슈의 상세 내용 - 합의된 기능·비기능 요구사항 - 구현 메모 - 관련 MR 상태 - 파이프라인 상태 - GitLab MCP 서버를 연결하면 Codex가 이슈를 직접 조회할 수 있다. - 이슈에는 다음과 같은 내용이 포함될 수 있다. - 문제 재현 방법 - 기능 요구사항 - 비기능 요구사항 - 필요한 테스트 - `README.md`, `AGENTS.md` 갱신 요구 - 구현 방향에 대한 메모 - 따라서 사용자가 긴 요구사항을 프롬프트에 복사하지 않아도, Codex가 GitLab 이슈를 단일 기준 정보로 활용해 구현할 수 있다. - 이는 단순히 코드를 고치는 것보다 프로젝트의 합의된 요구사항과 개발 프로세스에 맞춘 변경을 가능하게 한다. ## 실용적인 적용 권장 사항 - Codex에는 작업 범위와 기대 동작을 명확히 요청하고, `AGENTS.md`에 빌드·테스트·스타일 규칙을 기록하는 것이 좋다. - 코드 수정 전에는 실제 API와 WebSocket 동작을 명령줄에서 재현해 버그를 객관적으로 확인한다. - GitLab MCP를 사용해 이슈를 직접 참조하게 하면 요구사항 누락을 줄일 수 있다. - Codex가 작성한 코드는 GitLab MR, CI/CD, 보안 스캔, 사람의 리뷰를 거친 뒤 배포해야 한다.

cloudflare

Browser Run: 이제 Cloudflare Containers에서 실행되어 더 빠르고 확장성이 뛰어납니다 (새 탭에서 열림)

Browser Run은 Cloudflare Containers 기반으로 재구축되면서 더 높은 처리량, 낮은 지연 시간, 향상된 안정성을 확보했다. 분당 브라우저 생성 한도는 60개, 동시 실행 수는 120개로 늘어 기존보다 4배 향상됐고, Quick Action 응답 시간은 50% 이상 단축됐다. 이 성능 개선의 핵심은 지역별 사전 준비 컨테이너 풀, D1의 트랜잭션 기반 상태 관리, Queues를 활용한 배치 쓰기다. ## Browser Run의 역할 - Cloudflare의 글로벌 네트워크에서 실행되는 헤드리스 브라우저를 프로그래밍 방식으로 제어한다. - 주요 활용 사례: - 웹 애플리케이션의 엔드투엔드 테스트 - 의심스러운 URL의 안전한 조사 - PDF 렌더링 - 스크린샷 캡처와 콘텐츠 추출 - AI 에이전트의 웹 브라우징 - 목표는 자동화 브라우저를 안전하고 대규모로 활용할 수 있는 플랫폼이 되는 것이다. ## 기존 Browser Isolation 인프라의 한계 - 이전에는 Browser Run과 Browser Isolation(BISO)이 인프라를 공유했다. - BISO의 큰 컨테이너 이미지는 브라우저 시작 시간과 개발 속도를 저하시켰다. - BISO 브라우저의 글로벌 분산이 충분하지 않아 지연 시간과 복원력에도 문제가 있었다. - BISO의 장시간·지속적 세션과 Browser Run의 짧고 급격한 트래픽 패턴이 서로 맞지 않아 확장 병목이 발생했다. ## 점진적인 Containers 마이그레이션 - 요청 경로에 Worker를 추가해 일부 사용자에게만 Container 기반 브라우저를 제공하며 마이그레이션을 시작했다. - 기존 BISO 브라우저와 병행 운영하면서 성능을 비교하고 구현 오류를 검증했다. - 적용 순서는 다음과 같았다. - Quick Actions 엔드포인트 - 무료 계정의 Workers 브라우저 바인딩 - 종량제 계정 - 나머지 계약 고객 - 고객이 별도 설정을 변경하거나 Worker를 재배포하지 않아도 전환되도록 했다. ## 지역별 사전 준비 컨테이너 풀 - Durable Object(DO)는 요청에 가까운 위치에 생성될 수 있지만, 연결되는 Container는 지구 반대편에 배치될 수 있다. - 단일 메시지에서는 문제가 작지만, 스크린샷 요청처럼 WebSocket으로 수십 개 메시지를 주고받는 작업에서는 왕복 지연이 누적된다. - 이를 해결하기 위해: - 지역별로 DO 기반 브라우저 컨테이너를 미리 실행해 둔다. - 요청이 들어오면 해당 지역에서 사용자와 가장 가까운 DO-Container 쌍을 선택한다. - 사용자-DO, DO-Container 양쪽의 네트워크 거리를 줄인다. - 브라우저별 글로벌 상태를 관찰하고 수요에 따라 용량을 재배치해야 하므로 추가적인 아키텍처 복잡성이 생겼다. ## Workers KV의 일관성 문제 - 초기에는 각 컨테이너 상태를 Workers KV에 저장했다. - KV는 최종적 일관성을 사용하며, 캐시 TTL 때문에 최대 약 30초 또는 그 이상 오래된 상태를 읽을 수 있었다. - “사용 가능”으로 읽은 컨테이너가 실제 라우팅 시점에는 이미 다른 요청에 할당되는 경쟁 조건이 발생했다. - 이로 인해: - 동일 브라우저의 중복 할당 - 과도한 브라우저 예약 - 급격한 수요 증가에 대한 확장 지연 문제가 생겼다. ## D1을 이용한 원자적 브라우저 할당 - 컨테이너 상태를 KV에서 D1 데이터베이스로 이전했다. - D1은 SQLite 기반 트랜잭션을 제공하므로 브라우저 할당을 원자적으로 처리할 수 있다. - 브라우저는 사용자 간 공유 자원이 아니므로, 한 번 할당되면 독점적으로 사용되어야 한다. - 후보 컨테이너를 선택하고 상태를 `picked`로 변경하는 작업을 하나의 트랜잭션으로 수행해 동시에 두 요청이 같은 브라우저를 차지하는 문제를 방지한다. - 지역별로 D1 샤드를 구성해 위치 기반 컨테이너 관리도 유지했다. ## Queues를 활용한 상태 업데이트 배치 처리 - 수천 개 컨테이너가 5초마다 상태를 갱신하면 데이터베이스 쓰기 부하가 커진다. - 개별 쓰기만 사용하면 초당 약 1,000회 쓰기라는 한계로 인해 지역당 약 5,000개 컨테이너 수준에서 병목이 발생할 수 있다. - 상태 업데이트를 Queues에 모은 뒤 100개 단위로 배치 처리했다. - 배치 쓰기는 개별 쓰기보다 처리 시간이 크게 늘지 않으므로 처리량을 크게 높일 수 있다. - 100개 단위 배치 기준으로 지역당 최대 약 500,000개 컨테이너까지 업데이트할 수 있는 여유를 확보했다. - 각 컨테이너는 5초마다 자신의 상태를 지역별 큐에 기록한다. - 큐 소비자는 다음과 같이 설정했다. - 최대 배치 크기: 100개 - 최대 배치 대기 시간: 1초 - 최대 재시도 횟수: 1회 - 배치 쓰기의 현재 P95 지연 시간은 0.1ms다. ## 성능 개선 결과 - Workers 바인딩을 통한 브라우저 생성량: - 분당 최대 60개 - 동시 실행 브라우저: - 최대 120개 - 이전보다 4배 증가 - Quick Action 응답 시간: - 50% 이상 단축 - 개선 사항은 기존 고객의 코드 변경이나 Worker 재배포 없이 즉시 적용됐다. Cloudflare의 사례는 짧고 급격한 트래픽을 처리하는 시스템에서 최종적 일관성 저장소를 핵심 할당 경로에 사용할 때 발생할 수 있는 문제를 보여준다. 실시간 자원 예약에는 트랜잭션 기반 DB를 사용하고, 빈번한 상태 갱신은 큐와 배치 쓰기로 분리하는 설계가 효과적인 접근이다.

discord

메일이 (너무 많이) 도착했습니다: 3/25/26 음성 서비스 장애의 이면 (새 탭에서 열림)

Discord의 2026년 3월 25일 음성·영상 장애는 세션 관리 서버의 설정 변경에서 시작되어 여러 시스템을 거친 연쇄 장애로 확대됐다. Kubernetes 마이그레이션 중 세션 서버의 복제본 수를 줄이자 한 가용 영역에서 세션의 상당 부분이 비정상 종료됐고, 재연결·상태 정리 트래픽이 급증했다. 그 결과 사용자는 통화에 참여하지 못하고 “Awaiting Endpoint” 메시지를 보았으며, 장애는 12:13부터 15:30 PDT까지 이어졌다. ## 장애의 규모와 직접적인 증상 - 장애 발생 시점은 3월 25일 12:13 PDT, 복구 시점은 15:30 PDT였다. - Discord의 음성·영상 통화를 시작하거나 참여하기 어려웠다. - 많은 사용자가 통화 상태에서 **“Awaiting Endpoint”** 메시지를 확인했다. - 세션 관리 서버의 약 17%가 동시에 사라지면서 후속 시스템에 대규모 부하가 전달됐다. - 최종적으로 음성·영상 통화를 적절한 서버로 라우팅하는 서비스가 과부하를 겪었다. ## Kubernetes 마이그레이션과 변경 배경 - Discord는 Elixir 기반 실시간 서비스를 Kubernetes 환경으로 이전하고 있었다. - Elixir 서비스는 각 호스트에서 수천 개의 상태를 가진 프로세스를 실행한다. - 길드 - 사용자 presence - 통화 - 사용자 세션 - 서버를 종료할 때는 프로세스의 상태를 다른 노드로 넘긴 뒤 종료해야 서비스 중단을 피할 수 있다. - 배포 시스템은 서버의 엔터티 수가 0이 될 때까지 기다린 후 pod를 종료하도록 설계돼 있었다. - 주말 CPU 사용률이 높아지자 다음과 같은 리소스 조정을 시도했다. - pod당 CPU와 메모리 증가 - 전체 pod 수를 비례적으로 감소 - 스케줄러 사용량이 pod 수에 따른 고정 비용인지 측정 ## 세션 서버의 비정상 종료 - 변경 사항은 먼저 한 가용 영역에 배포됐다. - 복제본 수를 줄이는 과정에서 Kubernetes가 해당 영역의 pod 중 50%를 종료했다. - 서비스는 Kubernetes의 종료 신호를 받으면 프로세스를 다른 노드로 이전하려고 한다. - 그러나 진행 중인 다른 이벤트가 끝날 때까지 기다리는 안전 검사 때문에, Kubernetes의 종료 유예 시간이 먼저 만료됐다. - 결과적으로 프로세스 핸드오프가 시작되기 전에 pod가 종료됐다. - 세 영역이 균등하게 구성돼 있었기 때문에 Discord 전체 세션의 약 17%가 비정상적으로 중단됐다. ## Elixir GenServer와 모니터링 메시지의 폭증 - Discord의 실시간 시스템은 Elixir의 `GenServer` 프로세스를 기반으로 동작한다. - 각 프로세스는 자신의 mailbox에서 한 번에 하나의 메시지만 처리한다. - 단일 메시지 처리 방식은 동시성 문제를 줄인다. - 반대로 짧은 시간에 메시지가 폭증하면 처리 지연이 발생할 수 있다. - Elixir의 `Process.monitor` 또는 Discord의 확장형 `ZenMonitor`는 감시 대상 프로세스가 종료되면 `{:DOWN, ...}` 메시지를 전달한다. - 세션 17%가 동시에 종료되면서 해당 세션을 감시하던 여러 프로세스에 종료 알림이 일제히 전송됐다. - 이 메시지 폭풍은 실시간 시스템 전반으로 전파됐고, 가장 먼저 Gateway 서비스에 영향을 미쳤다. ## 사용자 재연결로 확대된 부하 - Gateway 서비스는 Discord의 모든 WebSocket 트래픽에 대한 입구이자 출구 역할을 한다. - 클라이언트가 연결되면 Gateway는 세션 서비스를 통해 사용자 세션을 만들고, 길드·채널·DM 등의 데이터를 전달한다. - 세션의 갑작스러운 종료는 클라우드 장애, 네트워크 오류, 클라이언트 환경 등에서도 발생할 수 있으므로 Gateway는 이를 감지하고 재연결을 유도한다. - 세션이 끊기면 Gateway는 사용자를 즉시 재연결시키고, 기존 세션을 낙관적으로 복구하려 한다. - 이번 장애에서는 대규모 세션 종료가 동시에 발생하면서 정상적인 복구 절차 자체가 대규모 재연결 부하로 변했다. - 제공된 글 내용은 이 재연결 과정과 이후 음성 라우팅 시스템의 상세한 연쇄 장애를 설명하기 직전에서 끝난다. ## 실용적인 교훈 - 상태를 가진 서비스를 Kubernetes에서 축소할 때는 replica 감소 자체보다 **상태 이전과 종료 유예 시간의 상호작용**을 검증해야 한다. - 장애 복구용 재연결 로직도 대규모 동시 장애에서는 부하 증폭기가 될 수 있으므로 재연결 속도 제한과 단계적 복구가 필요하다. - 한 서비스의 작은 설정 변경이 모니터링 메시지, WebSocket 재연결, 음성 라우팅 등 여러 계층을 거쳐 전혀 다른 병목을 압박할 수 있다. - 분산 시스템 변경은 단일 서비스의 CPU·메모리 지표뿐 아니라 장애 전파 경로와 최악의 동시 부하까지 함께 검증해야 한다.

figma

멀티플레이어를 더 안정적으로 (새 탭에서 열림)

Figma는 인메모리 상태와 30~60초 간격의 체크포인트에 의존하던 멀티플레이어 시스템에 변경 이력을 기록하는 저널(write-ahead log)을 도입했다. 저널은 파일의 전체 상태가 아닌 증분 변경을 자주 저장하므로 장애 발생 시 최신 체크포인트 이후의 변경을 재생해 복구할 수 있으며, 목표 데이터 손실을 1초 미만으로 줄였다. 또한 배포 시 모든 파일을 동시에 체크포인트하는 쓰기 부하 급증도 해소했다. ## 기존 멀티플레이어 구조 - 브라우저 클라이언트는 WebSocket으로 `multiplayer` 서비스에 연결한다. - 서버는 파일 상태를 메모리에 보관하면서 여러 클라이언트의 변경 사항을 수신·검증·정렬·충돌 해결한 뒤 전체 클라이언트에 전달한다. - 메모리 상태는 휘발성이므로 30~60초마다 파일 전체를 바이너리로 인코딩하고 압축해 S3에 체크포인트로 저장한다. - 체크포인트는 버전 기록 등 일부 기능의 기반이 된다. ## 체크포인트 중심 방식의 문제점 - 서버가 장애를 일으키면 마지막 체크포인트 이후 최대 60초의 작업을 잃을 수 있다. - 파일 전체를 저장하므로 파일의 크기와 복잡도가 커질수록 저장 비용도 증가한다. - 멀티플레이어를 재배포하면 메모리에 있던 모든 파일을 닫아야 하므로 동시에 대량의 체크포인트 쓰기가 발생한다. - 이로 인해 데이터베이스 부하가 급증하고, 배포가 사용자에게 보이지 않는 작업이어야 한다는 목표를 방해한다. ## 증분 변경을 저장하는 저널 - Figma는 파일 변경 사항을 기록하는 내구성 있는 트랜잭션 로그인 저널을 추가했다. - 멀티플레이어가 변경을 수락하면 변경 내용을 비동기적으로 저널에 기록한다. - 각 변경에는 파일별로 증가하는 시퀀스 번호를 부여한다. - 체크포인트에도 해당 시점의 시퀀스 번호를 함께 저장한다. - 저널에는 전체 파일이 아니라 사용자가 수행한 증분 변경만 저장한다. - 예: 텍스트 수정, 디자인 요소의 위치 변경, 목업 업데이트 등 - 증분 변경은 전체 파일보다 훨씬 작기 때문에 더 자주 기록해도 효율적이다. ## 장애 복구 방식 - 서버가 재시작되면 기존 체크포인트를 먼저 불러온다. - 체크포인트의 시퀀스 번호보다 큰 시퀀스 번호를 가진 저널 항목을 조회한다. - 해당 변경들을 순서대로 재생해 최신 파일 상태를 복원한다. - 기존 체크포인트 방식은 약 60초 간격으로 저장했지만, 저널은 약 0.5초 수준으로 변경 사항을 기록하는 방향을 취한다. - 그 결과 장애 시 데이터 손실 목표를 1초 미만으로 낮췄다. ## 배포 시 쓰기 부하 안정화 - 배포할 때 모든 연결을 종료하고, 아직 저장되지 않은 변경이 저널에 기록될 때까지 기다린다. - 99번째 백분위수 기준으로 이 과정은 1초 이내에 완료된다. - 배포를 위해 대규모 체크포인트를 한꺼번에 생성할 필요가 없어졌다. - 저널 쓰기는 평상시에도 지속적으로 발생하므로 데이터베이스 부하가 일정하고 예측 가능해진다. ## 데이터 저장소 선택 - 저널의 백엔드 저장소로 DynamoDB를 사용했다. - Postgres와 로컬 디스크 등 여러 선택지를 검토했지만, 높은 쓰기량을 수평 확장해야 한다는 점 때문에 Postgres는 선택하지 않았다. - 이 사례에서는 익숙한 데이터베이스보다 쓰기 규모와 확장성을 감당할 수 있는 저장소가 더 중요한 기준이었다. ## 변경 사항 배치 처리 - 클라이언트는 초당 30프레임, 즉 약 33ms마다 업데이트를 보낸다. - 모든 업데이트를 같은 빈도로 저널에 기록할 필요는 없으므로 여러 변경 사항을 묶어 일정 주기로 저장한다. - 이 배치 처리는 저널 쓰기 횟수를 줄이고 성능을 개선하면서도 체크포인트보다 훨씬 짧은 복구 지연 시간을 유지하기 위한 방식이다. Figma의 사례는 전체 상태를 드물게 저장하는 체크포인트와, 작은 변경을 자주 저장하는 저널을 함께 사용하는 구조가 실시간 협업 시스템에 적합하다는 점을 보여준다. 장애 복구 시간과 데이터 손실을 줄이려면 증분 로그를 도입하고, 시퀀스 번호를 기준으로 체크포인트와 로그를 연결하는 방식을 고려할 수 있다.

figma

LiveGraph: Figma의 실시간 (새 탭에서 열림)

Figma는 실시간 협업 제품에 필요한 데이터를 안정적으로 제공하기 위해 Postgres 위에 GraphQL 기반의 실시간 데이터 계층인 LiveGraph를 구축했다. LiveGraph는 프론트엔드가 선언적으로 데이터를 구독하면 데이터베이스 복제 스트림을 읽어 밀리초 단위로 변경 사항을 반영한다. 이를 통해 수동 이벤트 메시지와 클라이언트 상태 동기화의 복잡성을 줄이고, 대규모 실시간 데이터 구독을 지원한다. ## Figma에서 실시간 데이터가 필요한 이유 - 협업자가 파일을 추가하거나 권한을 변경하면 다른 사용자 화면에도 새로고침 없이 즉시 반영되어야 한다. - 따라서 서버에서 데이터를 한 번 가져오는 것만으로는 부족하며, 클라이언트가 현재 관심 있는 데이터의 변경 사항을 계속 받아야 한다. - 인프라 팀의 목표는 제품 개발자가 데이터 전파 방식이나 WebSocket 세부 구현을 직접 관리하지 않고도 실시간 뷰를 만들 수 있게 하는 것이었다. ## 기존 방식의 한계 - 초기에는 React 프론트엔드가 Ruby HTTP 엔드포인트에서 필요한 데이터를 한 번에 받아 Redux 전역 상태에 저장했다. - 데이터 변경 시 백엔드 코드에서 관련 클라이언트에 보낼 이벤트 메시지를 직접 작성하고, 프론트엔드는 WebSocket으로 이벤트를 받아 상태를 갱신했다. - 사용자와 데이터 규모가 커지면서 모든 데이터를 한 번에 로드하기 어려워졌고, 데이터를 점진적으로 불러오면서 다음 문제가 발생했다. - 특정 데이터가 메모리에 항상 존재한다는 보장이 없어짐 - 여러 제품 영역이 같은 데이터를 사용할 때 데이터 로딩 책임이 불분명해짐 - 이벤트를 어느 시점에 보내고 받아야 하는지 관리하기 어려워짐 - 단순한 “새 파일 생성” 이벤트와 달리 권한 변경은 다른 리소스의 가시성까지 연쇄적으로 바꿀 수 있어 이벤트 설계가 복잡했다. - 데이터베이스 쓰기 순서와 실시간 메시지의 송수신 순서가 항상 일치한다는 보장도 없었다. - 그 결과 클라이언트 상태가 서버 상태의 올바른 부분집합을 반영하지 못하는 일관성 버그가 발생했다. ## GraphQL 기반 Live Query 선택 - Figma는 개발자가 실시간 데이터 구독을 선언적으로 정의할 수 있는 일반적인 프레임워크가 필요하다고 판단했다. - GraphQL을 인터페이스로 사용하면 필요한 데이터와 관계를 쿼리로 표현하고, 시스템이 해당 데이터를 자동으로 가져오고 최신 상태로 유지할 수 있다. - 여기서 말하는 GraphQL 구독은 일반적인 이벤트 스트림 구독과 다르다. - GraphQL의 전통적인 `subscription`은 이벤트 메시지를 전달하는 방식에 가깝다. - LiveGraph가 목표로 한 것은 쿼리 결과 자체를 계속 갱신하는 “Live Query” 방식이다. - 프론트엔드는 GraphQL과 유사한 쿼리를 보내고, 서버는 결과를 JSON 트리로 반환한다. - 서버에는 엔터티와 관계를 정의하는 스키마 및 그래프의 일부를 조회할 수 있는 뷰가 존재한다. ## 기존 실시간 데이터베이스 대신 자체 구축한 이유 - Figma는 이미 Postgres를 대규모로 운영하고 있었기 때문에 Firebase나 RethinkDB 같은 별도의 실시간 데이터베이스로 이전할 수 없었다. - LiveGraph는 새로운 저장소가 아니라 기존 Postgres 위에 동작하는 쿼리 엔진이 되어야 했다. - Figma의 Multiplayer 시스템은 파일 단위의 쓰기와 충돌 해결을 담당하지만, LiveGraph는 여러 데이터의 조회와 실시간 동기화를 담당한다. - Hasura, Prisma, PostGraphile 등 GraphQL 기술도 검토했지만, 대규모 동시 구독을 핵심 요구사항으로 설계된 것은 아니었다. - Figma는 실시간 구독 수가 많아질수록 데이터베이스 부하가 커지는 폴링 방식도 피하고자 했다. - 폴링은 쿼리마다 주기를 정해야 한다. - 구독 수가 늘어나면 동일한 쿼리가 반복 실행되어 데이터베이스 부하가 증가한다. - 폴링보다 변경 발생 시점에 가까운 낮은 지연 시간을 확보하기 어렵다. ## 데이터베이스 복제 스트림 기반 설계 - LiveGraph는 주기적으로 데이터를 다시 조회하는 대신 Postgres의 데이터베이스 복제 로그를 추적한다. - 복제 스트림에서 변경 사항을 읽으면 실제 데이터베이스 변경을 감지한 뒤 구독 중인 쿼리 결과를 갱신할 수 있다. - 이 방식은 폴링보다 빠른 업데이트 지연 시간을 제공한다. - 다만 LiveGraph가 데이터베이스의 전체 변경량을 읽어야 하므로, 대규모 환경에서는 확장성이 중요하다. - Figma는 여러 데이터베이스 샤드의 변경 사항을 여러 머신에 분산 처리할 수 있는 구조를 고려했다. - 최종적으로 LiveGraph를 자체 구축한 이유는 Figma의 협업 기능에서 실시간 데이터가 핵심 기능이며, 이러한 요구사항이 경쟁력으로 이어질 수 있다고 판단했기 때문이다. ## 실용적인 결론 실시간 UI를 구축할 때 클라이언트별 수동 이벤트와 전역 상태 갱신에 의존하면 데이터 규모와 기능 복잡도가 커질수록 일관성 문제가 발생하기 쉽다. 기존 Postgres를 유지해야 하고 대규모 구독이 필요하다면, GraphQL 기반 선언적 쿼리와 데이터베이스 변경 스트림을 결합하는 방식이 폴링이나 수동 이벤트보다 확장성과 유지보수성 측면에서 유리하다.

figma

피그마의 멀티플레이어 (새 탭에서 열림)

Figma는 Google Docs처럼 복잡한 OT(Operational Transformation)를 적용하는 대신, 디자인 문서의 구조에 맞춘 단순한 자체 멀티플레이어 시스템을 구축했다. 클라이언트와 서버는 WebSocket으로 변경 사항을 동기화하고, 서버가 문서별 단일 조정자로 동작해 충돌을 관리한다. 이 방식은 실시간 협업뿐 아니라 오프라인 편집과 재접속까지 지원하면서도 Figma의 데이터 모델에 맞는 구현 단순성을 유지했다. ## 웹 기반 협업을 선택한 이유 - 멀티플레이어 기능이 있으면 파일을 내보내거나 복사본을 이메일로 주고받고, 변경 사항을 수동으로 동기화할 필요가 없다. - 링크 하나만으로 여러 사람이 현재 디자인 상태를 확인할 수 있다. - 디자이너뿐 아니라 카피라이터, 개발자 등도 같은 문서에 참여할 수 있다. - 초기에는 실시간 협업이 “디자인을 망치는 기능”으로 여겨졌지만, 웹 생산성 도구에는 자연스러운 기본 기능이 되었다. ## Figma의 클라이언트·서버 구조 - 웹 클라이언트는 WebSocket을 통해 멀티플레이어 서버 클러스터와 통신한다. - 문서마다 별도의 서버 프로세스를 두고, 해당 문서를 편집하는 사용자들이 같은 프로세스에 연결된다. - 문서를 열 때 클라이언트가 먼저 파일 전체를 내려받는다. - 이후 변경 사항은 양방향 WebSocket 연결을 통해 실시간으로 전송된다. - 댓글, 사용자, 팀, 프로젝트 같은 데이터는 멀티플레이어 시스템이 아니라 Postgres와 별도 동기화 시스템으로 관리한다. - 문서 편집과 기타 애플리케이션 데이터는 성능, 오프라인 지원, 보안 요구 사항이 다르기 때문에 구현을 분리했다. ## 오프라인 편집과 재접속 - 사용자는 네트워크가 끊긴 상태에서도 임의의 시간 동안 계속 편집할 수 있다. - 다시 온라인이 되면 클라이언트는 서버에서 최신 문서 사본을 받는다. - 그 위에 오프라인 동안 발생한 로컬 변경을 다시 적용한다. - 이후 새로운 WebSocket 연결을 통해 서버와 변경 사항을 계속 동기화한다. - 연결과 재연결 자체는 단순하게 만들고, 멀티플레이어의 핵심 복잡성은 이미 연결된 문서에 동시에 변경 사항이 들어오는 상황에 집중했다. ## OT와 CRDT 대신 자체 방식을 택한 이유 - OT는 Google Docs 등에서 널리 사용된 표준적인 협업 알고리즘이다. - 여러 사용자의 동시 작업을 변환해 충돌을 해결할 수 있지만, 구현과 검증이 복잡하다. - Figma는 텍스트 편집기와 달리 계층적인 디자인 객체를 편집하므로, 일반적인 OT 모델을 그대로 적용할 필요가 없다고 판단했다. - 스타트업으로서 빠르게 기능을 개발하고 실험하려면 더 단순한 구조가 유리했다. - Figma는 디자인 문서의 데이터 구조와 편집 패턴에 맞춘 전용 동기화 방식을 만들었다. ## 프로토타입을 통한 설계 검증 - 실제 제품 코드에 바로 적용하지 않고, 여러 클라이언트와 서버를 시뮬레이션하는 별도의 웹 기반 실험 환경을 만들었다. - 프로토타입에서는 여러 사용자의 상태와 변경 사항을 시각적으로 확인할 수 있었다. - 오프라인 클라이언트, 느린 네트워크, 제한된 대역폭 등 다양한 상황을 쉽게 재현했다. - 이를 통해 협업 알고리즘과 데이터 구조를 빠르게 비교하고 실험했다. - 설계가 확정된 뒤 프로토타입에서 검증한 아이디어를 기존 코드베이스에 이식했다. ## 서버 중심의 동기화 모델 - 하나의 문서에 연결된 사용자들은 같은 서버 프로세스를 공유한다. - 서버는 문서에 들어오는 변경 사항을 순서대로 처리하고 다른 클라이언트에 전달한다. - 클라이언트는 처음 받은 문서 상태를 기준으로 로컬 변경을 수행하면서 서버의 업데이트를 계속 반영한다. - 서버를 문서의 조정자로 두면 여러 클라이언트가 서로 직접 충돌을 해결할 필요가 줄어든다. - 동시에 발생한 변경의 처리 순서는 서버가 정하며, 모든 클라이언트가 최종적으로 같은 문서 상태에 도달하도록 한다. ## 이 접근법의 핵심 장점 - 범용 협업 알고리즘보다 Figma의 객체 중심 데이터 모델에 맞게 단순하게 구현할 수 있다. - 온라인 편집뿐 아니라 오프라인 작업과 재접속도 지원한다. - 별도 프로토타입으로 다양한 장애 상황을 반복적으로 테스트할 수 있다. - 문서 편집 동기화와 서비스 메타데이터 동기화를 분리해 각각의 요구 사항에 맞게 최적화할 수 있다. 실용적으로는 모든 협업 애플리케이션이 OT나 CRDT를 그대로 도입해야 하는 것은 아니다. 데이터 구조가 명확하고 충돌 패턴이 제한적이라면, 서버 중심의 단순한 프로토콜과 충분한 시뮬레이션 테스트를 결합하는 편이 더 빠르고 유지보수하기 쉬울 수 있다.