관리 메뉴

Mini

24. 11. 18. 개발일지 // api vs 서비스주입, redis 캐시 본문

개발일지

24. 11. 18. 개발일지 // api vs 서비스주입, redis 캐시

Mini_96 2024. 11. 19. 14:52

* 퀴즈셋 http api 호출 vs 서비스주입 방식 비교

분석

  1. Service 주입 방식의 장점:
    • 단일 책임 원칙(SRP) 준수
    • 테스트 용이성 향상
    • 캐싱 전략 구현 가능
    • 에러 처리 통합
    • 의존성 역전 원칙(DIP) 준수
  2. HTTP 직접 호출 방식의 단점:
    • 코드 중복 가능성
    • 테스트 어려움
    • 에러 처리 분산
    • 캐싱 전략 부재

CTO들의 견해

  1. Martin Fowler (ThoughtWorks):"서비스 계층 패턴은 비즈니스 로직을 캡슐화하고 재사용성을 높이는 핵심 패턴입니다."
  2. Sam Newman (Author of Building Microservices):"마이크로서비스 환경에서 서비스 추상화는 시스템 유연성의 핵심입니다."

Best Practices

  1. 인터페이스 정의
  2. 의존성 주입 활용
  3. 캐싱 전략 구현
  4. 환경 설정 분리
  5. 에러 처리 통합

추천사항

QuizService를 주입받아 사용하는 방식을 강력히 추천합니다. 이유는:

  1. 테스트 용이성
  2. 코드 재사용
  3. 관심사 분리
  4. 확장성
  • 결론 : 퀴즈서비스를 주입받아 사용하고자 함.

  • 문제 : createRoom에서 quizSetId, quizCount를 주지않음. 그래서 하드코딩 되있구나.

  • updateRoomQuizset여기서 풀 퀴즈셋을 정하는것 같다.
  • 그러면 기본값으로 풀 퀴즈를 정해놓으면 되겠군.
  • vs 퀴즈 스타트할때 퀴즈셋아이디, 카운트가 있는지 검사해도 될것같다.

  • bug? : setId기본값이 왜 2가 들어가는거지??
  • 다시 서버 restart하니까 해결되었다. dev -watch의 버근인듯하다.

test

 

* room에 퀴즈셋 title 추가

  • 추가 요구사항이 생겼다.
  • 퀴즈셋의 title을 여기서 가지고 있어야 한다.
  • 값 주입시점은 언제가 좋을까?

  • updateRoomQuizSet 여기가 좋겠다. -> quizSet API를 호출하는 startGame 밖에 없는듯.

 

* redis 캐시 사용

  • 기존 : 캐시 확인없이 무조건 API 호출
  • 개선 : 캐시가 있으면 캐시활용 ( 로컬캐시 -> 레디스 캐시 -> DB)
import { QuizSetData } from '../../InitDB/InitDB.Service';
import { Injectable, Logger } from '@nestjs/common';
import { InjectRedis } from '@nestjs-modules/ioredis';
import { Redis } from 'ioredis';
import { QuizService } from '../../quiz/quiz.service';
import { mockQuizData } from '../../../test/mocks/quiz-data.mock';

@Injectable()
export class QuizCacheService {
  private readonly quizCache = new Map<string, any>();
  private readonly logger = new Logger(QuizCacheService.name);
  private readonly CACHE_TTL = 1000 * 60 * 30; // 30분

  constructor(
    @InjectRedis() private readonly redis: Redis,
    private readonly quizService: QuizService
  ) {}

  /**
   * 캐시키 생성
   */
  private getCacheKey(quizSetId: number): string {
    return `quizset:${quizSetId}`;
  }

  /**
   * Redis 캐시에서 퀴즈셋 조회
   */
  private async getFromRedisCache(quizSetId: number): Promise<QuizSetData | null> {
    const cacheKey = this.getCacheKey(quizSetId);
    const cachedData = await this.redis.get(cacheKey);

    if (cachedData) {
      return JSON.parse(cachedData);
    }
    return null;
  }

  /**
   * Redis 캐시에 퀴즈셋 저장
   */
  private async setToRedisCache(quizSetId: number, data: QuizSetData): Promise<void> {
    const cacheKey = this.getCacheKey(quizSetId);
    await this.redis.set(cacheKey, JSON.stringify(data), 'EX', this.CACHE_TTL);
  }

  /**
   * 퀴즈셋 데이터 조회 (캐시 활용)
   */
  async getQuizSet(quizSetId: number) {
    // 1. 로컬 메모리 캐시 확인
    const localCached = this.quizCache.get(this.getCacheKey(quizSetId));
    if (localCached) {
      this.logger.debug(`Quiz ${quizSetId} found in local cache`);
      return localCached;
    }

    // 2. Redis 캐시 확인
    const redisCached = await this.getFromRedisCache(quizSetId);
    if (redisCached) {
      this.logger.debug(`Quiz ${quizSetId} found in Redis cache`);
      // 로컬 캐시에도 저장
      this.quizCache.set(this.getCacheKey(quizSetId), redisCached);
      return redisCached;
    }

    // 3. DB에서 조회
    const quizData = quizSetId === -1 ? mockQuizData : await this.quizService.findOne(quizSetId);

    // 4. 캐시에 저장
    await this.setToRedisCache(quizSetId, quizData);
    this.quizCache.set(this.getCacheKey(quizSetId), quizData);

    return quizData;
  }

  /**
   * 캐시 무효화
   */
  async invalidateCache(quizSetId: number): Promise<void> {
    const cacheKey = this.getCacheKey(quizSetId);
    this.quizCache.delete(cacheKey);
    await this.redis.del(cacheKey);
  }
}
  • 로컬캐시를 주기적으로 비워주는 알고리즘을 또 만들어야하네? -> 일단 기본 TTL을 제공하는 레디스 캐시만사용
  • 퀴즈셋은 자주 변경되지않는데이터 -> 캐시에 적절함.
  • redis에는 아래와 같이 저장된다.

  • 먼저 로컬캐시를 뒤진후에 그래도 없으면 DB에 요청을한다.

 

 

* test code 실행

  • e2e test에서 의존성주입 엄청난 에러..

  • 원인 : gameService에서 quizCacheService를 사용하고, 이것은 quizService를 사용하고, 이것은 TypeORM을 사용하기 때문으로 추정
  • 해결 : TypeORM 등 모든 관련 의존성주입 

  • test가 안끝나고 대기중인 문제
  • 원인 : import에 있던 실제 redis가 안닫히는 걸로 추정
  • 해결 : 기존의 test는 beforeALL에서 실제 redis말고 redisMock을 사용하게 되어있음. -> 사용하지않는 실제 redis import를 제거

  • 제거후에도 test가 잘된다!

 

* test code 추가

  • 게임 시작시 quizSetTitle이 올바르게 설정되어야 한다
  • 캐시에 없는 퀴즈셋의 경우 DB에서 조회하고 캐시에 저장해야 한다.
  • 캐시에 있는 퀴즈셋의 경우 DB 조회 없이 캐시에서 가져와야 한다
  • 캐시가 만료되면 DB에서 다시 조회해야 한다