Porta
스트리밍 이벤트를 메모리 밖으로 옮긴 이유

엔지니어링

스트리밍 이벤트를 메모리 밖으로 옮긴 이유

단일 프로세스에서는 충분해 보였던 in-memory 상태가 멀티 워커에서 깨지기 시작했고, Porta는 스트리밍 이벤트를 DB append log로 바꿨어요.

Porta의 첫 스트리밍 구현은 나쁜 출발은 아니었어요. 생성 진행 이벤트를 프로세스 메모리에 저장하고, 스트림 엔드포인트가 그 상태를 읽어 브라우저에 전송하는 방식이었죠.

단일 프로세스에서는 꽤 잘 동작했어요. 문제는 많은 설계가 그렇듯, 프로세스 경계를 넘는 순간부터 시작됐어요.

처음 방식이 취약했던 이유

크게 두 가지 문제가 있었어요.

첫 번째는 정리(cleanup)였어요. 완료된 스트림 상태가 메모리에 예상보다 오래 남을 수 있었고, 정리 시점도 이후 접근 패턴에 영향을 받았어요.

두 번째가 더 본질적이었어요.

백그라운드 생성 작업과 /stream 요청이 서로 다른 워커에 배정될 수 있게 되면, 더 이상 하나의 메모리 상태를 함께 보고 있다고 가정할 수 없었어요.

즉 사용자는 정상적인 running 화면을 보고 있어도, 생성 작업을 쓰는 프로세스와 스트림을 읽는 프로세스가 다르면 step이나 preview 같은 live 이벤트를 놓칠 수 있었어요.

이런 종류의 버그는 제품을 "가끔 랜덤하게 안 되는 것처럼" 느끼게 만들어요.

진짜 경계는 함수가 아니라 저장소였다

여기서 배운 핵심은 이거였어요.

문제의 경계는 "어느 helper가 이벤트를 publish하느냐"가 아니었어요. 핵심은 "생성과 스트리밍이 함께 믿을 수 있는 저장소가 무엇이냐"였어요.

생성 작업과 스트림 응답이 다른 워커에서 돌아갈 수 있다면, 프로세스 메모리는 계약으로 쓰기엔 너무 로컬한 상태였어요.

조금만 시스템이 자라면 깨질 수 있는 가정이 숨어 있었던 셈이에요.

해결은 이벤트를 공유 가능하고 durable하게 만드는 것이었다

그래서 Porta는 in-memory 상태 대신 DB 테이블에 스트림 이벤트를 적재하기 시작했어요.

구조는 의도적으로 단순해요.

  • project_id
  • seq
  • event_json
  • is_terminal
  • created_at

이 작은 변화가 런타임 동작을 꽤 바꿨어요. 생성 워커는 이벤트를 shared storage에 append하고, 스트림 엔드포인트는 마지막 sequence 이후의 이벤트를 읽어요.

이제 어떤 워커에서도 같은 순서의 이벤트 히스토리를 읽을 수 있고, 특정 프로세스 메모리에 기대지 않아도 돼요.

바로 좋아진 점들

이 구조는 한 가지 이상을 동시에 해결했어요. 우선 멀티 워커 환경에서 writer와 reader가 같은 source of truth를 보게 됐어요.

또 sequence 기반으로 ordering이 명확해져서 replay 동작과 향후 resume semantics를 더 설명 가능하게 만들었어요. 현재 reconnect 경로는 여전히 스트림 처음부터 다시 읽지만, 적어도 그 재생은 worker-local memory가 아니라 shared state를 기준으로 이뤄져요.

그리고 done, error 같은 terminal 상태도 일시적인 메모리 조건이 아니라 명시적인 이벤트로 저장되기 때문에 종료 판단이 더 건강한 계약이 됐어요.

즉 "이 객체가 아직 살아 있기를 바란다"는 식의 가정에서 벗어난 셈이에요.

대신 받아들인 트레이드오프

물론 공짜는 아니었어요.

이제 생성 도중 스트림 이벤트를 DB에 쓰고, 스트림 엔드포인트는 새 row를 polling해서 읽어요.

그 말은 곧:

  • 생성 중 DB write가 늘어나고
  • retention과 cleanup 정책이 중요해지고
  • 스트림이 "메모리 기반 초경량"이 아니라 "공유 가능하고 durable한 계약"으로 바뀌었다는 뜻이에요

그래도 이 제품 단계에서는 그 선택이 맞았어요.

Porta에 필요했던 건 거대한 event bus가 아니라, 일반적인 확장과 재연결 시나리오를 견디는 계약이었기 때문이에요.

이 패턴이 제품과 잘 맞았던 이유

Porta의 생성 스트림은 무한정 이어지는 채팅 시스템이 아니에요. 순서가 있는 상태 업데이트가 있고, terminal end state가 있는, 경계가 뚜렷한 작업이에요.

그래서 append-only event log가 자연스럽게 맞아요.

우리가 풀고 싶은 질문도 복잡하지 않아요.

"내 생성 작업에서 지금까지 무슨 일이 일어났는가?"

이렇게 문제를 바라보면, 워커 로컬 메모리보다 durable한 ordered event가 더 설득력 있게 느껴져요.

마무리

첫 구현이 틀렸던 건 아니에요. 다만 너무 로컬했어요.

많은 엔지니어링 작업은 사실, 로컬한 가정이 분산 시스템의 가정으로 바뀌는 순간을 제대로 알아차리는 일에 가까워요.

Porta에서는 스트리밍 이벤트를 shared storage로 옮긴 시점이, 아키텍처가 실제 런타임 구조를 따라잡은 순간이었어요.

출처