정말 크거나 무거운 단일 프로세스를 처리하려다 곤란해진 적이 있으신가요? 그렇다면 이 글이 도움이 될 수 있습니다.
이번 글에서는 하나의 프로세스로는 감당하기 어려울 만큼 거대한 메시지를 어떻게 관리하고 있는지 소개하려 합니다. 메시지를 여러 청크(chunk)로 나누고, 이를 별도의 프로세스들로 처리하는 방식입니다.
기술적인 세부 사항보다는 아키텍처 관점에서 접근해 보겠습니다.
캐싱과 pubsub(발행-구독)에 대한 이야기도 조금 다루지만, 구현 방법 자체에 대한 설명은 생략하고 패턴 자체에 집중할 예정입니다.
문제 상황
아마 가장 먼저 떠오르는 질문은 "왜 하나의 프로세스를 여러 동시성 프로세스로 나눠야 하는가?"일 것입니다.
여러 가지 이유가 있을 수 있지만, 제 경우에는 순전히 메시지가 너무 컸기 때문입니다.
상황을 좀 더 쉽게 이해하도록 간단한 다이어그램으로 설명드리겠습니다. 서비스 A와 서비스 B, 두 개의 독립된 서비스가 있다고 상상해 보세요. 그리고 그 사이에는 pubsub 서비스가 위치합니다.
pubsub 서비스가 익숙하지 않다면, 한 서비스의 메시지가 다른 서비스에 전달될 수 있도록 도와주는 중개자(broker)라고 생각하시면 됩니다.
서비스 A가 메시지를 발행(publish)하면, pubsub을 통해 서비스 B가 이를 처리합니다. 처리가 끝나면 해당 메시지가 완료되었음을 표시하는 또 다른 작업을 수행하죠.
간단하죠?
그런데 문제는, 메시지가 너무 클 경우 pubsub 서비스의 제약 때문에 메시지 발행 자체가 실패할 수 있다는 점입니다.
여기까지가 제가 겪었던 문제의 개요입니다. 그럼 이 문제를 어떻게 해결했는지 다음 섹션에서 풀어보겠습니다.
첫 번째 시도
처음 떠올린 방법은 pubsub 서비스가 처리할 수 있는 크기 제한을 늘리는 것이었습니다. 설정 변경 한 번으로 가능한 작업이죠.
하지만 세상 일이 그렇게 쉬울 리 없겠죠? 메시지가 계속해서 더 커진다면 어떻게 될까요? pubsub 크기를 계속 늘려야 할까요?
결론적으로 이런 접근은 수많은 확장성(scalability) 문제를 야기할 수 있습니다. 장기적인 해결책으로는 적합하지 않습니다.
그래서 다른 해결책을 고민했습니다. 바로 메시지를 여러 개로 분할하고, 각 부분을 개별적으로 처리하는 방식입니다.
이제 시스템은 다음과 같이 동작합니다. 메시지가 여러 개의 작은 메시지로 나뉘는 것이죠. 어떻게 나눌지, 메시지의 어느 부분을 기준으로 나눌지는 케이스와 흐름에 따라 달라질 수 있습니다.
제 경우에는 메시지 안에 아이템 목록(list of items)이 담겨 있었기 때문에, 아이템 하나하나를 기준으로 나눌 수 있었습니다.
예를 들어 아이템이 10개 있다고 가정해 보죠. 기존에는 10개 아이템 전부를 하나의 메시지로 발행했습니다. 하지만 이제 메시지를 분할하면 하나의 메시지가 10개의 메시지로 변환됩니다.
그 결과, 단일 프로세스가 여러 개의 프로세스로 나뉩니다. 한 번의 발행(publish)이 10번의 발행이 되고, 이는 곧 하나의 프로세스가 10개의 프로세스가 된다는 의미입니다.
언뜻 보면 이상적이지 않아 보일 수 있지만, 당시 저가 찾아낸 최선의 해결책이었고 실제로 잘 동작했습니다.
그렇다면 단순히 나누기만 하면 끝일까요?
아닙니다. 앞서 언급했던 마지막 단계, 즉 '프로세스 완료 표시' 부분을 기억하시나요?
새로운 다이어그램에서 이 부분이 빠져 있는 이유가 궁금하실 수 있습니다.
걱정하지 마세요. 깜빡한 것이 아니라, 다음 파트를 위해 일부러 남겨둔 것입니다.
핵심은 이것입니다. 메시지를 분할해 여러 프로세스로 쪼개면, 시스템 입장에서는 전체 프로세스가 실제로 끝났는지 알기 어렵습니다. 이것이 우리가 해결해야 할 또 하나의 주요 과제인데, 다행히 이 문제 역시 해결 방법을 찾았습니다.
프로세스 완료 처리 방법
그렇다면 여러 프로세스가 동시에 진행되는 상황에서, 전체 프로세스가 끝났는지는 어떻게 알 수 있을까요?
제가 생각해낸 해결책은 다음과 같습니다. 먼저 처리해야 할 프로세스의 총 개수를 저장해두고, 하나의 프로세스가 끝날 때마다 카운트를 1씩 감소시키는 방식입니다. 이렇게 하면 마지막 프로세스가 종료되었는지 알 수 있습니다.
꽤 단순하게 들리죠? 물론 이 데이터를 저장할 신뢰할 만한 공간만 있다면 말입니다.
실제로 선택할 수 있는 옵션이 많은데, 그중 하나가 바로 Redis입니다. 저도 이 문제를 해결하기 위해 Redis를 사용했습니다.
Redis에 대해 익숙하지 않으신 분들을 위해 간단히 설명드리면, Redis는 일반적으로 캐시(cache) 용도로 사용되는 서비스입니다.
Redis 활용 흐름은 다음과 같이 구성했습니다. 프로세스 자체는 기존과 완전히 동일하지만, 중간에 Redis가 추가된 형태입니다. 이때 유효한 초기 카운트 값을 설정하는 것이 중요합니다.
제 경우에는 목록을 발행하기 때문에, 목록의 길이를 초기 카운터 값으로 손쉽게 지정할 수 있었습니다. 그리고 각 프로세스가 완료될 때마다 카운터를 1씩 감소시킵니다. 이후 Redis 카운터 값만 확인하면 모든 프로세스가 완료되었는지 알 수 있습니다. 카운터가 0에 도달했다면, 모든 프로세스가 안전하게 완료된 것으로 표시해도 되는 것이죠.
마치며
정리하자면, 저는 하나의 거대한 메시지를 여러 개의 메시지로 분할하고, 이를 여러 프로세스에서 동시에 처리하도록 했습니다. 그리고 메시지 처리 진행 상황을 관리하기 위해 Redis 캐싱을 활용했습니다.
위에서 설명한 솔루션이 거대한 메시지 처리 문제를 만났을 때의 만능 열쇠(silver bullet)는 아닙니다. 메시지를 스트리밍(streaming)하는 등의 다른 접근 방식도 있으니, 상황에 맞게 선택하시길 권합니다.