체크리스트

추상 클래스 적용을 위해 미리 결정한 체크리스트는 다음과 같습니다.

  1. 독립된 캐시 관리: 각 서비스는 독립된 캐시를 가집니다. 이는 서비스 별로 요청의 빈도가 다르기 때문에 각각의 서비스가 효율적으로 자신의 캐시를 관리할 수 있도록 하기 위함입니다.
  2. 공통된 임시 데이터베이스 사용: 모든 서비스는 공통된 temporaryDatabase를 사용합니다. 이를 통해 서비스 간 데이터의 일관성을 유지하고, 필요한 데이터를 효율적으로 관리하도록 합니다.
  3. 데이터 탐색 순서: 데이터를 탐색할 때는 먼저 캐시를 조회한 후, 그 다음으로 temporaryDatabase를 조회하고, 마지막으로 실제 데이터베이스를 조회합니다. 이를 통해 가장 빠른 응답 속도를 제공하면서도 데이터의 일관성을 유지합니다.

적용내역

abstract BaseService

export abstract class BaseService<T extends HasUuid> {
  protected cache: LRUCache;
  protected className: string;
  protected field: string;
  protected prisma: PrismaServiceMySQL | PrismaServiceMongoDB;
  protected temporaryDatabaseService: TemporaryDatabaseService;

  constructor(options: BaseServiceOptions) {
    this.cache = new LRUCache(options.cacheSize);
    this.className = options.className;
    this.field = options.field;
    this.prisma = options.prisma;
    this.temporaryDatabaseService = options.temporaryDatabaseService;
  }

  abstract generateKey(data: T): string;
  
  create()
  findOne()
  update()
  remove()
  ...
}

데이터 Get 요청시 처리 방식

async findOne(key: string) {
  const data = await this.getDataFromCacheOrDB(key);
  const deleteCommand = this.temporaryDatabaseService.get(
    this.className,
    key,
    'delete',
  );
  if (deleteCommand) {
    throw new HttpException('Not Found', HttpStatus.NOT_FOUND);
  }
  if (data) {
    const mergedData = this.mergeWithUpdateCommand(data, key);
    this.cache.put(key, mergedData);
    return ResponseUtils.createResponse(HttpStatus.OK, mergedData);
  } else {
    throw new HttpException('Not Found', HttpStatus.NOT_FOUND);
  }
}

async getDataFromCacheOrDB(key: string): Promise<T | null> {
  if (!key) throw new HttpException('Bad Request', HttpStatus.BAD_REQUEST);
  const cacheData = this.cache.get(key);
  if (cacheData) return cacheData;
  const temporaryDatabaseData = this.temporaryDatabaseService.get(
    this.className,
    key,
    'insert',
  );
  if (temporaryDatabaseData) return temporaryDatabaseData;
  const databaseData = await this.prisma[this.className].findUnique({
    where: {
      [this.field]: key.includes('+') ? this.stringToObject(key) : key,
    },
  });
  return databaseData;
}

private mergeWithUpdateCommand(data: T, key: string): T {
  const updateCommand = this.temporaryDatabaseService.get(
    this.className,
    key,
    'update',
  );
  if (updateCommand) return { ...data, ...updateCommand.value };

  return data;
}

추상 클래스 적용 방식

@Injectable()
export class UsersService extends BaseService<UpdateUserDto> {
  constructor(
    protected prisma: PrismaServiceMySQL,
    protected temporaryDatabaseService: TemporaryDatabaseService,
  ) {
    super({
      prisma,
      temporaryDatabaseService,
      cacheSize: USER_CACHE_SIZE,
      className: 'USER_TB',
      field: 'email_provider',
    });
  }

  generateKey(data: UpdateUserDto) {
    return `email:${data.email}+provider:${data.provider}`;
  }
}