Redis DAO 설계
세션 저장소
핵심 포인트
- • TTL 관리: 만료 기반 세션 수명 관리
- • KEEPTTL: TTL 유지한 채 업데이트
- • Context 연동: 세션/멀티 트랜잭션 컨텍스트 주입
운영 체크
- • null 가드: get 결과 파싱 전 null 체크
- • 멀티 실행: 상위 서비스에서 exec 커밋
- • 키 정책: userId 기반 단일 키
세션 설계 팁
- • 원자성: 관련 쓰기는 멀티에 모아 한 번에 실행
- • 격리: 사용자 단위 키로 경합 최소화
- • 가시성: 세션 스냅샷을 로그로 남겨 디버깅 용이
@Injectable()
export class SessionRepository extends AbstractRedisRepository {
protected readonly dbNumber = RedisDbNumber.SESSION;
constructor() {
super();
this.createRedisClient();
}
async getSession(userId: number): Promise<Session | null> {
const raw = await this.redis.get(userId.toString());
if (!raw) return null;
return plainToInstance(Session, JSON.parse(raw));
}
async setSession(userId: number, session: Session): Promise<void> {
const key = userId.toString();
const multi = this.multi;
const ttl = SESSION_EXPIRED_TIME;
ttl > 0 ? multi.set(key, JSON.stringify(session), 'EX', ttl)
: multi.set(key, JSON.stringify(session));
ContextProvider.setSession(session);
ContextProvider.setRedisMulti(this.dbNumber, multi);
}
}Redis
트랜잭션 처리 시스템
구현 내용
트랜잭션 기본 원리
- • ACID 속성 보장
- • 원자성(Atomicity): 모두 성공 또는 모두 실패
- • 일관성(Consistency): 데이터 무결성 유지
- • 격리성(Isolation): 동시 실행 트랜잭션 분리
- • 지속성(Durability): 커밋된 변경사항 영구 저장
Redis 트랜잭션
- • Redis Multi: 원자적 명령 실행
- • 세션 시퀀스: 트랜잭션 성공 시 시퀀스 증가
- • EXEC/DISCARD: 트랜잭션 커밋/롤백
- • RedisHelper: Multi 풀 관리
트랜잭션 테스트
테스트 방법
- • 1단계: 로그인으로 Session ID와 User ID 발급
- • 2단계: Role/Gold/Error Point 설정
- • 3단계: 트랜잭션 테스트 실행
- • Error Point 0: 정상 트랜잭션 (성공)
- • Error Point 1: 1번 포인트 실패 (롤백)
- • Error Point 2: 2번 포인트 실패 (롤백)
- • Error Point 3: 3번 포인트 실패 (롤백)
사용자 정보
트랜잭션 테스트 설정
@ApiTags('database')
@Controller('database')
export class DatabaseController {
@Post('transaction')
@ApiResponseEntity({
summary: '트랜잭션 확인하기',
})
@Auth()
@UserLevelLock()
async testTransaction(
@Body() transactionInDto: TransactionInDto,
): Promise<ResponseEntity<string>> {
await this.databaseService.testTransaction(transactionInDto);
return ResponseEntity.ok().body('업데이트 성공');
}
}캐싱 전략
캐시 아키텍처
3계층 캐시 구조
- • L1 - 애플리케이션 메모리: LRU 캐시, 정적 데이터
- • L2 - Redis 인메모리 DB: 세션, 공통 데이터
- • L3 - MySQL 데이터베이스: 영구 저장소
- • 캐시 미스 처리: 상위 계층에서 하위 계층으로 조회
캐시 패턴
- • Cache-Aside: 애플리케이션에서 캐시 관리
- • Write-Through: 쓰기 시 캐시와 DB 동시 업데이트
- • Write-Behind: 비동기 캐시 업데이트
- • Refresh-Ahead: 만료 전 미리 갱신
데이터 분류
- • 정적 데이터: 게임 데이터, 설정 정보 (메모리)
- • 세션 데이터: 사용자 세션, 상태 (Redis)
- • 동적 데이터: 사용자 정보, 게임 진행 (DB)
- • 임시 데이터: 쿼리 결과, 계산 결과 (LRU)
캐시 무효화
- • TTL 기반: 시간 기반 자동 삭제
- • 이벤트 기반: 데이터 변경 시 삭제
- • 버전 관리: 버전 변경 시 무효화
- • Pub/Sub 알림: Redis 메시지로 동기화
Portfolio 캐시 구조
실제 구현 구조
- • StaticRepository: 메모리 정적 데이터 관리
- • SessionRepository: Redis 세션 데이터 관리
- • LRU Cache: 쿼리 결과 캐싱
- • Pub/Sub System: 실시간 데이터 동기화
L1 - 메모리 캐시
StaticRepository - JSON 데이터 메모리 로드
LRU Cache (max: 1000) - 쿼리 결과 캐싱
deepFreeze() - 불변성 보장
Object.freeze() - 메모리 최적화
L2 - Redis 캐시
user:session:{userId} - 사용자 세션
event:info - 이벤트 정보 (전체 공통)
ranking:global - 전체 랭킹
ranking:guild - 길드 랭킹
TTL: 3600초 (1시간)
L3 - MySQL DB
user 테이블 - 사용자 정보
user_detail 테이블 - 상세 정보
game 테이블 - 게임 진행 데이터
GET_LOCK() - 사용자 락 (분산 락)
영구 저장소
Pub/Sub 동기화
Redis 채널: 'data' 구독
메시지: 'all' - 전체 리로드
메시지: ['Character'] - 특정 데이터 리로드
실시간 캐시 무효화
export abstract class StaticBaseRepository<T> {
items: Record<number, T> = {};
cache = createLRU<string, T[]>({
max: 1000,
});
abstract load(): void;
keys(): number[] {
return Object.keys(this.items).map(Number);
}
values(): T[] {
return Object.values(this.items);
}
findById(id: number): T {
return this.items[id];
}
findByIdIn(ids: number[]): T[] {
return ids.map((id) => this.items[id]).filter((it) => !!it);
}
query(queryInterface: QueryInterface<T>): T[] {
const queryKey = JSON.stringify(queryInterface);
const cached = this.cache.get(queryKey);
if (cached) {
return [...cached];
}
const result = this.values().filter((data) => {
for (const key in queryInterface) {
const value = data[key];
const conditions = queryInterface[key];
if (!Array.isArray(conditions) || conditions.length === 0) {
continue;
}
if (value === undefined) {
return false;
}
if (Array.isArray(conditions[0])) {
const valueStr = JSON.stringify(value);
const match = conditions.some(
(condition) => JSON.stringify(condition) === valueStr,
);
if (!match) {
return false;
}
continue;
}
if (!conditions.includes(value)) {
return false;
}
}
return true;
});
Object.freeze(result);
this.cache.set(queryKey, result);
return [...result];
}
}