Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

임베딩 처리

이승찬 edited this page Oct 28, 2025 · 4 revisions

Redis 기반 임베딩 비동기 큐와 워커 분리 구조 설계기

Bubblog 프로젝트는 사용자가 작성한 블로그 글을 AI 모델을 통해 임베딩하고, 이를 기반으로 개인화된 챗봇 서비스를 제공하는 시스템이다. 이 과정은 Spring Boot 서버(서비스 및 데이터 관리)와 Node.js 기반 AI 서버(임베딩 처리)로 구성되어 있다. 이번 글에서는 이 두 서버 간의 비동기 데이터 전송 구조를 설계하고 개선해 온 과정을 정리한다.


1. 임베딩의 특성과 문제 인식

임베딩 대상이 되는 블로그 글의 길이는 고정되어 있지 않다. 짧은 글은 수십 단어에 불과하지만, 긴 글은 수천 단어에 이를 수도 있다. 임베딩 연산은 입력 길이에 비례해 처리 시간이 증가하며, 한 게시글이 여러 개의 청크로 분할되면 연산량이 선형적으로 커진다. 즉, 텍스트 길이 n에 따라 처리 시간과 부하가 불균일하게 변동하는 특성을 갖는다.

초기에는 Spring Boot 서버가 글이 수정될 때마다 AI 서버로 HTTP 요청을 보내 임베딩을 수행하도록 했다. Spring Boot는 WebClient를 통해 비동기 요청을 보내고, AI 서버의 응답을 기다리지 않은 채 다음 로직을 처리하는 방식이었다. 표면적으로는 "비동기 처리"였지만, 이 구조에는 근본적인 문제가 있었다.

  • 요청 유실 위험: AI 서버가 일시적으로 다운되거나 네트워크가 불안정할 경우, 요청이 그대로 손실됨
  • 재시도·복구 로직 부재: 실패한 요청을 다시 시도하거나 누락된 작업을 복원할 수단이 없음
  • 서버 결합도 증가: 두 서버가 실시간으로 연결되어야 하므로 한쪽 장애가 다른 쪽에도 영향을 줌

즉, "HTTP 비동기 호출"은 응답 대기만 제거했을 뿐, 데이터 신뢰성과 장애 복원력 측면에서는 완전히 불안정했다. 이 구조로는 대규모 게시글 임베딩 작업이나 서버 장애 상황을 감당하기 어려웠다.


2. 메시지 브로커 도입 시도와 현실적 제약

이 문제를 해결하기 위해, 팀은 메시지 브로커 기반 비동기 구조를 도입하기로 했다. 처음 검토한 것은 Kafka였다. Kafka는 대용량 스트리밍 데이터 파이프라인에서 널리 사용되며, 생산자(Producer)와 소비자(Consumer)를 명확히 분리해 안정적인 데이터 전송을 보장한다.

우리는 Spring Boot를 Producer로, Node.js를 Consumer로 구성하여 Spring Boot가 게시글 정보를 Kafka에 전송하면 Node.js가 이를 받아 임베딩을 수행하는 구조를 구상했다.

하지만 실제 배포 환경은 메모리 1GB의 EC2 인스턴스였다. Kafka 컨테이너 하나가 기본적으로 약 1GB의 메모리를 요구했고, 이미 같은 인스턴스에서 Spring Boot와 Redis가 함께 구동 중이었다. 결과적으로 서버는 부팅 직후 다운되었고, 메모리 제한을 걸어도 Kafka 런타임 자체가 너무 무거워 안정적으로 동작할 수 없었다.

결론적으로, Kafka는 기술적으로 훌륭했지만 우리의 리소스 환경에는 맞지 않았다. 당시 시스템 규모에서 Kafka를 운영하는 것은 명백한 오버엔지니어링이었다.


3. Redis List를 활용한 대안 설계

Kafka를 포기한 뒤, 팀은 "저비용·저복잡도·충분한 안정성"이라는 기준을 세웠다. 그리고 이미 서비스 내에서 세션 캐싱 용도로 사용 중이던 Redis를 메시지 큐로 확장 활용하기로 했다.

Redis의 List 자료구조는 기본적으로 FIFO(First-In, First-Out) 큐로 동작한다. 우리는 이를 이용해 임베딩 요청을 안정적으로 전송하고 처리하는 구조를 설계했다.

  • Spring Boot (Producer): 글 수정 시 임베딩이 필요한 경우, LPUSH embedding:queue {...} 명령으로 Redis 리스트에 요청 메시지를 추가한다.
  • Node.js (Consumer): BRPOP embedding:queue 0으로 큐의 데이터를 블로킹 대기하며, 새 메시지가 들어오면 즉시 가져와 처리한다. 큐가 비어 있으면 "잠들어 있는 상태"로 연결만 유지하므로 리소스 낭비도 없다.

이렇게 함으로써,

  • 요청은 Redis에 안전하게 저장되어 AI 서버가 일시적으로 중단되어도 유실되지 않고,
  • 실패 시에는 재시도하거나 embedding:failed 리스트로 옮겨서 추적할 수 있게 되었다.

4. 임베딩 워커 프로세스 분리

Redis 큐 구조를 도입하면서, 임베딩 처리를 기존 AI 서버 내부에서 실행할지, 별도 프로세스로 분리할지가 논의되었다. 결론적으로, 우리는 임베딩을 독립적인 워커 프로세스(worker process) 로 분리했다.

그 이유는 명확했다.

  1. 부하 분리 임베딩은 CPU 연산량이 높고, 게시글의 길이에 따라 처리 시간이 달라진다. 이를 메인 AI 서버에서 직접 처리하면 실시간 챗봇 응답 성능이 저하될 수 있다. 워커를 분리함으로써 임베딩 부하가 메인 서비스에 영향을 주지 않게 했다.

  2. 수평 확장성 확보 Redis 큐는 다수의 워커가 동시에 메시지를 소비할 수 있는 구조다. 따라서 부하가 증가하면 워커 프로세스를 여러 개 띄워 병렬 처리가 가능하다. 서버 코드 변경 없이 단순히 워커 개수를 늘리는 것만으로 확장이 이뤄진다.

  3. 장애 복원력 워커가 중단되더라도 Redis 큐에 데이터가 남아 있으므로, 재시작 시 누락 없이 이어서 작업을 수행할 수 있다. HTTP 요청 기반에서 불가능했던 안정적인 복원이 가능해졌다.

  4. 연구 환경 분리 팀은 장기적으로 OpenAI API를 사용하는 대신 자체 임베딩 모델을 직접 구현해보는 실험을 계획하고 있었다. 워커 구조는 이러한 실험을 메인 서비스와 분리해 안전하게 진행할 수 있는 환경을 제공했다.

결과적으로, 임베딩 워커는 서비스 운영과 연구를 동시에 지원하는 유연한 백그라운드 처리 계층이 되었다.


5. 데이터 처리 구조

Redis에는 다음과 같은 형태의 메시지가 저장된다.

{"postId":28,"title":true,"content":false}
  • title, content는 해당 항목의 임베딩 필요 여부를 의미한다.
  • 워커는 메시지를 읽어 DB에서 해당 게시글(postId)을 조회하고, 플래그가 true인 항목만 임베딩을 생성한다.
  • 결과는 post_title_embeddings, post_chunks 테이블에 저장된다.
  • 오류 발생 시 재시도 메타데이터(attempt, lastError)를 추가해 다시 큐에 넣거나, 반복 실패 시 embedding:failed 리스트에 기록된다.

이 설계는 Redis 내 데이터량을 최소화하고, DB를 단일 진실원(source of truth)으로 유지한다.

[画像:image]

6. 결과 및 평가

Redis 기반 비동기 큐와 워커 분리 구조는 실제 운영 환경에서 다음과 같은 효과를 가져왔다.

  • 데이터 유실 방지: 서버 재시작이나 일시적 장애에도 요청이 안전하게 보존됨
  • 부하 분리: 임베딩 연산이 실시간 응답 성능에 영향을 주지 않음
  • 확장성: 워커 프로세스 추가만으로 병렬 처리 가능
  • 유지보수성 향상: 큐 기반 구조로 실패 내역 추적 및 복구가 용이
  • 리소스 효율성: Kafka 대비 수십 분의 일 수준의 메모리로 안정 동작

결국 우리는 HTTP 비동기 호출 기반 구조의 한계(유실, 결합, 복구 부재) 를 극복하고, "단순하지만 견고한" 비동기 아키텍처를 구축할 수 있었다. Redis 큐와 워커 구조는 현재까지도 Bubblog의 임베딩 파이프라인을 안정적으로 운영하는 핵심 구성 요소로 자리 잡았다.


Clone this wiki locally

AltStyle によって変換されたページ (->オリジナル) /