Springboot

Kafka 를 활용한 지연처리 (지연큐)

땍땍 2025. 6. 21. 19:25

프로젝트 진행 중, 클라이언트에게 전달 받은 요청을 5분 이후에 처리해야 하는 지연 처리 기능이 필요하게 되었다.

이를 위해 다양한 방안을 검토하였고, 최종적으로 Kafka 를 활용해 지연 처리를 구현하게 되었다.

해당 글에서는 이러한 지연 처리 구현까지의 접근 방식과 고민 등을 정리하고자 한다.

 

---------------------------------------------------

 

⚠️ 상황

  • 클라이언트에게 전달 받는 요청을 5분 이후에 처리 해야 함
  • 해당 서비스는 다중 서버로 구성
  • 기본 메세지 큐로 Kafka 를 사용
  • Redis 는 캐싱, 임시 데이터 저장 등 다용한 용도로 활용 중

---------------------------------------------------

 

🧪 시도

 

  1. RabbitMQ , SQS 등을 활용한 지연 큐 활용

RabbitMQ, SQS 와 같은 메세지 큐에서는 지연 큐 라고 하는 기능을 기본적으로 지원한다.

이러한 기능은 외부 시스템에 의해 자동으로 처리 되며, SQS 의 경우 클라우드 형식으로 구동 되기에 서버에 부담을 주지 않는다는 장점이 존재하나,

하나의 기능 ( 지연 처리 ) 만을 위해 Kafka 이외의 메세지 큐를 도입하게 됨으로 발생하는 복잡성 및 인프라 운영 비용 증가 등 여러 문제 점 등이 예상 되었기에 채택하지 않았다.

 

 

2. Redis Keyspace Notifications 를 이용한 TTL 이벤트 활용 지연 처리

Redis 에는 Keyspace Notifications 라는 기능이 존재한다.

이 기능을 활용하면, 특정 Key 에 대해 CRUD 등 어떠한 이벤트가 발생하였을 때, Pub/Sub 방식으로 애플리케이션에 이벤트를 전송할 수 있다.

이를 통해, TTL 만료 시 발생 하는 알림을 어플리케이션에서 수신하는 방식으로 지연 처리를 구현하고자 하였다.

 

하지만, 이러한 방식에는 크게 두 가지의 문제점이 존재하여 채택하지 못 하였다.

 

  • Redis 의 성능 저하

해당 기능을 활성화 하면 Redis 의 성능이 약 20% 가량 감소한다는 것을 확인하였다.

속도적인 측면을 중요시 해야하는 Redis 에서는 이는 문제가 될 것이라 판단하였고, 때문인지 Redis 에서도 기본적으로 해당 설정은 비활성화 되어 있다.

 

  • 알림 전송 보장 불가

해당 기능은 알림의 전송을 보장해주지 않으며, 일반적인 방법으로 이를 보장할 수 있는 방법 또한 존재하지 않는다.

때문에 알림이 발생하여도 어플리케이션에는 이를 수신하지 못 하여 요청이 유실 되는 경우가 발생할 수 있다.

 

 

3. Spring Scheduler 를 활용한 주기적인 관리

클라이언트로 부터 요청을 전달 받으면, 이를 타임 스탬프와 함께 DB에 저장해두고, Spring Scheduler가 주기적으로 해당 DB 를 조회하며 작업을 처리하는 방식이다.

이러한 방식은 비교적 구현이 간단하다는 장점이 있지만, 아래와 같은 이유로 채택하지 않았다.

 

  • 서버 장애로 인한 작업 유실 문제

스케쥴링을 진행하던 서버에 장애가 발생 시, 일반적으로는 이를 복구할 수 없다는 문제가 발생하였다.

 

  • 다중 서버 환경에서 중복 작업 실행 방지를 위한 추가 처리 필요

다중 서버에서, 동일한 작업을 중복 실행하게 되면 자원 낭비는 물론 동시성 이슈 또한 발생할 수 있기에 이를 위해 분산 락을 적용하거나, 스케쥴링을 위한 서버를 따로 구동하거나 하는 등의 추가적인 비용이 예상 되었다.

 

  • 처리할 요청이 없어도 주기적으로 동작해야해 자원 낭비

Spring Scheduler 는 기본적으로, 정해진 주기에 맞춰 반복해 동작하기에 발생되는 문제이며,

이러한 문제는 요청 별로 정확한 처리 시점을 보장하지 못 한다는 문제로 이어지게 된다.

예시 :

1번 요청은 3분 뒤에

2번 요청은 5분 뒤에

3번 요청은 6분 뒤에 처리 되어야 하는 경우,

스케쥴러가 5분 간격으로 동작하게 된다면 1,2,3 번의 처리 시간은 최소 5분~10분 후에 처리되는 경우도 발생할 것이다.

이러한 처리 시간을 보장하기 위해서는 결국 스케쥴러를 더욱 주기적으로 동작 시켜야 하는데, 만약 ms 초 단위까지 세세하게 체크해야 하는 경우 스케쥴링에 과도하게 자원이 할당 되는 등의 문제가 발생할 수도 있을 것이다.

 

  • 스케쥴러에 할당된 스레드를 다른 용도로 사용할 수 없는 문제

Spring Scheduler 보통 스케쥴러만을 위한 스레드풀을 별도로 관리한다.

이러한 스레드는 기본적으로 다른 작업에는 활용할 수 없으며 처리할 요청이 없을 때도 이러한 스레드를 대기 (낭비) 시켜야 하는 문제가 발생한다.

 

 

4. Quartz 클러스터를 활용해 주문 별 동적으로 작업을 예약해 처리

Quartz 는 Spring Scheduler 보다 더 세밀한 제어가 가능한 스케쥴링 라이브러리로 다음과 같은 이점이 있었다.

  • 요청 별 동적 스케쥴링 지원
  • 다중 서버 ( 클러스터 ) 지원
  •  

이러한 이점은 Spring Scheduler 에서 문제가 되었던 몇 가지의 문제를 해결할 수 있었다.

  • 요청 별 처리 시간 보장
  • 처리 작업 없을 경우 미동작
  • 다중 서버 환경에서의 별도의 처리 불필요
  • 서버 장애로 인한 작업 유실 방지

 

다만, Quartz 클러스터는 주기적으로 DB 에 접근해야만 했고 이때 발생하는 자원 소모가 Spring Scheduler 를 활용해 발생하는 자원 소모 보다 클 것이라 예상이 되었기에 최종적으로 해당 방식 또한 채택하지 않았다.

 

---------------------------------------------------

💡 해결

최종적으로는, Kafka 와 Thread.Sleep 을 활용한 방식으로 지연 처리를 구현하게 되었다.

  1. 클라이언트에게 요청을 전달 받은 후, 처리 예정 시간을 포함한 메세지를 메세지 큐에 발행
  2. 컨슈머가 메세지 수신 후, 현재 시간과 처리 예정 시간을 비교
    • 예정 시간 이전 : 처리 예정 시간까지 대기 (Sleep)
    • 예정 시간 도달 : 즉시 처리

 

위와 같은 기능은, 언뜻보기에는 정말 간단한 방식이지만 아래와 같은 이점이 존재했다.

 

  • 처리 할 요청이 없는 경우, 작업 및 스레드 미할당

일반 플랫폼 스레드를 활용하기에 처리할 요청이 없는 경우, 이러한 스레드를 다른 용도로 활용이 가능하다.

또한, 대부분의 시간을 Sleep 으로 대기하기에 해당 스레드가 전체적인 성능에 미치는 영향 또한 미미하다.

 

  • 메세지 큐의 재처리 기능을 통한 작업 및 요청 유실 방지

메세지 큐는 기본적으로 메세지가 정상적으로 처리 되었는지 확인하고 실패 혹은 처리 지연 시, 이를 재처리 하도록 도와주기에 작업이 유실되는 문제를 방지할 수 있다.

 

  • 메세지 큐를 통한 다중 서버 환경에서의 작업 분산

Kafka 의 파티션을 통해 작업을 처리할 서버의 갯수를 지정할 수 있으며, 내부적으로 라우팅을 진행해주기에 어플리케이션 단에서 이를 위한 비용을 소모하지 않아도 된다는 이점이 존재한다.

 

  • 큐의 FIFO 구조로 가장 먼저 발행된, 처리 예정 시간이 가장 빠른 메세지를 우선적으로 처리

가장 먼저 발행된 (처리 시간이 가장 빠른) 메세지가 우선적으로 컨슈머로 전달이 되기에, 컨슈머는 모든 메세지를 최소한의 시간만을 대기하여 메세지를 처리할 수 있다는 이점이 존재한다.