본문 바로가기
개발/프로젝트-식당 웨이팅 앱 FOOD LINE

230607 실전프로젝트9 [Node.js/Nest.js_redis 적용하기]

by 코딩하는짱구 2023. 6. 5.
반응형

✅오늘학습 Keyword

2023.06.05 - [개념정리, 유용한팁] - Redis란 무엇인가? 윈도우에서 Redis 설치하기

 

Redis란 무엇인가? 윈도우에서 Redis 설치하기

✅Redis를 써보고자 한 이유 2023.06.05 - [분류 전체보기] - 230603 실전프로젝트7 [Node.js/Nest.js_sort별 목록조회 api 보완] 프로젝트에서 user위치를 기준으로 주변 식당 목록을 조회할 때, 매번 main repositor

veritas-crystal.tistory.com

이전 글에서 Redis의 개념, 기본 작동원리를 익히고 project에 적용해보기로 했다. 

 

✅오늘 한 일 

@Injectable()
export class StoresService {
  constructor(
    @InjectRepository(StoresRepository)
    private storesRepository: StoresRepository,
    private reviewsRepository: ReviewsRepository,
  ) {}

  async searchRestaurants(
    southWestLatitude: number,
    southWestLongitude: number,
    northEastLatitude: number,
    northEastLongitude: number,
    sortBy?: 'distance' | 'name' | 'waitingCnt' | 'waitingCnt2' | 'rating',
  ): Promise<{ 근처식당목록: Stores[] }> {
    const restaurants = await this.storesRepository.findAll();

    const restaurantsWithinRadius = restaurants.filter((restaurant) => {
      const withinLatitudeRange =
        Number(restaurant.La) >= southWestLatitude &&
        Number(restaurant.La) <= northEastLatitude;
      const withinLongitudeRange =
        Number(restaurant.Ma) >= southWestLongitude &&
        Number(restaurant.Ma) <= northEastLongitude;
      // console.log(withinLatitudeRange, withinLongitudeRange);
      return withinLatitudeRange && withinLongitudeRange;
    });

    // 거리 계산 로직
    const calculateDistance = (
      source: { latitude: number; longitude: number },
      target: { latitude: number; longitude: number },
    ): number => {
      const latDiff = Math.abs(source.latitude - target.latitude);
      const lngDiff = Math.abs(source.longitude - target.longitude);
      const approximateDistance = Math.floor(
        latDiff * 111000 + lngDiff * 111000,
      );
      return approximateDistance;
    };

    //user위치에 따른 거리값을 모든 sort조건에 포함시켜준다
    const userLocation = {
      latitude: southWestLatitude,
      longitude: southWestLongitude,
    };
    restaurantsWithinRadius.forEach((restaurant) => {
      const distance = calculateDistance(userLocation, {
        latitude: Number(restaurant.La),
        longitude: Number(restaurant.Ma),
      });
      restaurant.distance = distance;
    });

    //정렬로직모음
    restaurantsWithinRadius.sort((a, b) => {
      if (sortBy === 'distance') {
        return (a.distance || 0) - (b.distance || 0);
      } else if (sortBy === 'name') {
        const nameA = a.storeName.toUpperCase();
        const nameB = b.storeName.toUpperCase();
        if (nameA < nameB) {
          return -1;
        }
        if (nameA > nameB) {
          return 1;
        }
        return 0;
      } else if (sortBy === 'waitingCnt') {
        return a.currentWaitingCnt - b.currentWaitingCnt;
      } else if (sortBy === 'waitingCnt2') {
        return b.currentWaitingCnt - a.currentWaitingCnt;
      } else if (sortBy === 'rating') {
        return b.rating - a.rating;
      }
      return 0;
    });

    return { 근처식당목록: restaurantsWithinRadius };
  }

위의 식당 목록 조회 기능에 Redis를 적용하여 쿼리결과를 캐시에 저장하면, 같은 요청이 반복될 때 마다 db에 access 하는 대신 캐시에서 결과를 가져올 수 있다. 

 

 

 

✅오늘 겪은 문제 및 해결

1. stores.service에서 getcurrentwaitingcnt 함수를 가져오려했다.

아래와같은 오류 

 

[Nest] 21292  - 2023. 06. 07. 오후 7:23:13   ERROR [ExceptionHandler] Nest can't resolve dependencies of the WaitingsRepository (?). Please make sure that the argument WaitingsRepository at index [0] is available in the StoresModule context.

Potential solutions:
- Is StoresModule a valid NestJS module?
- If WaitingsRepository is a provider, is it part of the current StoresModule?
- If WaitingsRepository is exported from a separate @Module, is that module imported within StoresModule?
  @Module({
    imports: [ /* the Module containing WaitingsRepository */ ]
  })

Error: Nest can't resolve dependencies of the WaitingsRepository (?). Please make sure that the argument WaitingsRepository at index [0] is available in the StoresModule context.

Potential solutions:
- Is StoresModule a valid NestJS module?
- If WaitingsRepository is a provider, is it part of the current StoresModule?
- If WaitingsRepository is exported from a separate @Module, is that module imported within StoresModule?
  @Module({
    imports: [ /* the Module containing WaitingsRepository */ ]
  })

    at Injector.lookupComponentInParentModules (C:\Users\cryst\OneDrive\바탕 화면\sparta\NODE JS\주특기주차\실전프로젝트\matwaiting\backend\backend-1\node_modules\@nestjs\core\injector\injector.js:247:19)
    at processTicksAndRejections (node:internal/process/task_queues:95:5)
    at Injector.resolveComponentInstance (C:\Users\cryst\OneDrive\바탕 화면\sparta\NODE JS\주특기주차\실전프로젝트\matwaiting\backend\backend-1\node_modules\@nestjs\core\injector\injector.js:200:33)
    at resolveParam (C:\Users\cryst\OneDrive\바탕 화면\sparta\NODE JS\주특기주차\실전프로젝트\matwaiting\backend\backend-1\node_modules\@nestjs\core\injector\injector.js:120:38)
    at async Promise.all (index 0)
    at Injector.resolveConstructorParams (C:\Users\cryst\OneDrive\바탕 화면\sparta\NODE JS\주특기주차\실전프로젝트\matwaiting\backend\backend-1\node_modules\@nestjs\core\injector\injector.js:135:27)
    at Injector.loadInstance (C:\Users\cryst\OneDrive\바탕 화면\sparta\NODE JS\주특기주차\실전프로젝트\matwaiting\backend\backend-1\node_modules\@nestjs\core\injector\injector.js:61:13)
    at Injector.loadProvider (C:\Users\cryst\OneDrive\바탕 화면\sparta\NODE JS\주특기주차\실전프로젝트\matwaiting\backend\backend-1\node_modules\@nestjs\core\injector\injector.js:88:9)
    at C:\Users\cryst\OneDrive\바탕 화면\sparta\NODE JS\주특기주차\실전프로젝트\matwaiting\backend\backend-1\node_modules\@nestjs\core\injector\instance-loader.js:56:13
    at async Promise.all (index 9)




해결 stores.module에 의존성을 주입, 처음엔 waitingsrepository, waitings 테이블이 주입되어있지 않았음. 
나는 waiting service의 api를 끌어오려한거라 repo는 추가 안해도되는줄 알았음.

 

nest.js에서 service와 repo에서 뭘 쓰든 module에 반드시!! 의존성을 주입해줘야한다는 것, 결과적으론 stores.module에 의존성을 주입해줬더니 해결됐다.

 

import { ElasticsearchModule } from '@nestjs/elasticsearch';
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { StoresController } from './stores.controller';
import { StoresService } from './stores.service';
import { Stores } from './stores.entity';
import { StoresRepository } from './stores.repository';
import { LocationService } from 'src/location/location.service';
import { Tables } from 'src/tables/tables.entity';
import { TablesRepository } from 'src/tables/tables.repository';
import { ReviewsRepository } from 'src/reviews/reviews.repository';
import { Reviews } from 'src/reviews/reviews.entity';
import * as dotenv from 'dotenv';
import { WaitingsService } from 'src/waitings/waitings.service';
import { BullModule } from '@nestjs/bull';
import { WaitingsRepository } from 'src/waitings/waitings.repository';
import { Waitings } from 'src/waitings/waitings.entity';
// import { RedisCacheModule } from 'src/cache/redis.module';
// import { RedisCacheService } from 'src/cache/redis.service';

dotenv.config();

@Module({
  imports: [
    BullModule.registerQueue({
      name: 'waitingQueue',
    }),
    TypeOrmModule.forFeature([Stores, Tables, Reviews, Waitings]),
    ElasticsearchModule.register({
      node: 'http://localhost:9200',
      maxRetries: 10,
      requestTimeout: 60000,
      pingTimeout: 60000,
      sniffOnStart: true,
    }),
  ],
  controllers: [StoresController],
  providers: [
    StoresService,
    LocationService,
    StoresRepository,
    TablesRepository,
    ReviewsRepository,
    WaitingsService,
    WaitingsRepository,
  ],
})
export class StoresModule {}

2. 식당목록조회에 redis 적용하기(에러지옥)

A. redis client에 접속하는 명령어로 접속을 하려했으나 실패 

redis-cli -h redis-10555.c262.us-east-1-3.ec2.cloud.redislabs.com -p 10555 -a zxXBZoDFEW3cGnFA0tYMj0edC673xHhU

 

redis-client는 분명히 설치했는데 redis-cli 명령어가 인식되지 않는다는 에러가 발생하는 경우에는 Redis 클라이언트가 시스템의 PATH 환경 변수에 등록되어 있지 않을 수 있고, 이를 해결하기 위해 다음과 같은 단계를 추천했다.

  • 다운로드한 Redis 클라이언트 MSI 파일을 실행하여 설치 프로세스를 시작합니다.
  • 설치 중 "설정 추가 옵션" 단계에서 "PATH에 Redis 바이너리 추가" 옵션을 선택합니다. 이 옵션을 선택하면 Redis 클라이언트가 시스템의 PATH 환경 변수에 자동으로 등록됩니다. 만약 이미 설치가 완료되었다면 Redis 클라이언트를 다시 설치하고 해당 옵션을 선택해 주세요.
  • 설치를 완료하고 시스템을 재시작합니다. 이렇게 하면 환경 변수 변경이 적용됩니다.
    다시 VSCode를 열고 터미널을 실행한 후 redis-cli 명령어를 실행해 보세요.

다행히 위와 같은 방법으로 해결했다. 

 

B.우선 nest.js에 redis를 어떻게 연결할 것인가 부터 차근차근 알아봤다.

  1. src/cache/redis.modules.ts에 redis module을 연결한다. 
  2. src/cache/redis.service.ts에 redis set,get 등 로직을 설정한다. 
  3. src/cache/redis.service.ts에 test 해볼 함수를 작성한다.
  4. controller로 함수를 호출한다. 

 

식당목록조회에 redis 적용이 절대로! 안됐기때문에 ..

아주 기본부터 시작해보기로했다, test data를 저장하는 함수를 만들어서 내 local redis에 연결 실험해보기로 했다.

redis-cli -h localhost -p 6379 -a your_password

KEYS "searchRestaurants*"

// src/cache/redis.module.ts
import { Module } from '@nestjs/common';
import * as redisStore from 'cache-manager-redis-store';
import * as dotenv from 'dotenv';
import { RedisCacheService } from './redis.service';
import { CacheModule } from '@nestjs/cache-manager';
import { RedisController } from './redis.controller';

dotenv.config();

@Module({
  imports: [
    CacheModule.register({
      useFactory: async () => ({
        store: redisStore,
        host: process.env.REDIS_HOST,
        port: parseInt(process.env.REDIS_PORT),
        auth_pass: process.env.REDIS_PASSWORD,
        ttl: 86400,
      }),
    }),
  ],
  providers: [RedisCacheService],

  controllers: [RedisController],
})
export class RedisCacheModule {}

 

 

// src/cache/redis.service.ts

import { Injectable, Inject } from '@nestjs/common';
import { Cache } from 'cache-manager';
import { CACHE_MANAGER } from '@nestjs/cache-manager/dist';

@Injectable()
export class RedisCacheService {
  constructor(@Inject(CACHE_MANAGER) private cache: Cache) {}

  // async getCache() {
  //   const savedTime = await this.cache.get<number>('time');
  //   if (savedTime) {
  //     return 'saved time : ' + savedTime;
  //   }
  //   const now = new Date().getTime();
  //   await this.cache.set('time', now);
  //   return 'save new time : ' + now;
  // }

  async set(key: string, value: any, option?: any) {
    await this.cache.set(key, value, option);
  }

  async get(key: string): Promise<any> {
    return await this.cache.get(key);
  }

  async reset() {
    await this.cache.reset();
  }

  async del(key: string) {
    await this.cache.del(key);
  }

  async injectTestData() {
    const testData = {
      key1: 'value1',
      key2: 'value2',
      key3: 'value3',
      key4: 'value4',
      key5: 'value5',
      key6: 'value6',
    };

    for (const key in testData) {
      if (testData.hasOwnProperty(key)) {
        await this.set(key, testData[key]);
      }
    }
    for (const key in testData) {
      if (testData.hasOwnProperty(key)) {
        const value = await this.get(key);
        console.log(`Key: ${key}, Value: ${value}`);
      }
    }
  }
}
// example.controller.ts

import { Controller, Get, Param } from '@nestjs/common';
import { RedisCacheService } from './../cache/redis.service';
import { Public } from 'src/auth/common/decorators';

@Controller('example')
export class RedisController {
  constructor(private readonly redisCacheService: RedisCacheService) {}

  @Public()
  @Get('inject-test-data')
  async injectTestData(): Promise<string> {
    await this.redisCacheService.injectTestData();
    return 'Test data injected successfully.';
  }

  @Public()
  @Get('get-key')
  async getData(): Promise<any> {
    const key = 'key1';

    const value = await this.redisCacheService.get(key);
    const data = { [key]: value };
    console.log(data);
    return data;
  }

  @Public()
  @Get('get-data')
  async getAllData(): Promise<any> {
    const keys = ['key1', 'key2', 'key3', 'key4', 'key5', 'key6'];
    const data = {};

    for (const key of keys) {
      const value = await this.redisCacheService.get(key);
      data[key] = value;
    }
    console.log(data);
    return data;
  }

  //   @Public()
  //   @Get('cache')
  //   getCache() {
  //     return this.redisCacheService.getCache();
}

 

위 코드를 실행했을 때 아래와 같이 data가 잘 생성, 저장 되는 것을 볼 수 있다. 

 

그런데.. 저장된 db를 조회하면 .. 이렇게 undefined가 뜨고,

 

redis-cli로 검색해봐도 db는 저장되어있지 않다. 

더 이상한건 db를 저장한 후 몇초안에 바로 데이터 조회 요청을 날리면, db조회가 되고, 몇초가 지나면 사라진다는 것이다.

처음에는 db에 제대로 data가 들어갔다가 삭제되는줄 알았는데 알고보니 이건 CacheModule에서 제공하는 빌트인 된  in-memory 스토리지 기능이라고 한다. 

 

**

$ npm install cache-manager

$ npm install -D @types/cache-manager

 

주어진 코드에서 Redis 서버 설정이 없는 경우 Nest.js는 기본적으로 인메모리 캐시를 사용한다고 한다. 따라서 Redis 서버에 연결하지 않고도 애플리케이션 자체는 실행할 수 있는 것이다. (●'◡'●) 신기 신기 ..

 

✅오늘 알게된 점 및 추후 학습 계획

이틀 내내 Redis 적용에 매달리면서 처음엔 맨 땅에 헤딩같고 정말 답이 안보였는데, 

오늘 드디어 어느 부분에서 문제인지를 알게되었다! 

실제로 서버와 local server, 혹은 redislab server와 연동이 되지 않고 있는 것이다. 

구글링해봤는데 내 생각엔 version 문제가 아닐까 싶다.. library의 import module들도 이름이 다 바뀌고 형식이 계속 바뀌는 것 같다. 어디가 문제인지 알았으니 내일은 그 문제를 반드시 해결해야겠다 ..☺

 

반응형