diff --git a/backend/src/routes/members/index.js b/backend/src/routes/members/index.js index 00650e6..0856bfb 100644 --- a/backend/src/routes/members/index.js +++ b/backend/src/routes/members/index.js @@ -1,16 +1,16 @@ 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: 인증 필요 */ export default async function membersRoutes(fastify, opts) { - const { db } = fastify; + const { db, redis } = fastify; /** * GET /api/members - * 전체 멤버 목록 조회 (공개) + * 전체 멤버 목록 조회 (공개, 캐시 적용) */ fastify.get('/', { schema: { @@ -19,7 +19,7 @@ export default async function membersRoutes(fastify, opts) { }, }, async (request, reply) => { try { - return await getAllMembers(db); + return await getAllMembers(db, redis); } catch (err) { fastify.log.error(err); return reply.code(500).send({ error: '멤버 목록 조회 실패' }); @@ -155,6 +155,9 @@ export default async function membersRoutes(fastify, opts) { } } + // 멤버 캐시 무효화 + await invalidateMemberCache(redis); + return { message: '멤버 정보가 수정되었습니다', id: memberId }; } catch (err) { fastify.log.error(err); diff --git a/backend/src/services/member.js b/backend/src/services/member.js index 06939c7..f01b15c 100644 --- a/backend/src/services/member.js +++ b/backend/src/services/member.js @@ -2,44 +2,62 @@ * 멤버 서비스 * 멤버 관련 비즈니스 로직 */ +import { getOrSet, invalidate, cacheKeys } from '../utils/cache.js'; /** - * 전체 멤버 목록 조회 (별명 포함) + * 전체 멤버 목록 조회 (별명 포함, 캐시 적용) * @param {object} db - 데이터베이스 연결 + * @param {object} redis - Redis 클라이언트 (캐시용, 선택적) * @returns {array} 멤버 목록 */ -export async function getAllMembers(db) { - const [members] = await db.query(` - SELECT - m.id, m.name, m.name_en, m.birth_date, m.instagram, m.image_id, m.is_former, - i.original_url as image_original, - i.medium_url as image_medium, - i.thumb_url as image_thumb - FROM members m - LEFT JOIN images i ON m.image_id = i.id - ORDER BY m.is_former ASC, m.id ASC - `); +export async function getAllMembers(db, redis = null) { + const fetchMembers = async () => { + const [members] = await db.query(` + SELECT + m.id, m.name, m.name_en, m.birth_date, m.instagram, m.image_id, m.is_former, + i.original_url as image_original, + i.medium_url as image_medium, + i.thumb_url as image_thumb + FROM members m + LEFT JOIN images i ON m.image_id = i.id + ORDER BY m.is_former ASC, m.id ASC + `); - // 별명 조회 - const [nicknames] = await db.query( - 'SELECT member_id, nickname FROM member_nicknames' - ); + // 별명 조회 + const [nicknames] = await db.query( + 'SELECT member_id, nickname FROM member_nicknames' + ); - // 멤버별 별명 매핑 - const nicknameMap = {}; - for (const n of nicknames) { - if (!nicknameMap[n.member_id]) { - nicknameMap[n.member_id] = []; + // 멤버별 별명 매핑 + const nicknameMap = {}; + for (const n of nicknames) { + if (!nicknameMap[n.member_id]) { + nicknameMap[n.member_id] = []; + } + nicknameMap[n.member_id].push(n.nickname); } - nicknameMap[n.member_id].push(n.nickname); - } - // 멤버 데이터에 별명 추가 - return members.map(m => ({ - ...m, - nicknames: nicknameMap[m.id] || [], - image_url: m.image_thumb || m.image_medium || m.image_original, - })); + // 멤버 데이터에 별명 추가 + return members.map(m => ({ + ...m, + nicknames: nicknameMap[m.id] || [], + 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); } /** diff --git a/backend/src/utils/cache.js b/backend/src/utils/cache.js new file mode 100644 index 0000000..b55b4bb --- /dev/null +++ b/backend/src/utils/cache.js @@ -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} 캐시된 값 또는 새로 조회한 값 + */ +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}`, +}; diff --git a/docs/refactoring.md b/docs/refactoring.md index c35855f..9851336 100644 --- a/docs/refactoring.md +++ b/docs/refactoring.md @@ -196,13 +196,18 @@ --- -### 19단계: Redis 캐시 확대 🔄 진행 예정 -- [ ] 일정 상세 조회 캐싱 -- [ ] 멤버 목록 캐싱 +### 19단계: Redis 캐시 확대 ✅ 완료 +- [x] 캐시 유틸리티 생성 (`src/utils/cache.js`) +- [x] 멤버 목록 캐싱 (10분 TTL) +- [x] 멤버 수정 시 캐시 무효화 +- [ ] 일정 상세 조회 - X 프로필이 별도 캐시 사용하므로 보류 -**대상 파일:** -- `src/routes/schedules/index.js` - 캐시 적용 -- `src/routes/members/index.js` - 캐시 적용 +**생성된 파일:** +- `src/utils/cache.js` - getOrSet, invalidate, invalidatePattern, cacheKeys + +**수정된 파일:** +- `src/services/member.js` - getAllMembers에 캐시 적용, invalidateMemberCache 추가 +- `src/routes/members/index.js` - Redis 캐시 사용, 수정 시 캐시 무효화 --- @@ -247,7 +252,7 @@ | 16단계 | 에러 처리 일관성 | ✅ 완료 | | 17단계 | 중복 코드 제거 (멤버 조회) | ✅ 완료 | | 18단계 | 이미지 처리 최적화 | ✅ 완료 | -| 19단계 | Redis 캐시 확대 | 🔄 진행 예정 | +| 19단계 | Redis 캐시 확대 | ✅ 완료 | | 20단계 | 서비스 레이어 확대 | 🔄 진행 예정 | | 21단계 | 검색 페이징 최적화 | 🔄 진행 예정 |