setTimeout() 메서드
가장 간단하고 기본적인 방법으로 주어진 시간이 지나면 콜백함수에서 만료 완료 이벤트를 발행한다. 하지만 매우 큰 단점이 있는데 setTimeout() 메서드가 바로 메모리에 타이머를 저장한다는 것이다. 만약 만료 서비스가 어떤 이유로든 다시 시작된다면 그때 실행 중이던 모든 타이머가 사라지게 되는 참사가 발생한다. 따라서, 이 방법은 매우 간단하지만 치명적인 단점을 가진다.
이벤트 재전송
이 방법은 예약 생성 이벤트에 대한 구독자를 설정하는 것으로 이벤트가 들어오면 이벤트의 만료 시간을 나타내는 expiration 시간이 과거인지 확인한다. 만약 과거라면 만료 완료 이벤트를 발생시키고 아니면 아무런 행위를 하지 않는다. 아무런 행위를 하지 않으면 NATS Streaming은 설정 시간(e.g., 5초) 후에 자동으로 이벤트를 전달한다. 이러한 재전송 매커니즘으로 만료 완료 이벤트를 발행한다. 하지만 큰 단점이 존재하는데 애플리케이션 내에서 반복적으로 실패하는 이벤트를 추적하는 한 가지 방법은 이벤트가 재전송된 횟수를 추적하는 것이다. 횟수를 기준으로 이벤트를 처리할 수 없다는 일종의 경고를 전송할 수 있다. 따라서, 재전송 메커니즘은 로깅(logging) 목적으로도 사용되고 핵심 비즈니스 논리로도 사용된다면 상황이 매우 복잡해질 수 있다.
이벤트 버스
이 방법은 NATS Streaming 서버가 지원하지 않는 방법으로 메시지 브로커의 이벤트 버스에 내재하는 기능을 사용한다. 즉, 예약 생성 이벤트가 발생하면 만료 서비스에 수신한 후 지체 없이 만료 완료 이벤트를 발행하는데 대기할 시간을 이벤트 버스에 요청하는 것이다. 다시 말해, 이벤트 버스가 대기 시간만큼 기다린 후 만료 완료 이벤트를 전송하는 것이다. 이러한 방식을 스케줄된 메시지/이벤트라고 한다. 안타깝게도 NATS Streaming 서버는 이를 지원하지 않는다. 해당 기능이 지원되면 만료 서비스 자체가 필요 없는데 예약 서비스가 15분 후에 예약을 만료해야 한다는 알림을 자체적으로 설정할 수 있기 때문이다.
Bull
프로젝트에 구현한 방법으로 만료 서비스에서 Bull이라는 Redis 기반 패키지를 사용하는 것이다. Bull은 일종의 장기 타이머를 설정하거나 알림을 받을 수 있게 해준다. 즉, Redis에 데이터를 저장하고 처리하며 필요에 따라 스케줄링하는 기능을 제공한다. 구체적으로 만료 서비스에서 Bull을 통해 특정 시간 후 작업을 수행하라는 명령을 전달하고 Bull은 이를 Redis에 저장한다. Redis에는 작업 목록 및 특정 시점 후 수행될 작업들이 저장되며 특정 시간 후 Bull은 Redis로부터 알림을 받고 알림을 전달한다. 이 때, 만료 완료 이벤트를 발행한다.
일반적으로 Bull의 사용 방식은 현재 구현하려는 만료 완료와 다르다. 예를 들어, 사용자가 MP4와 같은 형식의 비디오 파일을 MKV로 변환하고자 요청을 한다고 가정하면 요청을 처리하는데 많은 처리 능력이 필요하다. 이와 같은 변환 작업을 웹 서버에서 처리하는 대신 별도의 작업 서버에서 처리하는 것이 일반적이다. 사용자가 웹 서버에 요청을 보내면 웹 서버는 작업(job)을 대기열에 삽입한다. 작업은 JavaScript 객체로 처리해야 할 내용을 설명한다. 작업 객체는 Redis 서버로 전송되고 Redis 서버는 이를 작업 목록에 저장한다. 여러 작업 서버는 Redis 서버를 지속적으로 확인하며 작업이 나타나면 이를 처리한 후 Redis에 처리 완료 메시지를 전송한다. 이것이 Bull의 일반적인 사용법으로 초기 작업 설정부터 처리, 완료, 알림까지 모든 과정을 처리한다.
Bull의 이러한 역할을 고려하면 사실 서비스 간의 비동기 메시지 처리에 사용하는 것을 적절치 않다. 즉, Bull은 주로 개별 작업을 처리하는 데 사용되며 대규모 메시지 처리를 위해 설계된 것이 아니다. 하지만, Bull에는 큐라는 것이 존재하는데 큐(queue)는 처리할 메시지의 흐름을 나타낸다. 큐 생성 시, 큐를 통해 흐르는 메시지를 어떻게 처리할 지 명시할 수 있다. 별도 작업 서버는 존재하지 않으면 모든 것은 만료 서비스에서 처리된다. 즉, 웹 서버와 작업 서버가 하나의 서버로 통합되어 있다. 만료 서비스가 멈추어도 서버는 계속 작동할 수 있다.
Bull을 사용하여 예약 생성 이벤트가 발생할 때 만료 완료 이벤트를 처리하는 코드는 다음과 같다.
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | /* 예약 생성 이벤트 */ export class BookingMadeSubscriber extends CoreSubscriber<BookingMadeEvent> { readonly subject = Subject.BookingMade; queueGroup = queueGroup; async onMessage( data: BookingMadeEvent['data'], message: Message, ): Promise<void> { try { const delay = new Date(data.expiration).getTime() - new Date().getTime(); console.log(`대기 시간(ms): ${delay}`); /* 새로운 작업(예약 생성 이벤트)을 생성하고 큐에 추가한다. */ await expirationQueue.add( { bookingId: data.id, }, { delay }, ); message.ack(); } catch (error) { console.error(error); } } } type BookingPayload = { bookingId: mongoose.Types.ObjectId; }; /* 예약 만료를 다루는 큐를 생성한다. */ export const expirationQueue = new Queue<BookingPayload>('booking:expiration', { redis: { host: process.env.REDIS_HOST, }, }); /* 정해진 시간이 지나면 만료 완료 이벤트를 발행한다. */ expirationQueue.process(async (job) => { new ExpirationCompletedPublisher(natsInstance.client).publish({ bookingId: job.data.bookingId, }); }); | cs |
update: 2024.10.25