refactor(backend): 19단계 Redis 캐시 확대 - 멤버 목록 캐싱

- utils/cache.js 생성: getOrSet, invalidate, invalidatePattern, cacheKeys
- services/member.js: getAllMembers에 Redis 캐시 적용 (10분 TTL)
- services/member.js: invalidateMemberCache 함수 추가
- routes/members: 캐시 사용 및 수정 시 캐시 무효화

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-01-21 16:01:12 +09:00
parent fec2a4455c
commit 3f27b1f457
4 changed files with 127 additions and 40 deletions

View file

@ -1,16 +1,16 @@
import { uploadMemberImage } from '../../services/image.js'; import { uploadMemberImage } from '../../services/image.js';
import { getAllMembers, getMemberByName, getMemberBasicByName } from '../../services/member.js'; import { getAllMembers, getMemberByName, getMemberBasicByName, invalidateMemberCache } from '../../services/member.js';
/** /**
* 멤버 라우트 * 멤버 라우트
* GET: 공개, PUT: 인증 필요 * GET: 공개, PUT: 인증 필요
*/ */
export default async function membersRoutes(fastify, opts) { export default async function membersRoutes(fastify, opts) {
const { db } = fastify; const { db, redis } = fastify;
/** /**
* GET /api/members * GET /api/members
* 전체 멤버 목록 조회 (공개) * 전체 멤버 목록 조회 (공개, 캐시 적용)
*/ */
fastify.get('/', { fastify.get('/', {
schema: { schema: {
@ -19,7 +19,7 @@ export default async function membersRoutes(fastify, opts) {
}, },
}, async (request, reply) => { }, async (request, reply) => {
try { try {
return await getAllMembers(db); return await getAllMembers(db, redis);
} catch (err) { } catch (err) {
fastify.log.error(err); fastify.log.error(err);
return reply.code(500).send({ error: '멤버 목록 조회 실패' }); return reply.code(500).send({ error: '멤버 목록 조회 실패' });
@ -155,6 +155,9 @@ export default async function membersRoutes(fastify, opts) {
} }
} }
// 멤버 캐시 무효화
await invalidateMemberCache(redis);
return { message: '멤버 정보가 수정되었습니다', id: memberId }; return { message: '멤버 정보가 수정되었습니다', id: memberId };
} catch (err) { } catch (err) {
fastify.log.error(err); fastify.log.error(err);

View file

@ -2,13 +2,16 @@
* 멤버 서비스 * 멤버 서비스
* 멤버 관련 비즈니스 로직 * 멤버 관련 비즈니스 로직
*/ */
import { getOrSet, invalidate, cacheKeys } from '../utils/cache.js';
/** /**
* 전체 멤버 목록 조회 (별명 포함) * 전체 멤버 목록 조회 (별명 포함, 캐시 적용)
* @param {object} db - 데이터베이스 연결 * @param {object} db - 데이터베이스 연결
* @param {object} redis - Redis 클라이언트 (캐시용, 선택적)
* @returns {array} 멤버 목록 * @returns {array} 멤버 목록
*/ */
export async function getAllMembers(db) { export async function getAllMembers(db, redis = null) {
const fetchMembers = async () => {
const [members] = await db.query(` const [members] = await db.query(`
SELECT SELECT
m.id, m.name, m.name_en, m.birth_date, m.instagram, m.image_id, m.is_former, m.id, m.name, m.name_en, m.birth_date, m.instagram, m.image_id, m.is_former,
@ -40,6 +43,21 @@ export async function getAllMembers(db) {
nicknames: nicknameMap[m.id] || [], nicknames: nicknameMap[m.id] || [],
image_url: m.image_thumb || m.image_medium || m.image_original, image_url: m.image_thumb || m.image_medium || m.image_original,
})); }));
};
// Redis가 있으면 캐시 사용
if (redis) {
return getOrSet(redis, cacheKeys.members, fetchMembers, 600); // 10분 캐시
}
return fetchMembers();
}
/**
* 멤버 캐시 무효화
* @param {object} redis - Redis 클라이언트
*/
export async function invalidateMemberCache(redis) {
await invalidate(redis, cacheKeys.members);
} }
/** /**

View file

@ -0,0 +1,61 @@
/**
* Redis 캐시 유틸리티
*/
// 기본 TTL (초 단위)
const DEFAULT_TTL = 300; // 5분
/**
* 캐시에서 값을 가져오거나 없으면 함수 실행 캐시
* @param {object} redis - Redis 클라이언트
* @param {string} key - 캐시
* @param {Function} fn - 데이터 조회 함수
* @param {number} ttl - TTL (), 기본 5
* @returns {Promise<any>} 캐시된 또는 새로 조회한
*/
export async function getOrSet(redis, key, fn, ttl = DEFAULT_TTL) {
// 캐시 조회
const cached = await redis.get(key);
if (cached) {
return JSON.parse(cached);
}
// 캐시 미스: 데이터 조회 후 캐싱
const data = await fn();
if (data !== null && data !== undefined) {
await redis.set(key, JSON.stringify(data), 'EX', ttl);
}
return data;
}
/**
* 캐시 무효화
* @param {object} redis - Redis 클라이언트
* @param {string|string[]} keys - 캐시 또는 배열
*/
export async function invalidate(redis, keys) {
const keyList = Array.isArray(keys) ? keys : [keys];
if (keyList.length > 0) {
await redis.del(...keyList);
}
}
/**
* 패턴으로 캐시 무효화
* @param {object} redis - Redis 클라이언트
* @param {string} pattern - 패턴 (: 'schedule:*')
*/
export async function invalidatePattern(redis, pattern) {
const keys = await redis.keys(pattern);
if (keys.length > 0) {
await redis.del(...keys);
}
}
// 캐시 키 생성 헬퍼
export const cacheKeys = {
members: 'members:all',
member: (name) => `member:${name}`,
scheduleDetail: (id) => `schedule:${id}`,
scheduleMonthly: (year, month) => `schedule:monthly:${year}:${month}`,
};

View file

@ -196,13 +196,18 @@
--- ---
### 19단계: Redis 캐시 확대 🔄 진행 예정 ### 19단계: Redis 캐시 확대 ✅ 완료
- [ ] 일정 상세 조회 캐싱 - [x] 캐시 유틸리티 생성 (`src/utils/cache.js`)
- [ ] 멤버 목록 캐싱 - [x] 멤버 목록 캐싱 (10분 TTL)
- [x] 멤버 수정 시 캐시 무효화
- [ ] 일정 상세 조회 - X 프로필이 별도 캐시 사용하므로 보류
**대상 파일:** **생성된 파일:**
- `src/routes/schedules/index.js` - 캐시 적용 - `src/utils/cache.js` - getOrSet, invalidate, invalidatePattern, cacheKeys
- `src/routes/members/index.js` - 캐시 적용
**수정된 파일:**
- `src/services/member.js` - getAllMembers에 캐시 적용, invalidateMemberCache 추가
- `src/routes/members/index.js` - Redis 캐시 사용, 수정 시 캐시 무효화
--- ---
@ -247,7 +252,7 @@
| 16단계 | 에러 처리 일관성 | ✅ 완료 | | 16단계 | 에러 처리 일관성 | ✅ 완료 |
| 17단계 | 중복 코드 제거 (멤버 조회) | ✅ 완료 | | 17단계 | 중복 코드 제거 (멤버 조회) | ✅ 완료 |
| 18단계 | 이미지 처리 최적화 | ✅ 완료 | | 18단계 | 이미지 처리 최적화 | ✅ 완료 |
| 19단계 | Redis 캐시 확대 | 🔄 진행 예정 | | 19단계 | Redis 캐시 확대 | ✅ 완료 |
| 20단계 | 서비스 레이어 확대 | 🔄 진행 예정 | | 20단계 | 서비스 레이어 확대 | 🔄 진행 예정 |
| 21단계 | 검색 페이징 최적화 | 🔄 진행 예정 | | 21단계 | 검색 페이징 최적화 | 🔄 진행 예정 |