이 튜토리얼에서는 Node.js와 Redis를 사용하여 확장 가능한 URL 단축 서비스를 만들 것입니다. 이 서비스는 분산 캐싱을 활용하여 고품질의 트래픽 처리, 지연 시간 감소, 그리고 신속한 확장을 가능하게 할 것입니다. 일관된 해싱, 캐시 무효화 전략, 샤딩과 같은 주요 개념을 탐구하여 시스템이 빠르고 신뢰할 수 있도록 보장할 것입니다.

이 가이드를 마치면 분산 캐싱을 활용하여 성능을 최적화하는 기능이 완벽하게 구현된 URL 단축 서비스를 갖게 될 것입니다. 또한 사용자가 URL을 입력하고 캐시 히트 및 미스와 같은 실시간 메트릭을 볼 수 있는 대화형 데모도 만들 것입니다.

학습할 내용

  • Node.js와 Redis를 사용하여 URL 단축 서비스를 구축하는 방법

  • 분산 캐싱을 구현하여 성능을 최적화하는 방법

  • 일관된 해싱 및 캐시 무효화 전략 이해

  • 다중 Redis 인스턴스를 시뮬레이션하여 샤딩 및 확장을 위한 Docker 사용

필수 사항

시작하기 전에 다음이 설치되어 있는지 확인하십시오:

  • Node.js (v14 이상)

  • Redis

  • Docker

  • JavaScript, Node.js 및 Redis에 대한 기본 지식.

목차

프로젝트 개요

URL 단축 서비스를 구축할 것입니다. 여기에는:

  1. 사용자가 긴 URL을 단축하고 원래 URL을 검색할 수 있습니다.

  2. 서비스는 단축된 URL과 원래 URL 간의 매핑을 저장하기 위해 Redis 캐싱을 사용합니다.

  3. 캐시는 고트래픽을 처리하기 위해 여러 개의 Redis 인스턴스에 분산되어 있습니다.

  4. 시스템은 실시간으로 캐시 히트미스를 보여줄 것입니다.

시스템 아키텍처

확장성과 성능을 보장하기 위해 서비스를 다음 구성 요소로 나눌 것입니다:

  1. API 서버: URL 단축 및 검색 요청 처리.

  2. Redis 캐싱 레이어: 분산 캐싱을 위해 여러 Redis 인스턴스 사용.

  3. 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}`);
});

이 코드에서 볼 수 있듯이, 우리는 다음을 가지고 있습니다:

  1. 일관된 해싱:

    • 간단한 해시 함수를 사용하여 키(단축된 URL)를 여러 Redis 클라이언트에 분배합니다.

    • 해시 함수는 URL이 Redis 인스턴스에 고르게 분배되도록 보장합니다.

  2. URL 단축:

    • /shorten 엔드포인트는 긴 URL을 받아 shortid 라이브러리를 사용하여 짧은 ID를 생성합니다.

    • 단축된 URL은 해시 함수를 사용하여 Redis 인스턴스 중 하나에 저장됩니다.

  3. 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: 애플리케이션 테스트

  1. Redis 인스턴스를 시작합니다:
docker start redis1 redis2 redis3
  1. Node.js 서버를 실행합니다:
node index.js
  1. 엔드포인트를 테스트하세요 curl이나 Postman을 사용하여:

    • URL을 단축하세요:

        POST http://localhost:3000//shorten
        Body: { "url": "https://example.com" }
      
    • 단축된 URL에 액세스하세요:

        GET http://localhost:3000//{shortId}
      

결론: 배운 내용

축하합니다! Node.js와 Redis를 사용하여 분산 캐싱을 통해 확장 가능한 URL 단축 서비스를 성공적으로 구축했습니다. 이 튜토리얼을 통해 다음을 배웠습니다:

  1. 다중 Redis 인스턴스에 캐시 항목을 분산하는 일관된 해싱을 구현하십시오.

  2. 데이터를 최신 상태로 유지하기 위해 캐시 무효화 전략으로 응용 프로그램을 최적화하십시오.

  3. Docker를 사용하여 다중 Redis 노드로 분산 환경을 시뮬레이션하십시오.

  4. 성능을 최적화하기 위해 캐시 히트 및 미스 모니터링하십시오.

다음 단계:

  • 데이터베이스 추가: 캐시 이상으로 지속성을 위해 URL을 데이터베이스에 저장하십시오.

  • 분석 구현: 단축된 URL의 클릭 수와 분석을 추적하십시오.

  • 클라우드에 배포: 자동 확장과 신뢰성을 위해 Kubernetes를 사용하여 응용 프로그램을 배포하십시오.

코딩 즐기세요!