이 튜토리얼에서는 Node.js와 Redis를 사용하여 확장 가능한 URL 단축 서비스를 만들 것입니다. 이 서비스는 분산 캐싱을 활용하여 고품질의 트래픽 처리, 지연 시간 감소, 그리고 신속한 확장을 가능하게 할 것입니다. 일관된 해싱, 캐시 무효화 전략, 샤딩과 같은 주요 개념을 탐구하여 시스템이 빠르고 신뢰할 수 있도록 보장할 것입니다.
이 가이드를 마치면 분산 캐싱을 활용하여 성능을 최적화하는 기능이 완벽하게 구현된 URL 단축 서비스를 갖게 될 것입니다. 또한 사용자가 URL을 입력하고 캐시 히트 및 미스와 같은 실시간 메트릭을 볼 수 있는 대화형 데모도 만들 것입니다.
학습할 내용
-
Node.js와 Redis를 사용하여 URL 단축 서비스를 구축하는 방법
-
분산 캐싱을 구현하여 성능을 최적화하는 방법
-
일관된 해싱 및 캐시 무효화 전략 이해
-
다중 Redis 인스턴스를 시뮬레이션하여 샤딩 및 확장을 위한 Docker 사용
필수 사항
시작하기 전에 다음이 설치되어 있는지 확인하십시오:
-
Node.js (v14 이상)
-
Redis
-
Docker
-
JavaScript, Node.js 및 Redis에 대한 기본 지식.
목차
프로젝트 개요
URL 단축 서비스를 구축할 것입니다. 여기에는:
-
사용자가 긴 URL을 단축하고 원래 URL을 검색할 수 있습니다.
-
서비스는 단축된 URL과 원래 URL 간의 매핑을 저장하기 위해 Redis 캐싱을 사용합니다.
-
캐시는 고트래픽을 처리하기 위해 여러 개의 Redis 인스턴스에 분산되어 있습니다.
-
시스템은 실시간으로 캐시 히트와 미스를 보여줄 것입니다.
시스템 아키텍처
확장성과 성능을 보장하기 위해 서비스를 다음 구성 요소로 나눌 것입니다:
-
API 서버: URL 단축 및 검색 요청 처리.
-
Redis 캐싱 레이어: 분산 캐싱을 위해 여러 Redis 인스턴스 사용.
-
Docker: 여러 Redis 컨테이너로 분산 환경 시뮬레이션.
단계 1: 프로젝트 설정
Node.js 애플리케이션을 초기화하여 프로젝트를 설정해 봅시다:
mkdir scalable-url-shortener
cd scalable-url-shortener
npm init -y
이제 필요한 종속성을 설치하세요:
npm install express redis shortid dotenv
-
express
: 가벼운 웹 서버 프레임워크. -
redis
: 캐싱 처리를 위해. -
shortid
: 짧고 고유한 ID 생성을 위해. -
dotenv
: 환경 변수 관리를 위한 도구.
프로젝트 루트에 .env 파일을 만듭니다:
PORT=3000
REDIS_HOST_1=localhost
REDIS_PORT_1=6379
REDIS_HOST_2=localhost
REDIS_PORT_2=6380
REDIS_HOST_3=localhost
REDIS_PORT_3=6381
이 변수들은 사용할 Redis 호스트와 포트를 정의합니다.
단계 2: Redis 인스턴스 설정
여러 개의 Redis 인스턴스를 사용하는 분산 환경을 시뮬레이션하기 위해 도커를 사용할 것입니다.
다음 명령어를 실행하여 세 개의 Redis 컨테이너를 시작합니다:
docker run -p 6379:6379 --name redis1 -d redis
docker run -p 6380:6379 --name redis2 -d redis
docker run -p 6381:6379 --name redis3 -d redis
이렇게 하면 서로 다른 포트에서 실행되는 세 개의 Redis 인스턴스가 설정됩니다. 이러한 인스턴스를 사용하여 일관된 해싱과 샤딩을 구현할 것입니다.
단계 3: URL 단축 서비스 구현
주 애플리케이션 파일 index.js를 생성해봅시다:
require('dotenv').config();
const express = require('express');
const redis = require('redis');
const shortid = require('shortid');
const app = express();
app.use(express.json());
const redisClients = [
redis.createClient({ host: process.env.REDIS_HOST_1, port: process.env.REDIS_PORT_1 }),
redis.createClient({ host: process.env.REDIS_HOST_2, port: process.env.REDIS_PORT_2 }),
redis.createClient({ host: process.env.REDIS_HOST_3, port: process.env.REDIS_PORT_3 })
];
// Redis 클라이언트 간에 키를 분배하는 해시 함수
function getRedisClient(key) {
const hash = key.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
return redisClients[hash % redisClients.length];
}
// URL을 단축하는 엔드포인트
app.post('/shorten', async (req, res) => {
const { url } = req.body;
if (!url) return res.status(400).send('URL is required');
const shortId = shortid.generate();
const redisClient = getRedisClient(shortId);
await redisClient.set(shortId, url);
res.json({ shortUrl: `http://localhost:${process.env.PORT}/${shortId}` });
});
// 원본 URL을 검색하는 엔드포인트
app.get('/:shortId', async (req, res) => {
const { shortId } = req.params;
const redisClient = getRedisClient(shortId);
redisClient.get(shortId, (err, url) => {
if (err || !url) {
return res.status(404).send('URL not found');
}
res.redirect(url);
});
});
app.listen(process.env.PORT, () => {
console.log(`Server running on port ${process.env.PORT}`);
});
이 코드에서 볼 수 있듯이, 우리는 다음을 가지고 있습니다:
-
일관된 해싱:
-
간단한 해시 함수를 사용하여 키(단축된 URL)를 여러 Redis 클라이언트에 분배합니다.
해시 함수는 URL이 Redis 인스턴스에 고르게 분배되도록 보장합니다.
-
-
URL 단축:
-
/shorten 엔드포인트는 긴 URL을 받아 shortid 라이브러리를 사용하여 짧은 ID를 생성합니다.
-
단축된 URL은 해시 함수를 사용하여 Redis 인스턴스 중 하나에 저장됩니다.
-
-
URL 리다이렉션:
-
/:shortId 엔드포인트는 캐시에서 원래 URL을 검색하고 사용자를 리다이렉션합니다.
-
캐시에서 URL을 찾을 수 없으면 404 응답이 반환됩니다.
-
단계 4: 캐시 무효화 구현
실제 응용 프로그램에서 URL은 시간이 지남에 따라 만료되거나 변경될 수 있습니다. 이를 처리하기 위해 캐시 무효화를 구현해야 합니다.
캐시에 만료 추가
index.js 파일을 수정하여 각 캐시된 항목에 만료 시간을 설정해 봅시다:
// 만료 시간을 설정하는 엔드포인트
app.post('/shorten', async (req, res) => {
const { url, ttl } = req.body; // ttl (time-to-live)은 선택 사항입니다
if (!url) return res.status(400).send('URL is required');
const shortId = shortid.generate();
const redisClient = getRedisClient(shortId);
await redisClient.set(shortId, url, 'EX', ttl || 3600); // 기본 TTL은 1시간입니다
res.json({ shortUrl: `http://localhost:${process.env.PORT}/${shortId}` });
});
-
TTL (Time-To-Live): 각 단축된 URL에 대한 기본 만료 시간으로 1시간을 설정합니다. 필요한 경우 각 URL에 대해 TTL을 사용자 정의할 수 있습니다.
-
캐시 무효화: TTL이 만료되면 해당 항목이 자동으로 캐시에서 제거됩니다.
단계 5: 캐시 지표 모니터링
캐시 히트와 미스를 모니터링하려면 index.js의 엔드포인트에 로깅을 추가할 것입니다:
app.get('/:shortId', async (req, res) => {
const { shortId } = req.params;
const redisClient = getRedisClient(shortId);
redisClient.get(shortId, (err, url) => {
if (err || !url) {
console.log(`Cache miss for key: ${shortId}`);
return res.status(404).send('URL not found');
}
console.log(`Cache hit for key: ${shortId}`);
res.redirect(url);
});
});
이 코드에서 무슨 일이 벌어지고 있는지 살펴보겠습니다:
-
캐시 히트: 캐시에서 URL을 찾으면 캐시 히트입니다.
-
캐시 미스: URL을 찾지 못하면 캐시 미스입니다.
-
이 로깅은 분산 캐시의 성능을 모니터링하는 데 도움이 됩니다.
단계 6: 애플리케이션 테스트
- Redis 인스턴스를 시작합니다:
docker start redis1 redis2 redis3
- Node.js 서버를 실행합니다:
node index.js
-
엔드포인트를 테스트하세요
curl
이나 Postman을 사용하여:-
URL을 단축하세요:
POST http://localhost:3000//shorten Body: { "url": "https://example.com" }
-
단축된 URL에 액세스하세요:
GET http://localhost:3000//{shortId}
-
결론: 배운 내용
축하합니다! Node.js와 Redis를 사용하여 분산 캐싱을 통해 확장 가능한 URL 단축 서비스를 성공적으로 구축했습니다. 이 튜토리얼을 통해 다음을 배웠습니다:
-
다중 Redis 인스턴스에 캐시 항목을 분산하는 일관된 해싱을 구현하십시오.
-
데이터를 최신 상태로 유지하기 위해 캐시 무효화 전략으로 응용 프로그램을 최적화하십시오.
-
Docker를 사용하여 다중 Redis 노드로 분산 환경을 시뮬레이션하십시오.
-
성능을 최적화하기 위해 캐시 히트 및 미스 모니터링하십시오.
다음 단계:
-
데이터베이스 추가: 캐시 이상으로 지속성을 위해 URL을 데이터베이스에 저장하십시오.
-
분석 구현: 단축된 URL의 클릭 수와 분석을 추적하십시오.
-
클라우드에 배포: 자동 확장과 신뢰성을 위해 Kubernetes를 사용하여 응용 프로그램을 배포하십시오.
코딩 즐기세요!