2편. Redis 실습 프로젝트 구성하기: Prisma, SQLite, Redis, Vitest
2편. Redis 실습 프로젝트 구성하기: Prisma, SQLite, Redis, Vitest
📚 목차
1. Redis 실습 프로젝트 목표와 전체 구조 설계하기
2. Prisma 7과 SQLite로 스키마 구성하기
3. Redis 연결 코드와 공통 라이브러리 구성하기
4. Vitest 기반 테스트 환경 구성하기

📂 [GitHub 코드 보러가기] : https://github.com/cericube/nodejs-practice-lab/tree/main/redis-examples
1. Redis 실습 프로젝트 목표와 전체 구조 설계하기
Redis의 여러 자료구조를 단순 명령어 수준에서만 확인하지 않고, 실제 백엔드 서비스 코드에서 어떻게 활용할 수 있는지 확인해 보겠습니다.
String → 사용자 조회 캐싱, 인증 코드, 조회수 카운터, Rate Limiting
Hash → 사용자 프로필, 세션, 상품 재고, 사용자 설정
List → 최근 본 게시글, 최근 검색어, 간단한 작업 큐, 로그 버퍼
Set → 좋아요, 일일 방문자, 온라인 사용자, 중복 요청 방지
Sorted Set → 인기 게시글, 검색어 순위, 사용자 포인트, 우선순위 큐
Stream → 주문 이벤트, 알림 이벤트 큐, 이메일 작업 큐, 감사 로그
Pub/Sub → 실시간 알림, 캐시 무효화, 채팅 브로드캐스트, 관리자 공지
🔷 루트 프로젝트 안에 독립 하위 프로젝트로 구성하기
기존 루트 프로젝트 안에 redis-examples를 별도 하위 프로젝트로 구성합니다.
nodejs-practice-lab/
├─ fastify-api-rest/
├─ redis-examples/
│ ├─ package.json
│ ├─ tsconfig.json
│ ├─ prisma.config.ts
│ ├─ vitest.config.ts
│ ├─ prisma/
│ ├─ src/
│ └─ tests/
├─ package.json
└─ tsconfig.json
redis-examples는 루트 프로젝트 안에 있지만, 자체 package.json, tsconfig.json, prisma.config.ts, vitest.config.ts를 가지는 독립 실습 프로젝트로 관리합니다.
🔷 workspace에 redis-examples 등록하기
루트 package.json에는 redis-examples를 workspace로 등록합니다.
{
"name": "nodejs-practice-lab",
"version": "1.0.0",
"private": true,
"type": "module",
"workspaces": [
"fastify-api-rest",
"redis-examples"
]
}
이렇게 하면 루트 프로젝트에서 여러 하위 Node.js 프로젝트를 함께 관리할 수 있습니다.
🔷 실습 프로젝트 구조
redis-examples/
├─ prisma/
│ ├─ migrations/
│ ├─ dev.db
│ └─ schema.prisma
├─ src/
│ ├─ generated/
│ │ └─ prisma/
│ ├─ lib/
│ │ ├─ prisma.ts
│ │ └─ redis.ts
│ ├─ redis/
│ │ ├─ redis-key.ts
│ │ └─ cache.service.ts
│ ├─ services/
│ │ ├─ user.service.ts
│ │ ├─ post.service.ts
│ │ ├─ product.service.ts
│ │ ├─ order.service.ts
│ │ ├─ search.service.ts
│ │ ├─ session.service.ts
│ │ ├─ notification.service.ts
│ │ └─ rate-limit.service.ts
│ └─ scripts/
│ └─ seed.ts
├─ tests/
│ ├─ setup.ts
│ └─ services/
│ ├─ user.service.test.ts
│ ├─ post.service.test.ts
│ ├─ product.service.test.ts
│ └─ redis-connection.test.ts
├─ .env
├─ package.json
├─ prisma.config.ts
├─ tsconfig.json
└─ vitest.config.ts
src/lib/prisma.ts → Prisma Client 공통 연결
src/lib/redis.ts → Redis Client 공통 연결
src/redis/redis-key.ts → Redis key 규칙 관리
src/redis/cache.service.ts → JSON 캐시 공통 처리
src/services/ → 실무 시나리오별 Service Layer
tests/ → Service 단위 테스트
2. Prisma 7과 SQLite로 스키마 구성하기
🔷 실습용 패키지 설치하기
redis-examples 하위 프로젝트에서 필요한 패키지를 설치합니다.
# Prisma 기본 패키지
npm install @prisma/client@7.2.0
npm install -D prisma@7.2.0
# SQLite Adapter 패키지
npm install @prisma/adapter-better-sqlite3@7.2.0 better-sqlite3@12.10.0
npm install -D @types/better-sqlite3@7.6.13
# Redis Client
npm install redis@6.0.0
# 환경 변수
npm install dotenv@17.4.2
# 테스트
npm install -D vitest@4.0.17 @vitest/coverage-v8@4.0.17
nodejs-practice-lab@1.0.0 D:\NodejsDevelope\workspace\nodejs-practice-lab
└─┬ redis-examples@1.0.0 -> .\redis-examples
├── @prisma/adapter-better-sqlite3@7.2.0
├── @prisma/client@7.2.0
├── @types/better-sqlite3@7.6.13
├── @vitest/coverage-v8@4.0.17
├── better-sqlite3@12.10.0
├── dotenv@17.4.2
├── prisma@7.2.0
├── redis@6.0.0
└── vitest@4.0.17
🔷 .env 설정하기
REDIS_URL=redis://:mypassword@127.0.0.1:6379
# Prisma에서 SQLite 상대 경로는 보통 prisma/schema.prisma 기준으로 해석됩니다.
DATABASE_URL="file:./dev.db"
🔷 prisma.config.ts 작성하기
import 'dotenv/config';
import { defineConfig, env } from 'prisma/config';
export default defineConfig({
schema: 'prisma/schema.prisma',
migrations: {
path: 'prisma/migrations',
},
datasource: {
url: env('DATABASE_URL'),
},
});
🔷 Prisma Schema 작성 : prisma/schema.prisma
// =========================
// Prisma Generator
// =========================
// Prisma Client 생성 설정
// - Prisma 7부터 provider는 "prisma-client" 사용 (-js 제거)
// - output을 지정하지 않으면 monorepo / tsconfig path 환경에서 import 꼬일 수 있음
generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
}
datasource db {
provider = "sqlite"
}
// 사용자 정보 모델
// Redis String: 사용자 조회 캐싱
// Redis Hash: 사용자 프로필, 세션, 사용자 설정
// Redis Sorted Set: 사용자 포인트 랭킹 실습에 사용
model User {
id Int @id @default(autoincrement())
email String @unique
name String
point Int @default(0)
status String @default("ACTIVE")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// 사용자가 작성한 게시글 목록
posts Post[]
// 사용자의 주문 목록
orders Order[]
}
// 게시글 모델
// Redis String: 조회수 카운터
// Redis List: 최근 본 게시글
// Redis Set: 좋아요 사용자 목록
// Redis Sorted Set: 인기 게시글 랭킹 실습에 사용
model Post {
id Int @id @default(autoincrement())
title String
content String
authorId Int
status String @default("DRAFT")
viewCount Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// 게시글 작성자과의 관계 설정
author User @relation(fields: [authorId], references: [id])
}
// 상품 모델
// Redis Hash: 상품 재고 상태 캐싱 실습에 사용
model Product {
id Int @id @default(autoincrement())
name String
stock Int @default(0)
status String @default("ON_SALE")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// 주문 모델
// Redis Stream: 주문 생성 이벤트 실습에 사용
model Order {
id Int @id @default(autoincrement())
userId Int
status String @default("CREATED")
totalPrice Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// 주문한 사용자
user User @relation(fields: [userId], references: [id])
}
// 감사 로그 모델
// Redis Stream: 감사 로그 이벤트 실습
// DB 저장 방식과 Redis Stream 저장 방식을 비교할 때 사용
model AuditLog {
id Int @id @default(autoincrement())
action String
target String
message String
createdAt DateTime @default(now())
}
🔷 데이터베이스 생성 및 Prisma Client 생성
# schema.prisma 파일의 내용대로 데이터베이스 테이블 생성
npx prisma migrate dev --name init
Loaded Prisma config from prisma.config.ts.
Prisma schema loaded from prisma\schema.prisma.
Datasource "db": SQLite database "dev.db" at "file:./dev.db"
# 클라이언트 코드를 생성
npx prisma generate
# 실행 후 다음 경로에 Prisma Client 코드가 생성됩니다.
# src/generated/prisma/
3. Redis 연결 코드와 공통 라이브러리 구성하기
🔷 Prisma Client 공통 파일 작성하기 : src/lib/prisma.ts
import 'dotenv/config';
import { PrismaClient } from '../generated/prisma/client';
import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3';
// .env 파일에 정의된 DATABASE_URL 값을 읽어옵니다.
const connectionString = process.env.DATABASE_URL;
if (!connectionString) {
throw new Error('DATABASE_URL is not defined in .env file');
}
// Prisma에 전달할 Better SQLite3 어댑터 인스턴스를 생성합니다.
const adapter = new PrismaBetterSqlite3({
url: connectionString,
});
// Prisma 클라이언트를 생성하여 애플리케이션에서 재사용할 수 있도록 export 합니다.
// 다른 모듈에서 import { prisma } from './lib/prisma' 형태로 사용합니다.
export const prisma = new PrismaClient({
adapter,
});
이 파일은 모든 Service에서 공통으로 사용하는 Prisma Client입니다.
🔷 Redis Client 공통 파일 작성하기 : src/lib/redis.ts
import 'dotenv/config';
import { createClient } from 'redis';
// 환경 변수에서 Redis 연결 URL을 읽어옵니다.
const redisUrl = process.env.REDIS_URL;
// REDIS_URL이 정의되어 있지 않으면 애플리케이션 실행을 중단합니다.
if (!redisUrl) {
throw new Error('REDIS_URL is not defined');
}
// Redis 클라이언트를 생성합니다. URL을 통해 Redis 서버에 연결하도록 설정합니다.
export const redis = createClient({
url: redisUrl,
});
// Redis 클라이언트에서 발생하는 에러를 콘솔에 출력합니다.
redis.on('error', (error) => {
console.error('[Redis Error]', error);
});
// Redis 연결을 초기화하는 함수입니다.
// 이미 연결되어 있지 않으면 connect()를 호출하여 서버에 연결합니다.
export async function connectRedis() {
if (!redis.isOpen) {
await redis.connect();
}
return redis;
}
// Redis 연결을 종료하는 함수입니다.
// 연결이 열려 있는 경우 quit()를 호출하여 안전하게 연결을 끊습니다.
export async function disconnectRedis() {
if (redis.isOpen) {
await redis.quit();
}
}
🔷 RedisKey 유틸 파일 만들기 : src/redis/redis-key.ts
// src/redis/redis-key.ts
/**
* Redis Key 규칙을 한 곳에서 관리하는 유틸입니다.
*
* 기본 규칙:
* - cache:* : JSON 캐시
* - string:* : Redis String 실습
* - hash:* : Redis Hash 실습
* - list:* : Redis List 실습
* - set:* : Redis Set 실습
* - zset:* : Redis Sorted Set 실습
* - stream:* : Redis Stream 실습
* - channel:* : Redis Pub/Sub 실습
*/
export const RedisKey = {
cache: {
user: (userId: number) => `cache:user:${userId}`, // 사용자 단건 조회 캐시
},
string: {
authCode: (email: string) => `string:auth-code:${email}`, // 이메일 인증 코드
rateLimit: (key: string) => `string:rate-limit:${key}`, // 요청 횟수 제한
postViewCount: (postId: number) => `string:post-view-count:${postId}`, // 게시글 조회수 카운터
},
hash: {
userProfile: (userId: number) => `hash:user-profile:${userId}`, // 사용자 프로필 캐시
userSession: (sessionId: string) => `hash:session:${sessionId}`, // 로그인 세션 정보
userSetting: (userId: number) => `hash:user-setting:${userId}`, // 사용자 설정 정보
productStock: (productId: number) => `hash:product-stock:${productId}`, // 상품 재고 상태
},
list: {
postRecentViews: (userId: number) => `list:user:${userId}:recent-posts`, // 최근 본 게시글 목록
searchRecent: (userId: number) => `list:user:${userId}:recent-searches`, // 최근 검색어 목록
simpleJobQueue: () => `list:simple-job-queue`, // 간단한 작업 큐
logBuffer: () => `list:log-buffer`, // 최근 로그 버퍼
},
set: {
postLikes: (postId: number) => `set:post-likes:${postId}`, // 게시글 좋아요 사용자 목록
dailyVisitors: (date: string) => `set:daily-visitors:${date}`, // 일일 방문자 중복 제거
onlineUsers: () => `set:online-users`, // 현재 온라인 사용자 목록
duplicateRequest: (requestId: string) => `set:duplicate-request:${requestId}`, // 중복 요청 방지
},
zset: {
postRanking: () => `zset:post-ranking`, // 인기 게시글 랭킹
searchRanking: () => `zset:search-ranking`, // 인기 검색어 순위
userPointRanking: () => `zset:user-point-ranking`, // 사용자 포인트 랭킹
priorityQueue: () => `zset:priority-queue`, // 우선순위 큐
},
stream: {
orders: () => `stream:orders`, // 주문 이벤트 스트림
notifications: () => `stream:notifications`, // 알림 이벤트 큐
emails: () => `stream:emails`, // 이메일 작업 큐
auditLogs: () => `stream:audit-logs`, // 감사 로그 스트림
},
channel: {
notification: () => `channel:notification`, // 실시간 알림 채널
cacheInvalidation: () => `channel:cache-invalidation`, // 캐시 무효화 채널
chat: (roomId: string) => `channel:chat:${roomId}`, // 채팅방 메시지 채널
adminNotice: () => `channel:admin-notice`, // 관리자 공지 채널
},
} as const;
🔷 CacheService 기본 구조 만들기 : src/redis/cache.service.ts
// src/redis/cache.service.ts
import { redis } from '../lib/redis.js';
/**
* Redis String 기반 JSON 캐시를 다루는 공통 서비스입니다.
*
* Redis의 String 자료구조는 문자열만 저장할 수 있으므로,
* 객체 데이터는 JSON.stringify()로 문자열 변환 후 저장하고
* 조회할 때는 JSON.parse()로 다시 객체로 변환합니다.
*
* 사용 예:
* - 사용자 조회 결과 캐싱
* - 게시글 상세 조회 캐싱
* - 상품 상세 정보 캐싱
*/
export class CacheService {
/**
* Redis에서 JSON 문자열을 조회한 뒤 객체로 변환합니다.
*
* @param key Redis key
* @returns 캐시가 있으면 객체, 없으면 null
*/
async getJson<T>(key: string): Promise<T | null> {
const cached = await redis.get(key);
if (!cached) {
return null;
}
try {
return JSON.parse(cached) as T;
} catch {
await this.deleteCache(key);
return null;
}
}
/**
* 객체 데이터를 JSON 문자열로 변환하여 Redis에 저장합니다.
*
* TTL을 함께 설정하여 캐시가 일정 시간이 지나면
* 자동으로 만료되도록 합니다.
*
* @param key Redis key
* @param value 저장할 객체 데이터
* @param ttlSeconds 캐시 만료 시간, 초 단위
*/
async setJson<T>(key: string, value: T, ttlSeconds: number): Promise<void> {
if (!Number.isInteger(ttlSeconds) || ttlSeconds <= 0) {
throw new Error('ttlSeconds must be a positive integer');
}
const serializedValue = JSON.stringify(value);
await redis.set(key, serializedValue, {
EX: ttlSeconds,
});
}
/**
* Redis key를 삭제합니다.
*
* 주로 DB 데이터가 수정되거나 삭제되었을 때
* 기존 캐시를 무효화하기 위해 사용합니다.
*
* @param key 삭제할 Redis key
*/
async deleteCache(key: string): Promise<void> {
await redis.del(key);
}
/**
* Redis key가 존재하는지 확인합니다.
*
* @param key 확인할 Redis key
* @returns key가 존재하면 true, 없으면 false
*/
async exists(key: string): Promise<boolean> {
const result = await redis.exists(key);
return result === 1;
}
}
4. Vitest 기반 테스트 환경 구성하기
🔷 vitest.config.ts 작성하기
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
// describe/it/expect를 테스트 파일에서 바로 사용할 수 있게 합니다.
globals: true,
// Fastify/Prisma 서버 코드를 테스트하므로 브라우저 대신 Node 런타임을 사용합니다.
environment: 'node',
// 기능별 테스트를 tests 아래에 모으는 현재 디렉터리 규칙입니다.
include: ['tests/**/*.test.ts'],
// DB 통합 테스트가 포함되어 있어 기본값보다 여유 있게 둡니다.
testTimeout: 10_000,
// 테스트 실행 전에 setup.ts 파일을 먼저 실행하여 테스트 환경을 초기화합니다.
// 또는 각 테스트 파일이 실행되기 전에 setup.ts에서 필요한 초기화 작업을 수행할 수 있습니다.
// setupFiles: ['./test/setup.ts'],
// 6. 커버리지 설정
coverage: {
// 테스트 실행 시 커버리지 수집을 활성화합니다.
enabled: true,
// V8 엔진의 native coverage API를 사용하는 공급자 (빠르고 정확함)
provider: 'v8',
// 생성할 커버리지 리포트 형식 (터미널 출력, JSON, 브라우저 HTML 등)
reporter: ['text', 'json', 'html'],
// 커버리지 리포트가 저장될 디렉터리 경로
reportsDirectory: './coverage',
// 커버리지 측정 대상 파일을 지정합니다.
// 보통 테스트 대상 소스코드를 포함하는 경로를 지정합니다.
include: ['src/**/*.ts'],
// 커버리지에서 제외할 파일 목록입니다.
// 서버 엔트리포인트, 타입 정의(d.ts), 테스트 파일 등은 일반적으로 제외합니다.
exclude: [
'**/*.d.ts', // 타입 선언 파일은 실행 코드가 아니므로 제외
'**/*.test.ts', // 테스트 자체는 커버리지 측정 대상이 아님
],
},
},
});
🔷 테스트 공통 setup 작성하기 : tests/setup.ts
import { afterAll, beforeEach } from 'vitest';
import { prisma } from '../src/lib/prisma.js';
import { connectRedis, disconnectRedis } from '../src/lib/redis.js';
beforeEach(async () => {
await prisma.auditLog.deleteMany();
await prisma.order.deleteMany();
await prisma.post.deleteMany();
await prisma.product.deleteMany();
await prisma.user.deleteMany();
const redis = await connectRedis();
await redis.flushDb();
});
afterAll(async () => {
await prisma.$disconnect();
await disconnectRedis();
});
🔷 기본 Service 테스트 작성하기 : tests/services/redis-connection.test.ts
import { describe, expect, it } from 'vitest';
import { redis } from '../../src/lib/redis.js';
import '../setup.js';
describe('Redis Connection', () => {
it('Redis에 값을 저장하고 조회할 수 있다', async () => {
await redis.set('greeting', 'Hello, Redis!');
const value = await redis.get('greeting');
expect(value).toBe('Hello, Redis!');
});
});
🔷테스트 실행
redis-examples> npx vitest .\tests\services\redis-connections.test.ts
DEV v4.0.17 D:/NodejsDevelope/workspace/nodejs-practice-lab/redis-examples
Coverage enabled with v8
✓ tests/services/redis-connections.test.ts (1 test) 116ms
✓ Redis Connection (1)
✓ Redis에 값을 저장하고 조회할 수 있다 109ms
Test Files 1 passed (1)
Tests 1 passed (1)
Start at 13:05:03
Duration 1.45s (transform 136ms, setup 0ms, import 688ms, tests 116ms, environment 0ms)
※ 게시된 글 및 이미지 중 일부는 AI 도구의 도움을 받아 생성되거나 다듬어졌습니다.