Computer >> 컴퓨터 >  >> 프로그래밍 >> Redis

Upstash, Redis, QStash로 구축하는 실시간 비상 대응 시스템

이번 글에서는 Upstash를 활용해 국가의 대피소 지도 정보를 Redis에 안전하게 저장하고 접근하는 방법과, QStash를 통해 데이터베이스를 실시간으로 업데이트하는 방법을 알아보겠습니다.

소개

오늘날 전 세계적으로 자연재해와 군사적 위협이 갈수록 빈번해지면서, 사회 서비스 분야에서도 디지털화의 필요성이 그 어느 때보다 커지고 있습니다.

AMBER 경보 같은 긴급 방송 시스템부터 COVID-19 추적 앱, SOS 시스템까지, 국가에 영향을 미치는 다양한 위기 상황에 대응하기 위해 폭넓은 기술이 활용되어 왔습니다.

이제 Redis와 서버리스 워크로드가 실시간 비상 대응 시스템 구축에 어떤 핵심 역할을 할 수 있는지 살펴보겠습니다. 이 시스템은 시민들이 가장 가까운 대피소를 찾도록 도와줄 뿐만 아니라, 수용 인원, 편의 시설(장애인 시설 등), 사용 가능한 자원(물, 전기, 의약품 등)에 대한 정보도 함께 제공합니다.

Upstash, Redis, QStash로 구축하는 실시간 비상 대응 시스템
이미지 출처: Project Cloud4

왜 서버리스인가?

서버리스 개념은 최근 몇 년간 개발자 사이에서 큰 인기를 얻었습니다. 이 아키텍처를 채택하는 스타트업이 늘어나고 있으며, 대기업에서도 도입 사례가 꾸준히 증가하고 있습니다.

특히 국가 기관의 입장에서는, 신속한 개발 환경과 자동 관리형 서비스가 비상 대응 시스템 구축의 복잡성과 운영 부담을 획기적으로 줄여줄 수 있습니다. 여기에 두 가지 요소를 더 고려해야 합니다.

  1. 예산: 만약 국가가 실제 재난 상황을 겪지 않는다면, 의사 결정권자가 사용되지 않는 컴퓨팅 파워에 대한 비용 지출을 승인할 수 있을까요? 사실상 불가능합니다.
  2. 트래픽 급증: 경보가 발령되고 수백만 명이 동시에 웹 플랫폼에 접속해 가장 가까운 대피소를 찾는 상황을 상상해 보세요. REST API가 이러한 갑작스러운 RPS(초당 요청 수) 증가를 감당할 수 있을까요? 필수적인 실시간 컴포넌트는 어떨까요?

이 모든 질문에 대한 답은 Upstash와 Ably 같은 진정한 서버리스 플랫폼에서 찾을 수 있습니다. 이들은 자동 확장되며, 실제 사용한 리소스에 대해서만 과금됩니다.

왜 Redis인가?

이 프로젝트의 데이터 요구 사항에는 최대한 빠른 속도로 수행해야 하는 다양한 알고리즘 연산이 포함됩니다. 대피소 정보를 나타내는 기본 데이터 구조부터 특정 위치의 대피소를 다양한 기준으로 정렬하는 간단한 방법까지, Redis는 NoSQL 데이터베이스의 유연성을 유지하면서도 개발을 단순화하고 가속화하는 다양한 기능을 제공합니다.

왜 QStash인가?

전통적인 메시지 큐는 이벤트 기반 시스템을 설계하는 데 훌륭한 방법을 제공하지만, 한 가지 함정이 있습니다. 대부분의 메시지 큐는 AMQP 같은 상태 유지(stateful) 프로토콜에 완전히 의존하기 때문에, 서버리스 함수처럼 짧게 실행되는 환경에서는 사용할 수 없습니다.

QStash는 HTTP, 즉 이러한 환경에서 일반적으로 사용되는 무상태(stateless) 프로토콜을 통해 요청을 보낼 수 있게 함으로써 이 문제를 해결합니다. 여기에 웹훅을 결합하면 데이터베이스에서 데이터를 가져오고 업데이트하는 실시간 파이프라인을 구축할 수 있습니다.

준비물

참고: 샘플 코드만 훑어보고 싶다면 준비물과 설정 섹션은 건너뛰어도 됩니다. 해당 섹션은 우리가 무엇을 어떻게 사용하는지 보여주기 위한 것입니다.

직접 이런 시스템을 구축하고 싶다면 다음이 필요합니다.

  • Redis 데이터베이스를 생성하고 QStash 엔드포인트 또는 토픽을 사용할 Upstash 계정
  • 웹훅 연동이 설정된 Ably 계정
  • Next.js 프로젝트
  • 로컬 요청을 인터넷에서 접근 가능한 URL로 프록시하려면 ngrok 같은 터널링 서비스
  • 필요한 라이브러리를 지원하는 패키지 매니저. 이 글에서는 npm을 사용하지만 반드시 npm일 필요는 없습니다.

주의: 이 글에서 제공하는 코드는 해당 기술들을 애플리케이션 아키텍처 맥락에서 어떻게 사용하는지 보여주는 기본적인 개념 증명(PoC) 샘플입니다. 제공된 코드는 프로덕션 환경에 바로 사용할 수 없으며, 가독성을 위해 단순화하고 축약했습니다. 실제 배포를 원한다면 예외 처리, 보안 등 필요한 수정을 반드시 수행하세요.

설정

서버리스 함수

Next.js의 엣지 API 라우트를 사용하여 사용자의 위치에 더 가까운 곳에서 서버리스 워크로드를 실행합니다.

Next.js 프로젝트를 생성하려면 상위 디렉터리에서 npx create-next-app@latest <your-app-name> 명령어를 실행하세요. 이후 npm run dev로 개발 서버를 시작하고, npm run build로 프로덕션 번들을 생성하며, npm run start로 프로덕션 서버를 시작할 수 있습니다.

Next.js는 API 라우트에 기본적으로 node 환경을 사용합니다. 이를 변경하는 방법은 두 가지가 있습니다.

  1. 라우트별 방식. 원하는 라우트에 다음 export를 추가합니다:
    export const config = {
     runtime: "experimental-edge",
    };
  2. 전역 방식. edge를 기본 런타임으로 만들려면 next.config.js 파일에 다음을 추가합니다:
    const nextConfig = {
     // ...
     experimental: {
     runtime: "experimental-edge",
     },
     // ...
    };

Upstash

Upstash 프로바이더 생성 방법은 공식 가이드를 따르세요.

QStash 수신(receiver) 라우트 설정 방법 역시 공식 가이드를 참고하세요. Ably는 웹훅 메시지 인코딩으로 JSON과 MessagePack 두 가지 옵션을 제공합니다. 전자(JSON)를 사용하려면 잠재적인 암호화 호환성 문제를 피하기 위해 JWT 대신 인증 헤더를 사용해야 할 수 있습니다. 간단함을 위해 여기서는 JSON을 사용하겠습니다.

Ably

실시간 기능에는 Ably의 프로토콜을 사용합니다. Ably는 가능한 경우 WebSocket을 기본으로 사용합니다. 시작하려면 공식 가이드를 따르고, Next.js를 사용 중이라면 @ably-labs/react-hooks npm 패키지를 확인해 보세요.

또한 웹훅 연동이 QStash URL을 가리키도록 설정하고, 필요한 헤더를 추가해야 합니다.

프로덕션 환경으로 배포할 계획이라면 인증 및 보안 설정도 반드시 확인하세요.

선택 사항: 로컬 터널링

ngrok 시작 방법은 공식 가이드를 따르세요.

아키텍처

흐름

애플리케이션의 흐름을 더 잘 이해하기 위해 다음 다이어그램(excalidraw.com으로 작성)을 살펴보겠습니다.

Upstash, Redis, QStash로 구축하는 실시간 비상 대응 시스템

보시다시피 두 가지 유형의 사용자를 정의했습니다.

  1. 시민(citizen): 대피소 또는 특정 위치의 정보에 접근할 수 있고, 대피소로 이동할 의사를 알릴 수 있습니다.
  2. 관리자(admin): 대피소의 물류 데이터를 수정하고, 가용 현황을 변경할 수 있습니다(예: 특정 인원/가족이 해당 위치에 도착했음을 확인).

프론트엔드와 백엔드를 최대한 분리(decoupling)하면서 데이터를 실시간으로 제공하기 위해, 클라이언트와 통신하는 Ably 채널을 사용하고 가용 현황이 변경될 때마다 웹훅을 트리거합니다.

또한 상태 유지 연결은 사용하지 않아야 하므로, 백엔드에서 메시지를 발행할 때는 Ably의 REST API를 사용합니다.

데이터

현재 저장해야 할 주요 데이터 유형은 두 가지입니다.

  • 대피소(shelter) 정보 — Redis 해시(hash) 형태로 저장하며 다음 속성을 가집니다:

    • 키 이름: country-city-number 형식 (예: RO-CJ-01)
    • 가용 인원 (예: 300)
    • 편의 시설 (예: ["disabled persons special acces", "counseling"])
    • 자원:
      {
       resource: quantity,
       또는
       resource: list
       ...
      }
      예시:
      {
       "water": "200 liters",
       "medicine": ["insuline", ...]
      }
    • 전기: yes/no
    • 난방: yes/no
    • 위치: [longitude, latitude] 또는
      {
       longitude: number,
       latitude: number
      }
    • _id: 무작위로 생성합니다.
  • 위치(location) 정보

    중첩 쿼리(nested query)로 특정 위치의 가용 대피소를 찾을 수도 있지만, 쿼리를 단순화하고 최적화하려면 별도의 데이터 구조로 저장하는 것이 더 좋습니다. 이를 위해 Redis 셋(set)을 사용하고 요소 이름을 <geographical-unit>-<name>(예: country-RO 또는 city-CJ) 형식으로 지정하면 중복도 방지할 수 있습니다.

    예시:

     city-CJ : ["RO-CJ-01", "RO-CJ-02", "RO-CJ-03"]

하지만 여전히 하나의 문제가 남습니다. 시민의 대피소 이동 의사와, 관리자의 확인으로 트리거된 실제 점유율 업데이트를 어떻게 구분할 수 있을까요? 두 가지 방법이 있습니다.

  1. 대피소의 가용 인원 키를 지속적으로 업데이트하는 방식:

    사용자가 이동 의사를 발표하면(혼자 또는 가족 등과 함께) x만큼 증가시킵니다.
    관리자가 해당 확인이 유효하지 않다고 판단하면 x만큼 감소시킵니다(또는 -x만큼 증가).

    문제는 관리자가 신뢰할 수 있는 원천(source of truth) 역할을 하며 시민의 위치를 계속 추적해야 하고, 대피소 데이터를 즉시 신뢰할 수 없다는 점입니다.

  2. shelter-"availability" 형식의 별도 문자열 키를 생성하는 방식 (예: RO-CJ-01-availability). 실시간 업데이트되는 값을 여기에 저장합니다.

    이렇게 하면 실시간 업데이트가 가능하면서도, 해시의 가용 인원 키는 신뢰할 수 있는 원천으로 유지할 수 있습니다.

    클라이언트 측에서는 해시와 문자열 키를 모두 조회하여, 하나는 실제 가용 인원으로, 다른 하나는 예상 가용 인원으로 표시합니다. 대안으로 문자열 키만 조회하고 필요할 때 신뢰 원천 값으로 되돌리는 방법도 있습니다. 이 경우 네트워크 부하는 줄어들지만, 관리자가 첫 번째 시나리오처럼 데이터를 지속적으로 검증해야 하고, 네트워크 호출을 실질적으로 줄이려면 대피소의 가용 인원 키를 별도로 저장해야 한다는 점을 기억하세요(다른 정보들도 여전히 가져와야 하니까요!).

정렬

Redis의 또 다른 데이터 유형인 정렬 셋(sorted set)을 활용해 보겠습니다. 특정 위치의 대피소를 가용 인원, 편의 시설 또는 사용 가능한 자원 기준으로 정렬하고 싶다고 가정해 봅시다. 앞서 언급했듯이 중첩 쿼리로 처리할 수도 있지만, 시간이 오래 걸리고 데이터 전송량도 훨씬 커집니다. 더 나은 해결책은 필터링 기준마다 정렬 셋을 만드는 것입니다(<geographical-unit>-<name>-<criteria> 형식으로 명명, 예: city-CJ-availability 또는 country-RO-food).

예시: city-B-availability

ScoreContent
200RO-B-02
100RO-B-01

이 예시에서 가용 인원 점수는 총 좌석 수 - 현재 점유 인원으로 계산되었습니다.

예제

API와 클라이언트의 동작을 보여주는 기본 코드를 작성해 보겠습니다.

엔드포인트

기본적으로 다음 두 가지가 필요합니다.

  • 클라이언트가 요청한 정보를 제공하는 데이터 엔드포인트
  • 데이터 유형을 생성하거나 업데이트하는 엔드포인트

클라이언트

사용자의 위치를 추적한 후, "availability" 채널에 자동으로 연결하고 시민이 대피소로 이동할 의사가 있으면 메시지를 발행합니다.

먼저 /_app.js 파일에서 연결을 설정합니다:

import { useEffect, useState } from "react";
 
import "../styles/globals.css";
 
import { configureAbly } from "@ably-labs/react-hooks";
 
export default function App({ Component, pageProps }) {
 const [loaded, setLoaded] = useState(false);
 useEffect(() => {
 configureAbly({
 // 프로덕션 시스템에서는 반드시 인증을 사용하세요.
 key: process.env.NEXT_PUBLIC_ABLY_API_KEY,
 });
 setLoaded(true);
 }, []);
 
 if (!loaded) return <div>loading...</div>;
 return <Component {...pageProps} />;
}

그런 다음 필요한 컴포넌트에서 채널에 연결합니다:

import { useChannel } from "@ably-labs/react-hooks";
 
export default function Test() {
 const [availability] = useChannel("availability", (msg) => {
 console.log(msg);
 });
 
 return <></>;
}

데이터베이스 작업

새 데이터 추가

Redis에 새 데이터를 추가하려면 HSET, SET, SADD 명령어를 사용할 수 있습니다.

export default async (req) => {
 let data = await req.json();
 
 if (data.type === "shelter") {
 // 더 이상 필요 없는 'type' 키를 제거합니다.
 delete data.type;
 const { name: shelter, availability } = data;
 
 let shelterData = data;
 // 무작위 ID를 생성합니다.
 shelterData._id = Math.random()
 .toString(36)
 .replace(/[^a-z]+/g, "")
 .substring(0, 7);
 
 // 대피소 정보를 해시로 저장합니다.
 await redis.hset(shelter, shelterData);
 
 // 실시간 업데이트되는 가용 인원을 문자열로 저장합니다.
 await redis.set(shelter + "-availability", availability);
 return new Response("ok");
 } else if (data.type === "location") {
 const { name: location, shelters } = data;
 
 await redis.sadd(location, ...shelters);
 
 return new Response("ok");
 }
};

기존 데이터 업데이트

기본적인 UD(update-delete) 연산 외에도, Redis의 INCRBY와 HINCRBY 명령어를 사용해 대피소의 가용 인원을 수정할 수 있습니다.

시민이 대피소로 이동할 의사를 발표하면, 클라이언트 앱에서 메시지를 발행할 수 있습니다.

availability.publish("update", {
 shelter: "RO-CJ-01",
 availability: 1, // 사용자가 취소하면 -x.
});

아키텍처에서 설명했듯이, 이는 웹훅을 트리거하며 해당 웹훅에서 데이터를 받아 shelter-"availability" 키를 업데이트할 수 있습니다.

await redis.incrby(shelter + "-availability", value);

서버 측에서 업데이트하는 경우(예: 관리자 확인)에는 REST API를 사용하는 것을 잊지 마세요.

데이터 조회

서버에서는 GET, SMEMBERS, HGETALL 명령어를 사용할 수 있습니다.

클라이언트에서는 채널의 메시지를 수신(listen)하고 그에 따라 상태를 업데이트합니다.

마치며

오늘은 강력한 서버리스 아키텍처를 활용해 실제 세계의 비상 대응 추적 앱을 구축하는 방법을 살펴보았습니다. 이 목표를 달성하는 방법은 여러 가지가 있지만, 개발자 경험이 단순화된다는 점에서 저는 개인적으로 이런 흐름을 권장합니다.

분산 컴퓨팅 시스템 구축에 익숙한 시니어 개발자일 수도 있고, 세상을 좋은 방향으로 바꿀 수 있는 아이디어를 가진 초보 개발자일 수도 있습니다. 어떤 경우든 올바른 도구를 찾는 것은 프로세스에서 항상 필수적인 부분입니다.

이 글에 대한 생각을 들려주세요. 궁금한 점이 있다면 LinkedIn으로 메시지를 보내거나 Github에서 제 다른 코드를 확인해 주세요.