10-12 20:41
Today
Total
«   2025/10   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
관리 메뉴

개발하는 고라니

Delayed Event(지연 이벤트) 본문

Restart

Delayed Event(지연 이벤트)

조용한고라니 2025. 10. 12. 02:22
반응형

Event

이벤트라는 단어 참 친숙하지요?

일상 생활부터 소프트웨어 개발까지, 이벤트는 셀 수 없이 많은 곳에서 다양하게 쓰이고 있습니다.

 

저는 특히 도메인 이벤트(Domain Event)라는 개념을 중요하게 사용합니다. 도메인 이벤트란 "비즈니스적으로 의미 있는 어떤 사건이 발생했다"는 사실 그 자체를 말합니다.

예를 들어 '주문이 생성되었다'거나 '회원 가입이 완료되었다'는 사실이 바로 도메인 이벤트입니다.

이렇게 사건을 중심으로 시스템을 설계하면, 비즈니스 로직 간의 결합도를 낮추어 시스템을 훨씬 유연하게 만들 수 있습니다.

Delayed Event

이번에는 지연 이벤트에 대해 이야기 해보고자 합니다.

 

"지연 이벤트"란 시스템에서 어떤 일이 발생했을 때 그에 대한 후속 조치를 즉시 실행하지 않고 의도적으로 지정된 시간만큼 지연시킨 후 처리하는 이벤트 혹은 메세지를 의미합니다.

 

활용 사례는 정말 많습니다.

대표적으로 예약 메세지 가 있겠네요. "다음 날 아침 9시에 보고서 제출 메세지를 보내줘" 처럼요.

또 주문에서도 쓰일 수 있습니다. 주문은 했지만 결제를 10분내 하지않은 주문 자동 취소 같이 쓰일 수 있겠네요.

어떻게 구현할까?

방법 또한 다양합니다.

이 글에서는 크게 3가지로 설명하고자 합니다.

  • Batch 기반 Polling
  • Message Queue
  • Redis Keyspace Notification

Batch 기반 Polling

도메인의 변경이 일어났을 때 발행한 이벤트를 Database 혹은 Cache 같은 곳에 저장합니다.

예를 들면 "주문이 발생됨" 이라는 데이터와 주문 ID, 언제 소비(처리)가 되어야하는지 시간을 넣어두면 좋겠네요.

 

이제 배치 Job을 구성하고 매 1분마다 해당 데이터를 탐색합니다. 아직 소비가 되지 않았으면서 처리되어야 하는 시간이 현재시간에 일치하는 이벤트 데이터를 찾아 결제가 완료되었는지 검사하고 조건에 따라 처리합니다.

 

가장 단순하지만 비효율적이라 생각되지 않나요? 저는 가장 먼저 떠올렸지만 바로 생각을 접었습니다.

 

1. 확장성이 떨어집니다. 다른 종류의 지연 이벤트를 활용한 로직을 구성해야한다면, 또 다른 배치 Job을 만들어야합니다. 또한 데이터의 양이 늘어나면 스캔해야할 데이터의 범위도 늘어나게 됩니다. 이는 Database 의 부하에도 영향을 줄 수 있습니다.

 

2. 불필요한 자원이 소비됩니다. 실제로 처리할 이벤트가 없더라도 매 분 배치는 동작하게 됩니다.

 

3. 초단위 까지 맞춰서 하기엔 무리가 있습니다. 위에서 말씀드렸듯, 배치를 1분마다 동작하게 합니다. 이를 1초마다 동작하게 한다면 (2)의 단점이 매우 부각될 뿐더러, 배치 Job이 밀리며 의도대로 동작하지 않을 리스크가 있습니다. (무엇보다 일반적으로 그렇게 하지 않습니다.)

Message Queue

메세지 큐로는 2가지를 알아보았습니다.

  • AWS SQS
  • RabbitMQ

(아래에서 이벤트와 메세지를 혼용해서 사용하고 있습니다. context에 따른 것이니 크게 개의치 않아도 됩니다.)

SQS (Simple Queue Service)

AWS에서 제공하는 SQS는 대표적으로 다음과 같은 특징을 지닙니다.

  • 비동기 처리(Async Processing)
  • 완전 관리형 서비스(Fully Managed Service)
  • 메세지 처리 보장
  • 메세지를 큐에 저장하여 프로듀서와 컨슈머를 분리

RabbitMQ나 Kafka와 같이 직접 구성하고 관리해야하는 인프라와 달리 SQS는 AWS에서 모두 관리를 해주기에 우리는 단순히 사용만 하면 됩니다.

 

단순하게 메세지 큐가 필요하다면 바로 이용했겠지만, 아래와 같은 이슈들로 인해 후순위로 밀려났습니다.

 

1. SQS는 "지연 메세지" 기능을 제공하지만, 최대 15분 까지만 제공합니다.

 

2. 지연 메세지 기능을 사용하면 FIFO(First-In-First-Out) Queue를 사용할 수 없습니다. 이는 메세지 순서를 100% 보장할 수 없음을 의미합니다. (SQS는 Standard Queue를 기본적으로 사용합니다. 이는 순서를 보장하진 않습니다.)

 

3. At Least Once Delivery

SQS는 "메세지가 최소 한 번 소비자에게 전달" 을 보장합니다. 다시 말해 네트워크 이슈 등으로 두 번 이상 전달될 수 있다는 의미입니다. 따라서 멱등성 있는 설계가 요구됩니다.

 

4. 가시성 시간 초과(Visibillity Timeout)

소비자가 Queue에서 메세지를 가져가면, 그 메세지는 다른 소비자에게 보이지 않는 "처리 중" 상태가 됩니다. 이처럼 숨겨져 있는 시간을 가시성 초과 시간이라 합니다.

가시성 시간 초과는 메세지 처리 시간보다 너무 길거나, 너무 짧지 않도록 적절히 설정해야합니다.

 

이외에 고려해야할 점은 메세지의 크기 제한(1MB), DLQ 등이 있으나 크게 의미를 두진 않았습니다.

 

만약 SQS를 사용하기로 결정했다면 최대 15분 지연이라는 이슈를 커버하기 위해 아래와 같은 아키텍처를 설계했을 것 같습니다.

 

메세지를 발행할 때 최대 15분의 지연 설정을 넣어 발행합니다. 비록 이 메세지가 30분 뒤에 소비되어야 한대도 말이죠.

위에서도 주문을 예를 들었으니, 마찬가지로 주문을 이용해봅니다.

 

이 메세지는 어떤 주문이고, 어느 시간에 처리가 되어야하는지 적혀있습니다.

N분 후 이 메세지는 Consumer에게 소비가 될 것 이고, 메세지를 읽은 컨슈머는 시간을 비교해 처리해야하는 시간보다 일찍 읽었다면,

다시 지연 설정을 넣어 SQS에 메세지를 발행합니다.

반대로 제 시간에 읽었다면 결제가 되었는지 체크하고 조건에 따라 주문을 취소합니다.

RabbitMQ + Delayed Plugin

RabbitMQ는 AMQP 프로토콜을 구현한 오픈 소스 메시지 브로커로, 시스템 간에 메시지를 안정적으로 전달하는 중간 다리 역할을 합니다.

 

아래와 같이 주요한 특징을 지닙니다.

 

  • 유연한 라우팅 (Flexible Routing)  메시지를 단순히 큐에 넣는 것을 넘어, 'Exchange'라는 라우터를 통해 다양한 규칙(Direct, Topic, Fanout 등)으로 메시지를 여러 큐에 분배하거나 특정 큐에만 선별적으로 보낼 수 있습니다. 이를 통해 복잡한 메시징 시나리오를 쉽게 구현할 수 있습니다.
  • 비동기 처리와 낮은 결합도 (Asynchronous & Decoupled) 메시지를 보내는 서비스(Producer)와 처리하는 서비스(Consumer)가 서로를 직접 알 필요 없이 독립적으로 동작합니다. 이는 한쪽 서비스의 장애가 다른 쪽으로 전파되는 것을 막아주며, 시스템 전체의 안정성과 확장성을 높여줍니다.
  • 데이터 보증 및 안정성 (Data Guarantee & Reliability) 메시지를 디스크에 저장하는 지속성(Durability) 옵션과 소비자가 처리를 완료했음을 확인하는 메시지 확인(Acknowledgement) 메커니즘을 통해, 서버가 재시작되거나 장애가 발생해도 메시지가 유실되지 않도록 보장합니다.
  • 고가용성 및 확장성 (High Availability & Scalability) 여러 서버를 하나로 묶는 클러스터링(Clustering)과 큐의 데이터를 여러 노드에 복제하는 미러링(Mirroring) 기능을 통해 일부 서버에 장애가 발생해도 중단 없이 서비스를 운영할 수 있습니다.
  • 풍부한 기능과 성숙한 생태계 (Rich Features & Mature Ecosystem) 메시지 유효 시간(TTL), 데드 레터링(처리 실패 메시지 격리), 관리자 UI, 다양한 프로그래밍 언어 지원 등 오랜 기간 발전해 온 성숙한 기능들을 제공하여 개발 및 운영 편의성이 뛰어납니다.

 

RabbitMQ에 Delayed Plugin을 이용하면 안정적으로 지연 메세지를 전송하고 소비할 수 있습니다.

지연 속성은 queue나 exchange 에 설정하는 것이 아닌 메세지에 설정하기에 메세지마다 3분, 5분, 10분 등 다양하게 설정하여 전송할 수 있습니다.

 

다만 현재 팀에서 RabbitMQ 를 운용하고 있지 않기에 배제하였습니다.

Redis Keyspace Notification

Redis docs : Redis keyspace notifications

Redis Keyspace Notifications 란?

Redis를 단순한 데이터 저장소를 넘어, 데이터에 변화가 생겼을 때 먼저 우리에게 알려주는 똑똑한 이벤트 발행자(Publisher)로 만들어주는 기능입니다.

 

이 기능을 사용하면, 주기적으로 데이터를 확인하는 비효율적인 방식(Polling)을 없애고 실시간으로 데이터 변경에 반응하는 동적인 애플리케이션을 만들 수 있습니다.

어떻게 동작하는가?

Redis의 Pub/Sub (발행/구독) 모델을 기반으로 동작합니다.

 

데이터에 특정 이벤트가 발생하면, Redis는 정해진 채널로 메시지를 발행(Publish)합니다. 애플리케이션은 관심 있는 채널을 구독(Subscribe)하고 있다가, 메시지가 도착하면 즉시 알아채고 특정 동작을 수행합니다.

## 주요 이벤트

SET: 키에 새로운 값이 저장될 때
DEL: 키가 삭제될 때
EXPIRE: 키의 유효시간(TTL)이 만료되어 사라질 때 (활용도 높음)
HSET: 해시(Hash) 데이터가 변경될 때

 

주의사항

  • 약간의 부하
    • 이벤트를 발행하는 과정에서 Redis 서버의 CPU를 아주 약간 더 사용합니다. 대부분의 경우 무시할 수준이지만, 초당 수십만 건의 이벤트가 발생하는 환경에서는 영향을 고려해야 합니다.
  • 전달 보장
    • 100% 전달을 보장하지는 않습니다. 구독 클라이언트가 잠시 네트워크 문제로 끊겼을 때 그 사이의 이벤트를 놓칠 수 있습니다. 따라서 반드시 처리되어야 하는 중요한 데이터는 RabbitMQ 같은 메시지 큐와 함께 사용하는 것이 좋습니다.
  • 중복 처리와 Race Condition
    • keyspace notification은 기본적으로 pub/sub 구조로 동작합니다. 즉, subscriber가 N 개이면 N번 소비하여 수행합니다.
    • 단 한번만 처리되어야하는 로직이라면 keyspace notifications외 다른 방법을 강구하거나, 동시성 제어를 해야합니다.

Conclusion

결과적으로 저는 지연 이벤트를 처리하기 위해 Redis의 Keyspace Notification을 이용하기로 했습니다.

 

이를 이용하면 위에서 언급했듯, pub/sub 으로 동작하기 때문에 subscribe 한 모든 서버에 이벤트가 전달되게 됩니다. 우리는 보통 운영환경에서 멀티서버 구조를 갖기에 반드시 중복으로 처리되게 됩니다.

 

후속 글에서 이를 다루겠습니다.

Redis Keyspace Notification 에 대해 좀더 자세히 알아보고, redis-cli를 통해 간단히 사용해보며 실제 Code로 구현해봅니다.

어떠한 설계로 중복처리와 race condition을 방지했는지 말씀드리고 완성된 아키텍처를 설명드리겠습니다.

반응형

'Restart' 카테고리의 다른 글

Redis Keyspace Notification playground  (0) 2025.10.12
Comments