Post

폴링하던 서버가 사라지면, 이벤트는 어떻게 될까

엘리베이터 4대 중 어떤 게 오는지만 표시등에 뜨고, 몇 층에 있는지는 안 보인다. 그래서 그냥 기다린다. 근데 문득 이런 생각이 들었다 — 저 표시등이 꺼지면 어떻게 되지? 내가 눌렀던 버튼 기록은 남아있을까, 아니면 처음부터 다시 눌러야 할까?

업무에서 실제로 이 질문에 부딪힌 적이 있다. “개통이 완료됐다”는 이벤트가, 그걸 지켜보던 프로세스가 사라진 뒤에도 여전히 기록으로 남아있어야 하는 상황이었다.

실시간이라서가 아니라, 기록이 남기 때문에

이벤트 큐가 폴링보다 나은 이유를 “실시간으로 알 수 있어서”라고 생각하기 쉬운데, 실제로 중요한 건 그게 아니었다. 지켜보던 프로세스가 사라져도, 그 기록이 남아있기 때문이었다.

코루틴 기반 폴링의 문제 — 지켜보던 게 사라지면 끝

원래는 코루틴으로 폴링 루프를 돌렸다. 일정 주기로 “개통됐나?”를 확인하는 방식이었다. 문제는 이 폴링이 서버 프로세스 안에서만 존재한다는 것이었다. 서버가 내려가면, 그 순간 진행 중이던 확인 작업 자체가 통째로 사라진다. 어디에도 “이 요청은 아직 확인 중”이라는 기록이 남지 않는다.

즉 이 방식은 지켜보는 사람이 있어야만 그 일이 존재하는 구조였다. 지켜보던 사람이 자리를 비우면(서버 다운), 그 일도 없었던 게 된다.

Redis Consumer로 전환 — 기록은 큐에 남는다

Redis Streams의 컨슈머 그룹 구조로 바꿨다. 메시지(할 일)가 스트림에 영속적으로 기록되고, 컨슈머 그룹이 “누가 어떤 메시지를 아직 처리 안 했는지”를 추적한다.

여기서 핵심은 Pending Entries List(PEL)다. 컨슈머가 XREADGROUP으로 메시지를 읽으면, 그 메시지는 자동으로 PEL에 등록된다. “누군가 이 메시지를 가져갔는데 아직 처리 완료(XACK)는 안 했다”는 기록이다. 처리가 끝나서 XACK을 호출하면 PEL에서 빠진다.

만약 그 컨슈머(서버)가 메시지를 읽고 처리하던 중에 죽어버리면? XACK을 못 했으니 그 메시지는 PEL에 “처리 중”인 채로 남는다. 스트림에서 사라지진 않지만, 아무도 마저 처리해주지 않는 방치 상태가 된다. 이때 다른 컨슈머가 XCLAIM으로 “이 메시지가 일정 시간(min-idle-time) 이상 방치돼 있었으면, 내가 대신 소유권을 가져갈게”라고 선언할 수 있다. 죽은 컨슈머 대신 살아있는 다른 컨슈머, 혹은 재시작된 같은 서버가 이어서 처리하면 된다. 두 컨슈머가 동시에 클레임을 시도해도 하나만 성공하도록 돼 있어서, 같은 메시지를 중복 처리할 위험도 낮다.

XCLAIM을 쓰려면 먼저 XPENDING으로 방치된 메시지를 직접 찾아야 하는데, XAUTOCLAIM은 이 두 단계를 합쳐서 “일정 시간 이상 방치된 것들을 알아서 스캔해서 한 번에 가져와줘”라고 요청하는 편의 명령이다.

정리하면, 서버 A가 메시지를 읽다가 죽어도 메시지는 PEL에 그대로 남아있고, 서버 B(혹은 재시작된 A)가 XAUTOCLAIM으로 주기적으로 방치된 것들을 긁어와 이어받는다. 지켜보던 프로세스가 사라져도, 그 일은 여전히 존재하는 구조로 바뀐 것이다.

카프카는 쓰지 않았다. 이미 Redis 인프라가 있었고, 지금 처리량·연동 규모 기준으로는 카프카까지 가면 운영 복잡도만 늘어나는 오버스펙이었다. 다만 이건 “카프카는 틀렸다”는 결론이 아니라 “지금 규모엔 안 맞다”는 판단에 가깝다. 연동하는 제휴사가 늘고 트래픽이 커지면, 파티셔닝이나 리플레이 같은 카프카의 장점이 필요해지는 시점이 올 수 있다. 제일 유명한 도구가 항상 정답은 아니고, 지금 필요한 만큼만 쓰는 게 맞다는 판단이었다.

실제로는 이렇게 썼다

통신중개 업무의 개통 상태 확인이 바로 이 경우였다. 원래 코루틴 폴링으로 구현돼 있던 걸 위와 같은 구조로 바꾼 실제 사례다.

정리하면

단순 인메모리 폴링으로 충분한지, 기록이 남는(영속적인) 큐 구조가 필요한지는 세 가지로 판단하면 된다.

폴링 작업이 서버 재시작 경계를 넘어갈 만큼 오래 걸리는가. 한 번의 확인이 순식간에 끝난다면(밀리초~초 단위), 서버가 재시작될 확률 자체가 낮아서 인메모리 폴링도 크게 위험하지 않다. 확인이 몇 분~몇 시간씩 걸리는 작업이라면, 그 사이 서버가 재시작될 가능성을 반드시 고려해야 한다 — 이 경우 기록이 남는 큐가 필요하다.

작업이 유실됐을 때 대가가 큰가. 개통 확인처럼 “고객이 신청했는데 아무도 그 결과를 확인 안 해준 채로 묻혀버리는” 상황은 대가가 크다. 반면 단순 캐시 워밍처럼 실패해도 다음 주기에 다시 하면 그만인 작업은 인메모리 폴링으로도 충분하다.

처리량·운영 복잡도가 무거운 도구를 정당화할 만큼 큰가. 지금 수준에서 Redis Streams 정도의 기록이 남는 큐면 충분했다. 카프카까지 필요한 시점은 연동 규모·트래픽이 늘어나서 파티셔닝이나 장기 보관·리플레이가 실제로 필요해질 때다.

결국 이 셋 다 “지켜보던 게 사라져도 괜찮은 일인가”라는 하나의 질문으로 수렴한다. 괜찮다면 인메모리 폴링으로 충분하고, 안 괜찮다면 그 기록을 프로세스 바깥에 남겨야 한다.

참고


이 글은 업무든 일상이든, 거기서 얻은 통찰을 컴퓨터 과학 개념에 빗대어 풀어보는 '경험 디버깅(exp-debug)' 연재입니다. 실제 경험에서 얻은 인사이트를 바탕으로, AI와 함께 정리하고 다듬었습니다.

This post is licensed under CC BY 4.0 by the author.