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:
parent
fec2a4455c
commit
3f27b1f457
4 changed files with 127 additions and 40 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
61
backend/src/utils/cache.js
Normal file
61
backend/src/utils/cache.js
Normal 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}`,
|
||||
};
|
||||
|
|
@ -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단계 | 검색 페이징 최적화 | 🔄 진행 예정 |
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue