본문 바로가기

ETC/프로젝트

미션 시스템 구축기 : 정합성을 위한 설계 전환 (feat. Redis + Celery)

안녕하세요 🙂

이번 글에서는 서비스 내 미션 기능을 구현하면서 겪었던 고민들과, 결국 Redis와 Celery를 이용해 비동기 처리를 도입하게 된 과정에 대해 이야기해보려 합니다.

문제의 시작: 이벤트마다 수십 개의 쿼리?

사용자가 글을 작성하거나, 좋아요를 누르거나, 특정 시간에 접속하는 등 어떤 행동을 했을 때, 그 행동이 미션 조건을 만족하는지 판단해주는 로직이 필요했습니다.

그런데 문제는...
미션 조건마다 DB 쿼리를 한 번씩 보내야 한다는 점이었죠.

처음엔 “조건 몇 개만 확인하면 되겠지” 싶었지만, 실제로 구현에 들어가 보니 상황은 달랐습니다.

미션 조건마다 별도의 DB 쿼리가 필요했고, 글 작성이나 좋아요 같은 이벤트 하나당 적게는 5~10개의 조건을 검사해야 했습니다.
결국 단일 요청으로 10건에서 많게는 50건 이상의 DB 쿼리가 실행되는 구조였고, 트래픽이 많아질수록 서버에 큰 부하를 주는 병목 구간이 될 수 있다는 걸 알게 되었습니다.



그래서 다양한 방식을 고민해봤습니다.

첫 번째로 떠올린 건 WebSocket을 이용한 실시간 처리였습니다.

사용자가 앱을 사용하는 동안 미션 상태를 지속적으로 추적하고, 조건을 만족할 때 즉시 알림을 주는 방식이죠. 이 방식은 기술적으로 가장 직관적이며 사용자 경험(UX) 측면에서도 이상적이었지만, 현실적인 벽도 있었습니다.

WebSocket을 유지하려면 서버와의 연결 상태를 계속 관리해야 했고, 클라이언트와의 동기화도 섬세하게 설계해야 했습니다. 무엇보다, 미션 기능의 비즈니스 중요도에 비해 너무 무겁고 과한 아키텍처였습니다. 도입과 운영의 복잡도 대비 효율이 떨어졌죠.


두 번째는 Polling 방식, 즉 일정 주기마다 미션 상태를 확인하는 접근입니다.

프론트엔드에서 30초나 1분 단위로 서버에 미션 상태를 요청하게 하고, 서버는 그때마다 현재 조건을 평가해서 응답을 돌려주는 식입니다. 이 방법은 상대적으로 간단하고 구현이 쉬웠지만, 여전히 각 요청마다 수많은 조건을 검사해야 했기에 DB에 주는 부하를 줄이지 못한다는 한계가 있었습니다. 오히려 요청 빈도를 조절하려다 사용자 피드백 속도까지 느려지는 결과를 낳을 수도 있었습니다.


그렇게 여러 접근을 검토하고 실제로 테스트까지 해본 끝에, 저는 Celery + Redis 기반의 비동기 백그라운드 처리 방식을 선택하게 되었습니다.


이 구조는 사용자 행동이 발생했을 때 즉시 DB를 건드리지 않고, Redis를 통해 Celery 워커로 이벤트를 전달합니다. 서버는 빠르게 응답을 마치고, 그 뒤에 워커가 따로 미션 조건을 평가하고 결과를 저장하는 방식이죠.


덕분에 사용자 경험은 부드럽게 유지하면서도, 서버의 순간 부하를 상당히 줄일 수 있었습니다.

무엇보다도 미션 기능 자체가 “실시간성”보다 “정합성”과 “지속적 관리”에 더 초점이 있었기 때문에, 백그라운드 작업으로 넘기는 것이 기능의 성격과도 잘 맞아떨어졌습니다.

따라서 사용자가 글을 작성하거나 좋아요를 누르면, 그 행위 자체는 빠르게 처리한 뒤, 그에 대한 미션 평가 요청은 Redis를 통해 Celery로 비동기 전송되도록 했습니다. 이렇게 하면 미션 로직은 백그라운드에서 조용히 수행되고, 사용자는 응답 지연을 전혀 느끼지 않게 됩니다.



이런 구조가 실제로 어떻게 동작하는지, 하나의 사용자 행동을 예시로 살펴보겠습니다.

시스템 구조 다이어그램



예를 들어, 한 사용자가 글을 쓰는 순간 다음과 같은 흐름이 작동합니다:

  1. 클라이언트는 서버로 글 작성 요청(POST)을 보냅니다.
  2. 서버는 요청을 받아 글을 DB에 저장한 후, 내부적으로 미션 평가용 API를 호출합니다.
    이 API는 단일 mission_code가 아니라 여러 조건을 통합적으로 검사하는 역할을 합니다.
  3. 해당 API는 Celery 태스크를 트리거하여, evaluate_mission_condition(user_id, mission_code) 형식의 함수를 워커가 실행하도록 넘깁니다.
  4. Celery 워커는 해당 사용자의 미션 조건을 차례대로 평가합니다.
    (예: 첫 글인지, 280자 이상인지, 7일 연속 작성인지 등…)
  5. 조건을 만족하면, 미션 달성 상태를 DB에 반영하고 필요 시 사용자에게 알림도 전송합니다.

이 구조의 장점은 명확했습니다.

  • 사용자 요청에 대한 응답 속도는 빨라지고,
  • 서버 부하는 분산되며,
  • 미션 조건 평가의 확장성도 높아졌습니다.

무엇보다도, 미션 종류가 늘어나더라도 코드 구조가 복잡해지지 않았습니다.

각 미션마다 평가 함수만 분리해두면, Celery가 이를 병렬로 처리해주는 구조이기 때문에, 새로운 미션이 추가될 때도 기존 로직을 해치지 않고 확장할 수 있다는 점이 큰 이점이었습니다.


그리고 생각보다 중요했던 것들

이 시스템을 만들며 느낀 건, 단순한 기능도 결국은 설계 철학이 중요하다는 점이었습니다.
기능을 빨리 만들 수 있다고 해서 ‘빠르게 처리하자’는 생각만으로 움직이면, 금세 병목이나 유지보수의 한계에 부딪히게 됩니다.

이번에는 특히 다음과 같은 포인트들이 중요했습니다:

  • 서버 부하를 최소화할 수 있는 구조로 설계할 것
  • 미래의 기능 확장 가능성을 염두에 둘 것
  • 사용자의 체감 속도를 해치지 않으면서도 내부 로직은 충분히 유연하게 만들 것

초기에는 단순히 조건 몇 개 비교하면 되겠지라고 생각했던 로직이었지만,
결과적으로는 Redis, Celery, 비동기 처리, DB 구조 설계 등 꽤 다양한 기술 요소를 엮어야만 했고,
덕분에 “빠르게 끝낼 수 있는 일도 결국은 설계가 모든 걸 결정한다”는 걸 다시 한번 배웠습니다.


지금도 이 미션 시스템은 매일 수천 명의 사용자 행동을 기반으로 조용히 작동하고 있습니다.

사용자는 의식하지 못한 채로, 서버는 과도한 부하 없이, 개발자는 미션을 손쉽게 추가/변경할 수 있는 구조로 만들어졌다는 점에서 개인적으로 가장 만족스러운 구현 중 하나였습니다.

 

읽어주셔서 감사합니다 :)