본문 바로가기

Project/기록

Log 영속성을 위한 MQ 도입

 

💡 로그 유실 없는 비동기 로그 수집 아키텍처

보안 로그 수집 시스템에서 단일 장애가 발생했을 때 로그 유실 위험이 있다는 문제를 발견했습니다. 기존에는 로그를 바로 DB에 기록하는 동기 구조였습니다. 하지만, 트래픽이 급증하게 되면 다음 문제가 발생할 수 있습니다.

  • DB Connection Pool 고갈
  • DML Lock 경합

이를 해결하기 위해 BullMQ 기반의 Queue 시스템과 Redis를 활용한 비동기 아키텍처로 전환하여 DB 부하를 차단하고, 트래픽 버스트를 안정적으로 받아낼 수 있는 비동기 구조로 개선했습니다.

Redis의 Eviction 정책, AOF 설정 등 운영 환경에서 고려해야 할 포인트들도 정리했으며, 장애 발생 시 유실 없는 처리를 위해 재처리 및 장애 대응 방안까지 반영했습니다. 이 글은 발생할 수 있는 장애 원인 분석부터 아키텍처 전환까지의 문제 해결 과정을 상세히 기록했습니다.

 

🚨 문제의 시작: 민감한 로그, 한 번 유실되면 돌이킬 수 없다

민감한 콘텐츠를 다루는 시스템을 운영하며, 사용자 로그가 단순한 기록을 넘어 매우 중요한 역할을 한다는 점을 깊이 인식하고 있었습니다. 로그는 시스템 사용자의 행동을 파악하고, 만약 문제가 발생했을 때 어디에서 문제가 생겼는지, 그리고 누가 해당 정보를 유출했는지 추적할 수 있는 핵심 수단입니다. 이러한 이유로 로그는 시스템은 안정성보안의 근간이자, 문제 발생 시 정확한 원인 분석과 대응을 가능하게 하는 필수 요소입니다. 하지만 기존 시스템은 로그를 DB에 바로 저장하는 구조였습니다. 이로 인해 다음과 같은 문제가 발생할 수 있습니다.

초기 아키텍처 구조

  • 장애나 트래픽 급증과 같은 비정상적인 상황에서 로그 저장이 제대로 이루어지지 않고 유실될 위험이 매우 큼
  • 중요한 행동 기록이 누락되면, 문제의 원인 규명은 물론, 보안 사고에 대한 정확한 대응과 책임 소재 파악이 어려움

따라서 이러한 로그 유실 문제는 단순 데이터 손실을 넘어, 서비스 신뢰성 저하와 심각한 보안 위협으로 직결된다는 점을 절실히 깨닫게 되었고, 이는 저희 시스템 아키텍처 구조를 근본적으로 재검토하는 계기가 되었습니다.


🚨 기존 방식의 한계와 근본적 위험

기존 시스템은 로그가 발생할 때마다 애플리케이션 서버에서 데이터베이스(DB)로 직접 저장 요청을 보내는 동기적 구조였습니다.
이 과정에서 별도의 중간 버퍼나 큐가 없었기에, DB가 로그 저장의 유일한 경로, 즉 단일 장애 지점(SPOF: Single Point Of Failure) 입니다. 또한, 장애에 대비한 자동 Failover나 데이터 복구를 위한 백업 및 이중화 체계도 제대로 갖추어지지 않은 상태였습니다. 아래는 서버의 부하 까지 고민한 내용입니다.

  • 단일 장애 지점(SPOF) 문제 : 
    DB에 장애가 발생할 경우, 로그 저장 요청이 실패하거나 타임아웃으로 로그가 유실될 위험이 큽니다.
  • 서버 과부하 및 트래픽 급증 시 문제 : 
    트래픽 급증과 함께 DB 연결 지연과 요청 실패가 빈번해져 로그 저장 지연 또는 누락이 발생합니다.
  • 자동 Failover 체계 미비 : 
    장애 발생 시 자동으로 대체 저장소로 전환되지 않아 로그 재수집이 어렵고, 데이터 손실이 발생할 수 있습니다.
  • 고가용성(HA) 및 복원력 부족 :
    하드웨어 장애, 네트워크 문제, 운영 실수 등 다양한 원인으로 로그 데이터가 영구 손실될 위험이 있습니다.
  • 대용량 로그 처리의 어려움 : 
    로그가 폭증할 경우 DB 부하가 급증하여 전체 시스템 안정성에 악영향을 줍니다.

💡 Message Queue 도입: 안정성 확보를 위한 설계 전환

기존 구조에서는 로그를 DB에 직접 저장하다 보니, 장애 상황 발생시 로그 유실 위험과 단일 장애 지점(SPOF) 문제로 시스템 안정성이 저하되는 상황이 발생할 수 있습니다. 민감한 콘텐츠를 처리하는 시스템에서 로그의 안정적 관리를 보장하기 위해 MQ 기반의 비동기 처리 구조로 전환하게 되었습니다. API Server와 DB간의 강결합을 제거하고 SPOF의 문제를 예방하기 위한 수단이었습니다.

Message Queue 도입 아키텍처

  • 단일 장애 지점(SPOF) 해소 : 
    MQ를 도입함으로써 애플리케이션 서버와 DB 간의 직접 연결 의존성을 제거하고, 장애 발생 시 로그 저장 실패로 이어질 위험을 줄였습니다.
  • 비동기 처리 및 내결함성 강화 : 
    로그 저장 요청이 MQ에 안전하게 쌓이고, 별도의 consumer가 순차적으로 DB에 기록함으로써 장애 상황에서도 로그 손실 없이 처리됩니다. 또한 작업 실패 시 재시도와 상태 추적이 용이해집니다.
  • 확장성과 유연성 확보 : 
    MQ 기반 구조는 대용량 로그 처리뿐 아니라 장애 복구와 데이터 복원력 면에서도 기존 방식보다 뛰어난 안정성을 제공합니다. 또한, 큐는 로그 발생 속도와 DB 저장 속도를 분리해 일시적인 부하 증가에도 유실 없이 안정적으로 버퍼링할 수 있도록 합니다. 이를 백프레셔(backpressure) 제어라고 하며, 대규모 트래픽 폭증 시에도 안정성을 확보하는 핵심 메커니즘입니다.

이 전환은 단순한 기술 변경이 아니라, 전체 시스템 안정성과 신뢰성을 한 단계 끌어올리는 중요한 전환점이 되었습니다.


💡 BullMQ와 Redis: 선택 이유 및 기술적 고려사항

Node.js 환경에 적합한 Message Queue 솔루션으로 BullMQ를 선택했습니다. BullMQ는 Redis 기반이며, Node.js의 비동기 이벤트 모델과 자연스럽게 어울려 빠르고 효율적인 작업 처리가 가능합니다.

BullMQ

Redis가 메모리 기반 데이터 저장소라는 특성 때문에 운영상의 고려사항이 있었고, 해당 내용에 대한 문제를 고려했습니다.

  • In-Memory 기반 :
    디스크 기반 DB보다 훨씬 빠른 입출력을 제공하지만, 메모리 용량 한계로 인한 OOM(Out Of Memory) 위험이 존재합니다. Redis의 TTL, Eviction Policy를 고민해야합니다.
  • AOF 기능 :
    Redis는 메모리 기반이라 장애 시 데이터가 날아갈 수 있습니다. AOF(Append Only File)를 활성화하면, 모든 쓰기 작업이 디스크에 순차적으로 기록되어 장애 발생 시 대부분의 데이터를 복구할 수 있습니다. 다만 fsync 주기 설정에 따라 마지막 몇 개 기록은 유실될 가능성도 존재합니다.
  • 작업 완료 후 메모리 자동 삭제 : 
    로그나 작업 결과가 Redis에 계속 쌓이면 메모리 부담이 커져 removeOnComplete 옵션을 통해 성공한 작업의 메타데이터를 Redis에서 제거해 메모리 및 저장소 사용량을 줄이고, 장기적인 안정성을 확보해야합니다.
  • Sentinel 및 클러스터 구성 : 
    Redis 서버 자체의 장애나 네트워크 문제는 시스템 전체에 영향을 미치므로, Sentinel과 클러스터를 통한 이중화 및 자동 장애 복구 체계가 반드시 필요합니다.
    • 현재는 AOF 기능을 바탕으로 Disk 저장으로 버티고 있으나, 추후 Sentinel이나 클러스터로 고가용성(HA)과 장애 복구까지 관리를 해야합니다.

결국, Redis는 성능과 확장성 면에서 뛰어나지만, 메모리 한계, 장애 복구, 데이터 영속성 확보 등 여러 위험 요소를 충분히 인지하고 대비하지 않으면 시스템 안정성을 해칠 수 있기 때문에 신중한 운영과 설계가 필수적이었습니다. Redis를 학습하면서 발생할 수 있는 문제를 파악해서 TTL 설정과 Eviction Policy를 적절하게 배치했습니다.


💡 Node.js 이벤트 루프와 BullMQ 안정성 확보

Node.js는 싱글 스레드 기반의 이벤트 루프 구조를 가지고 있기 때문에, 이벤트 루프가 차단(blocking)되면 전체 서비스 지연이나 멈춤 현상이 발생할 수 있습니다. 특히 BullMQ와 같은 Message Queue를 사용할 때 이 이벤트 루프 특성을 충분히 이해하지 않으면, 예기치 않은 안정성 문제가 발생할 수 있습니다. 

BullMQ Important Note

🚨 BullMQ에서의 Stalled 문제와 이벤트 루프의 상호작용

BullMQ는 내부적으로 작업이 Stalled 상태에 빠지는 것을 방지하기 위해 일정 주기로 Heartbeat를 갱신합니다. 이 Heartbeat 갱신은 기본적으로 setTimeout() 기반으로 실행되며, 약 30초마다 Redis에 작업이 정상 동작 중임을 알려 작업이 Stalled로 간주되지 않도록 관리합니다.

그러나 이 메커니즘은 Node.js의 이벤트 루프가 정상적으로 순환된다는 가정 하에 안정성을 제공합니다. 만약 아래와 같은 상황이 발생하면 문제가 나타날 수 있습니다.

  • CPU 사용률이 높은 작업이 이벤트 루프를 장시간 점유(CPU Bound)
  • I/O blocking 작업이 이벤트 루프를 지연
  • GC(Pause) 등으로 이벤트 루프 응답이 늦어짐

이 때 BullMQ의 Heartbeat를 갱신해야 하는 타이밍에 이벤트 루프가 대응하지 못하게 되고, 결과적으로 BullMQ는 해당 작업을 Stalled로 간주하여 Redis로 되돌려버리게 됩니다.

특히 이 현상은 반복되면서 지속적으로 Stalled 작업이 발생하고, Retry → Stalled → Retry → Stalled가 반복되며 전체 작업 흐름에 심각한 장애를 유발할 수 있습니다.

실제 실무에서는 이러한 Stalled 악순환이 발생할 수 있음을 반드시 고려해야 합니다. 이러한 문제를 해결하기 위해 다음과 같은 안정성 보강 방안을 설계하여 적용했습니다.

  • 워커 프로세스 분리 :
    BullMQ의 작업 소비(consumer)는 메인 API 서버 프로세스와 완전히 분리된 별도의 워커 프로세스에서 실행됩니다. 이를 통해 메인 프로세스의 이벤트 루프가 Block 되더라도 작업 처리에 영향을 미치지 않도록 했습니다.
  • CPU Bound 작업의 worker_threads 활용 :
    CPU 집약적인 연산은 worker_threads 모듈을 이용하여 멀티 스레드 환경에서 병렬 처리하도록 설계했습니다. 이를 통해 싱글 스레드 이벤트 루프에서 CPU 사용률이 급증하는 것을 방지하였고, 동시에 더 높은 처리량도 확보할 수 있었습니다.

이러한 설계를 통해 "BullMQ와 Node.js의 특성상 발생할 수 있는 이벤트 루프 블로킹 이슈와 Stalled 악순환 문제"를 사전에 예방하고, 시스템의 안정성과 신뢰성을 크게 향상시킬 수 있었습니다.


🚀 결과

  • 민감한 로그 유실 문제를 근본적으로 해소
  • 시스템의 안정성, 신뢰성, 확장성 대폭 강화
  • 장애 복구 및 내결함성 확보
  • Node.js + BullMQ + Redis의 아키텍처적 조화를 최적화

🚨 기술적 한계

  • 단일 장애 지점(SPOF) 문제
    • API Server와 DB 간의 강결합을 제거하는 MQ를 중간 버퍼 역할로 두었기 때문에,MQ를 중간 계층으로 도입하여 API 서버와 DB 간의 직접적인 강결합을 해소하고, 장애 발생 시 로그 유실 가능성을 낮출 수 있었습니다. 다만 MQ와 Redis 자체가 새로운 SPOF가 될 수 있으므로 이 역시 장애 대비책이 필요합니다.
  • 서버 과부하 및 트래픽 급증 시 문제
    • 서버 과부하 및 트래픽 급증은 해당 시스템에서 자주 발생되는 것은 아니지만, MQ의 역할로 TPS를 안정적이게 운영할 수 있고 단일 큐로 되어있기 때문에 급증되어도 순차 처리로 인해 서버 과부하로 인한 Connection Dry 문제나 Lock Wait timeout 문제는 예방할 수 있습니다.
  • 자동 Failover 체계 미비
    • 현재 Redis의 Sentinel이나 Cluster를 고민하고 있으나, 위와 마찬가지로 Redis 자체가 문제가 되어서 중단되었다면 AOF(Append of file)을 설정해 두었기 때문에 Disk에 저장됩니다. 대신 고가용성(HA)은 떨어지는 문제가 발생합니다.
  • 고가용성(HA) 및 복원력 부족
    • Sentinel, Cluster를 통해서 HA와 Failover 체계를 마련할 수 있지만 현재는 필요하지 않아 보류해둔 상태입니다. 오버 엔지니어링이 될 수 있는 부분이기 때문에 하나의 대응책으로만 마련했습니다.
  • 대용량 로그 처리의 어려움
    • 해당 아키텍처로 큰 대용량 처리는 불가능할 것으로 보입니다. 하지만 추후 Scale out을 통해서 서버를 증설하고 MQ도 한개가 아닌 여러개를 생성해서 대용량 로그를 처리할 수 있음을 판단합니다. 그리고 Log가 많아진다면 MSA로의 아키텍처 재 설계 또는 DB의 Replication을 고민할 수 있을 것 같습니다.