소개
이 튜토리얼에서는 Node.js와 Redis를 활용해 확장성 있는 URL 단축 서비스를 직접 구축해 봅니다. 이 서비스는 분산 캐싱(distributed caching)을 통해 대량의 트래픽을 효율적으로 처리하고, 응답 지연 시간을 줄이며, 트래픽 증가에 따라 원활하게 확장할 수 있습니다. 더불어 일관성 해싱(consistent hashing), 캐시 무효화 전략, 샤딩(sharding) 같은 핵심 개념도 함께 살펴보며 시스템이 빠르고 안정적으로 동작하도록 만들겠습니다.
이 가이드를 끝까지 따라 하면 분산 캐싱으로 성능을 최적화한, 실제로 작동하는 URL 단축기를 완성하게 됩니다. 마지막에는 사용자가 URL을 입력하고 캐시 히트(hit)와 미스(miss) 같은 실시간 지표를 확인할 수 있는 인터랙티브 데모도 함께 만들어 봅니다.
학습 내용
Node.js와 Redis로 URL 단축 서비스를 구축하는 방법
성능 최적화를 위한 분산 캐싱 구현 방법
일관성 해싱과 캐시 무효화 전략에 대한 이해
Docker를 사용해 여러 Redis 인스턴스를 띄우고 샤딩·확장을 시뮬레이션하는 방법
사전 준비 사항
시작하기 전에 다음 항목이 설치되어 있는지 확인하세요.
Node.js (v14 이상)
Redis
Docker
JavaScript, Node.js, Redis에 대한 기본 지식
목차
프로젝트 개요
1단계: 프로젝트 설정
2단계: Redis 인스턴스 설정
3단계: URL 단축 서비스 구현
4단계: 캐시 무효화 구현
5단계: 캐시 지표 모니터링
6단계: 애플리케이션 테스트
결론: 학습 내용 정리
프로젝트 개요
우리가 만들 URL 단축 서비스의 동작 방식은 다음과 같습니다.
사용자는 긴 URL을 짧게 줄일 수 있고, 단축된 URL로 원본 URL을 다시 조회할 수 있습니다.
서비스는 Redis 캐싱을 사용해 단축 URL과 원본 URL 사이의 매핑 정보를 저장합니다.
캐시는 여러 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: 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 인스턴스 설정
Docker를 사용하면 여러 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
이렇게 하면 서로 다른 포트(6379, 6380, 6381)에서 실행되는 세 개의 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: `https://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}`);
});
이 코드의 핵심 동작을 정리하면 다음과 같습니다.
일관성 해싱(Consistent Hashing):
간단한 해시 함수를 사용해 키(단축 URL ID)를 여러 Redis 클라이언트에 분산시킵니다.
해시 함수 덕분에 URL들이 세 Redis 인스턴스에 고르게 분배됩니다.
URL 단축:
/shorten 엔드포인트는 긴 URL을 전달받고, shortid 라이브러리로 짧은 고유 ID를 생성합니다.
생성된 단축 URL 매핑은 해시 함수가 계산한 결과에 따라 세 Redis 인스턴스 중 하나에 저장됩니다.
URL 리디렉션:
/:shortId 엔드포인트는 캐시에서 원본 URL을 조회한 뒤 사용자를 해당 주소로 리디렉션합니다.
캐시에서 URL을 찾지 못하면 404 응답을 반환합니다.
4단계: 캐시 무효화 구현
실제 운영 환경에서는 URL이 시간이 지나면서 만료되거나 변경될 수 있습니다. 이런 상황에 대응하려면 캐시 무효화(cache invalidation) 로직이 반드시 필요합니다.
캐시된 URL에 만료 시간(TTL) 추가하기
index.js 파일을 수정해 각 캐시 항목에 만료 시간을 설정해 보겠습니다.
// 만료 시간이 있는 URL 단축 엔드포인트
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: `https://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);
});
});
코드의 동작 방식은 다음과 같습니다.
캐시 히트(Cache Hit): 요청한 URL이 캐시에 존재하는 경우입니다.
캐시 미스(Cache Miss): 요청한 URL이 캐시에 없는 경우입니다.
이 로그를 활용하면 분산 캐시의 성능과 상태를 손쉽게 모니터링할 수 있습니다.
6단계: 애플리케이션 테스트
Redis 인스턴스를 시작합니다.
docker start redis1 redis2 redis3
Node.js 서버를 실행합니다.
node index.js
curl또는 Postman으로 엔드포인트를 테스트합니다.URL 단축 요청:
POST https://localhost:3000/shorten Body: { "url": "https://example.com" }단축된 URL 접속:
GET https://localhost:3000/{shortId}
결론: 학습 내용 정리
축하합니다! Node.js와 Redis를 활용해 분산 캐싱이 적용된 확장성 있는 URL 단축 서비스를 성공적으로 구축했습니다. 이 튜토리얼을 통해 다음 내용을 익혔습니다.
일관성 해싱을 구현해 캐시 항목을 여러 Redis 인스턴스에 분산시키는 방법
데이터를 항상 최신 상태로 유지하는 캐시 무효화 전략 적용 방법
Docker로 여러 Redis 노드가 있는 분산 환경을 시뮬레이션하는 방법
캐시 히트/미스를 모니터링해 성능을 최적화하는 방법
다음 단계
데이터베이스 추가: 캐시 외에 데이터베이스에 URL을 저장해 영구적인 보관 기능을 구현할 수 있습니다.
분석 기능 구현: 단축 URL의 클릭 수와 접속 통계를 추적하는 기능을 추가해 보세요.
클라우드 배포: Kubernetes를 활용해 애플리케이션을 배포하면 오토스케일링과 장애 복원력을 확보할 수 있습니다.
즐거운 코딩 되세요!