이 글에서는 Next.js와 Prisma로 만든 SaaS 애플리케이션에 Upstash를 활용해 할당량(quota) 시스템을 구축하는 방법을 소개합니다. Next.js API 라우트를 사용해 간단한 API를 만들어 보겠습니다.
많은 SaaS 애플리케이션에서 할당량 시스템을 한 번쯤 접해보셨을 것입니다. 할당량 시스템은 사용자가 주어진 기간 동안 수행할 수 있는 작업 횟수를 제한하는 장치입니다.
이번 예시에서는 "Free" 플랜 사용자가 월 1,000회의 API 요청까지만 보낼 수 있다고 가정합니다. 사용자가 1,000회를 초과하는 요청을 시도하면 애플리케이션은 추가 요청을 차단합니다.
이러한 API 요청은 스프레드시트의 내용을 가져와 JSON으로 변환하는 데 사용됩니다. 이것이 바로 fastsheet가 하는 일입니다. 어떤 Google Sheets 스프레드시트든 JSON API로 변환해 주죠.

데이터베이스 스키마 정의하기
앞서 말씀드린 대로 Prisma를 ORM으로 사용해 데이터베이스와 상호작용하겠습니다. 다음은 할당량 시스템을 구현한 User 모델과 스프레드시트를 저장하는 Spreadsheet 모델의 예시입니다.
model User {
id Int @id @default(autoincrement())
planId String @default("FREE")
email String @unique
quota Int @default(0)
spreadsheets Spreadsheet[]
}
model Spreadsheet {
id Int @id @default(autoincrement())
userId Int
user User @relation(fields: [userId], references: [id])
content String
}
planId필드는 사용자가 구독 중인 플랜을 나타냅니다.quota필드는 현재 달에 해당 사용자가 수행한 API 요청 횟수입니다.Spreadsheet모델은 하나의User와 연결된 스프레드시트를 의미합니다.- 하나의
User는 여러 개의Spreadsheet를 가질 수 있습니다.
설명을 단순하게 유지하기 위해 월 1,000회의 API 요청을 허용하는 "FREE" 플랜만 다루겠습니다.
사용자 할당량 증가시키기
사용자가 API 요청을 보낼 때마다 해당 사용자의 할당량을 1씩 증가시키고 싶습니다.
언뜻 보면 아주 간단해 보입니다. Next.js API 라우트에서 API 요청이 발생할 때마다 할당량을 1씩 늘려주면 되니까요.
// pages/api/spreadsheets/[id]/index.ts
import type { NextApiRequest, NextApiResponse } from "next";
const MAX_FREE_TIER_QUOTA = 1000;
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
// URL에서 스프레드시트 ID를 가져옵니다.
const { id } = req.params;
// 데이터베이스에서 스프레드시트와 연관된 사용자를 조회합니다.
const spreadsheet = await prisma.spreadsheet.findUnique({
where: { id: Number(id) },
include: { user: true },
});
// 스프레드시트가 존재하는지 확인합니다.
if (!spreadsheet) {
return res.status(404).json({ error: "Spreadsheet not found" });
}
// 사용자가 할당량을 초과하지 않았는지 확인합니다.
if (spreadsheet.user.quota >= MAX_FREE_TIER_QUOTA) {
return res.status(429).json({ error: "Quota exceeded" });
}
// 사용자의 할당량을 1 증가시킵니다.
await prisma.user.update({
where: { id: spreadsheet.user.id },
data: { quota: { increment: 1 } },
});
// 스프레드시트 내용을 반환합니다.
return res.json({ content: spreadsheet.content });
}
그런데 이 방식에는 문제가 있습니다. 기대만큼 빠르지 않다는 점입니다.
실제로 이 코드는 API 요청이 발생할 때마다 할당량을 증가시키기 위한 데이터베이스 트랜잭션을 생성합니다.
즉, 데이터베이스에 접속해 사용자의 할당량을 조회하고, 할당량을 1 증가시키는 트랜잭션을 생성한 뒤, 그제야 스프레드시트 내용을 반환합니다.
이 방식이 비효율적인 이유는 여럿입니다.
- 데이터베이스 자체가 느리면 응답 시간 역시 느려집니다.
- 데이터베이스가 서버와 물리적으로 멀리 떨어져 있어도 응답 시간이 지연됩니다.
- 이 코드를 Edge 호환으로 만들기 어렵습니다. 전 세계 곳곳에 데이터베이스 복제본을 배치해야 하는데, 결코 쉬운 작업이 아닙니다!
Upstash로 사용자 할당량 관리하기
이 문제를 해결하기 위해 Upstash Redis®를 사용해 사용자의 할당량을 관리하겠습니다. Upstash Redis®는 서버리스 환경을 위해 설계된 클라우드 기반의 빠르고 안정적인 Redis® 데이터베이스로, Edge(사용자와 가까운 곳에서 코드 실행)를 기본적으로 지원합니다.
Redis®의 INCR 명령어를 사용하면 사용자의 할당량을 1씩 증가시킬 수 있습니다. 이 명령어는 원자적(atomic)으로 동작하기 때문에 단 한 번만 실행되며, 그마저도 매우 빠릅니다.
다음은 동일한 로직을 Upstash Redis®로 할당량을 관리하도록 수정한 코드입니다.
// pages/api/spreadsheets/[id]/index.ts
import type { NextApiRequest, NextApiResponse } from "next";
import { Redis } from "@upstash/redis";
const MAX_FREE_TIER_QUOTA = 1000;
// 환경 변수를 사용해 Upstash Redis 인스턴스를 생성합니다.
// .env 파일에 해당 값들이 정의되어 있는지 확인하세요.
const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
// URL에서 스프레드시트 ID를 가져옵니다.
const { id } = req.params;
// 데이터베이스에서 스프레드시트와 연관된 사용자를 조회합니다.
const spreadsheet = await prisma.spreadsheet.findUnique({
where: { id: Number(id) },
include: { user: true },
});
// 스프레드시트가 존재하는지 확인합니다.
if (!spreadsheet) {
return res.status(404).json({ error: "Spreadsheet not found" });
}
// Redis 할당량 키. 사용자 ID별로 고유합니다.
const quotaKey = `user:${spreadsheet.user.id}:quota`;
// Redis에서 사용자의 할당량을 조회하면서 1 증가시킵니다.
const quota = await redis.incr(quotaKey);
// 키가 이전에 존재하지 않았다면 반환값은 1이며,
// 이때 만료 시간을 1일로 설정합니다.
if (quota === 1) {
await redis.expire(quotaKey, 60 * 60 * 24);
}
// 사용자가 할당량을 초과하지 않았는지 확인합니다.
if (quota > MAX_FREE_TIER_QUOTA) {
return res.status(429).json({ error: "Quota exceeded" });
}
// 스프레드시트 내용을 반환합니다.
return res.json({ content: spreadsheet.content });
}
Upstash Redis®를 활용하면 코드 확장이 매우 쉬워집니다. Upstash Redis® Edge의 강력한 성능을 활용해 사용자와 가까운 위치에서 코드를 실행할 수 있기 때문입니다.
마무리
Upstash Redis®를 사용하면 서버리스 환경에서도 캐싱 시스템을 손쉽게 구현할 수 있습니다.
여기서는 할당량 시스템을 구현해 보았지만, 이 아이디어는 그 외에도 다양한 사례에 적용할 수 있습니다. 예를 들어 더 많은 데이터베이스 쿼리를 캐싱하거나, API 요청 결과를 캐싱하는 용도로도 활용할 수 있습니다.
캐싱의 궁극적인 목표는 API 응답 속도를 높이고 리소스 소비를 줄이는 것입니다. 예컨대 PlanetScale 데이터베이스의 읽기/쓰기 쿼리 횟수를 줄일 수 있겠죠.
다음 글에서는 Upstash QStash를 사용해 Redis®에 저장된 사용자의 할당량을 주기적으로 조회하고 데이터베이스에 동기화하는 방법을 알아보겠습니다. 이를 통해 할당량 데이터가 항상 최신 상태를 유지하며 손실되지 않도록 보장할 수 있습니다.
직접 체험해 보기
이런 최적화가 실제로 동작하는 모습이 궁금하시다면 fastsheet를 확인해 보세요. 몇 번의 클릭만으로 Google Sheets를 API로 변환해 주는 API 서비스로, 넉넉한 무료 티어를 제공합니다.