[꿀팁] nestjs에서 redis cache를 활용해 api call 최적화하기 (1)

in-ch·2024년 4월 13일
1

꿀팁

목록 보기
13/14

서론


현대 웹 개발에서 성능 최적화는 더 이상 선택사항이 아닌 필수입니다.
특히, API 호출을 최적화하여 응답 시간을 단축하고 서버 부하를 줄이는 것은 매우 중요합니다.

이번에 NestJSRedis를 결합하여 API 호출을 최적화해보도록 하겠습니다.

캐싱이란?



컴퓨팅에서 캐시는 일반적으로 데이터 하위 집합을 저장하는 고속 데이터 스토리지 계층입니다.
이를 활용해 클라이언트에서 동일한 요청이 있을 경우 DB에 접근하는 것보다 더 빠르게 요청을 처리할 수 있습니다.

캐싱된 데이터는 RAM(Random Access Memory)과 같이 빠르게 엑세스할 수 있는 하드웨어에 저장됩니다.

캐싱이 왜 필요할까요?

서비스의 사용자 수가 증가함에 따라 서버로 부터 많은 HTTP 요청을 받게 됩니다.
만약 여러 사용자가 동일한 요청을 계속 보내면 어떻게 될까요?

이는 병목 현상을 초래할 수 있습니다.

또한 일반적으로 관계형 데이터베이스는 구조화된 데이터를 다룸에 따라 신뢰성은 높으나 속도에 최적화되어 있지는 않습니다. 따라서 캐시를 활용해 동일한 여러 응답에 대한 최적화를 진행하면 더 빠른 응답을 할 수 있게 됩니다.

예제 프로젝트 생성하기


먼저 Nest.js 프로젝트를 생성하여

프로젝트 생성하기

nest new redis-nest-test

user module 새로 만들기

nest g mo user

user

user 폴더 안에 user.controller.ts 파일을 생성하고 다음과 같이 작성합니다.

import { Controller, Get } from '@nestjs/common';

function generateRandomName() {
  const alphabet = 'abcdefghijklmnopqrstuvwxyz';
  let name = '';
  for (let i = 0; i < 4; i++) {
    name += alphabet[Math.floor(Math.random() * alphabet.length)];
  }
  return name;
}

function generateRandomEmail(name) {
  const domains = ['example.com', 'test.com', 'mail.com', 'company.com'];
  const randomNumber = Math.floor(Math.random() * 1000);
  return `${name}${randomNumber}@${domains[Math.floor(Math.random() * domains.length)]}`;
}

@Controller('user')
export class UserController {
  constructor() {}

  @Get()
  getUser() {
    const data = [];

    for (let i = 0; i <= 100000; i++) {
      const name = generateRandomName();
      const email = generateRandomEmail(name);
      data.push({ id: i, email, name });
    }

    return new Promise((resolve, _) => {
      setTimeout(() => {
        resolve(data);
      }, 2000);
    });
  }
}

예제를 간단히 설명드리자면 다음과 같습니다.

  • '/user'로 Get 요청이 들어오면 랜덤한 유저를 100,000개 생성하여 응답하는 예제입니다.
  • api 호출 시간을 테스트하기 위해 일부로 2s의 응답 시간을 지연시켰습니다.
  • 원래 비즈니스 로직을 service layer에 분리시켜야 하지만 테스트를 위한 거니 일단 controller layer에서 테스트해보도록 하겠습니다.

이제 user.module.tsuser.controller.ts를 import하도록 합니다.

import { Module } from '@nestjs/common';
import { UserController } from './user.controller';

@Module({
  imports: [],
  controllers: [UserController],
  providers: [],
})
export class UserModule {}
  • 여기까지의 프로젝트 구조
    in-ch
  • 테스트 결과
    테스트 결과

위의 테스트 결과에서 확인해보면 2.29s 정도 소요된 것을 확인해볼 수 있습니다.

캐싱 적용하기


캐시를 활용해 이제 최적화해보도록 하겠습니다.

먼저 첫 시작으로 NestJs에서 제공하는 cache-manager를 구현하는 것부터 시작하겠습니다. 그러면 캐시가 서버의 RAM에 저장될 것입니다.
그 후 확장 가능한 캐싱 솔루션을 위해 Redis를 활용할 겁니다.

캐싱을 구현하는 방법에는 UseInterceptorscache-manager를 활용하는 방법이 있습니다.
먼저 간단한 UseInterceptors를 먼저 확인해보도록 하겠습니다.

필요 패키지 설치

yarn add @nestjs/cache-manager cache-manager

UseInterceptors 데코레이터 활용하기

  • app.module.ts
import { CacheModule } from '@nestjs/cache-manager';
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserModule } from './user/user.module';

@Module({
  imports: [
    UserModule,
    CacheModule.register({
      isGlobal: true,
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

한가짐 집고 넘어갈 점은 isGlobaltrue로 설정하는 겁니다.
이렇게 하면 특정 서비스나 컨트롤러에서 사용하는 경우 캐싱 모듈을 다시 가져오지 않아도 됩니다.

  • user.controller.ts
import { CacheInterceptor } from '@nestjs/cache-manager';
import { Controller, Get, UseInterceptors } from '@nestjs/common';

function generateRandomName() {
  const alphabet = 'abcdefghijklmnopqrstuvwxyz';
  let name = '';
  for (let i = 0; i < 4; i++) {
    name += alphabet[Math.floor(Math.random() * alphabet.length)];
  }
  return name;
}

function generateRandomEmail(name) {
  const domains = ['example.com', 'test.com', 'mail.com', 'company.com'];
  const randomNumber = Math.floor(Math.random() * 1000);
  return `${name}${randomNumber}@${domains[Math.floor(Math.random() * domains.length)]}`;
}

@Controller('user')
export class UserController {
  constructor() {}

  @UseInterceptors(CacheInterceptor) // 이거 추가
  @Get()
  getUser() {
    const data = [];

    for (let i = 0; i <= 100000; i++) {
      const name = generateRandomName();
      const email = generateRandomEmail(name);
      data.push({ id: i, email, name });
    }

    return new Promise((resolve, _) => {
      setTimeout(() => {
        resolve(data);
      }, 2000);
    });
  }
}

UseInterceptors 테스트 해보기

  • 먼저 첫번째 호출에는 2s 이상이 걸리는 것을 확인해 볼 수 있습니다.
  • 그 이후 호출의 경우 100ms 이하의 매우 빠른 응답 속도를 확인해 볼 수 있습니다.

cache-manager 활용하기

간단한 캐싱의 경우 UseInterceptors를 활용하면 됩니다.
하지만 만약 응답한 서버의 시간을 리턴해야 하는 상황이 발생하면 어떡할까요?

이럴 경우 캐시의 내용을 수정해야 합니다.
cache-manager를 활용하면 약간의 오버헤드로 훨씬 더 강력한 유연성을 제공합니다.

  @Get('/man')
  async getMans() {
    const cachedData = await this.cacheManager.get('man');
    if (cachedData)
      return {
        currentTime: new Date().toString(),
        data: cachedData,
      };
    const data = [];

    for (let i = 0; i <= 100; i++) {
      const name = generateRandomName();
      const email = generateRandomEmail(name);
      data.push({ id: i, email, name });
    }

    this.cacheManager.set('man', data);

    return new Promise((resolve, _) => {
      setTimeout(() => {
        resolve({
          currentTime: new Date().toString(),
          data,
        });
      }, 2000);
    });
  }

cache-manager 테스트

cache-manager 테스트

  • 응답 속도는 빨라진 상태에서 current data 만 달라진 것을 확인해 볼 수 있습니다.

cache-manager의 다양한 옵션

예제에서는 get, set만 사용하였지만 더 다양한 옵션이 있습니다.


// 캐시 멀티 set
await multiCache.mset(
  [
    ['foo', 'bar'],
    ['foo2', 'bar2'],
  ],
  ttl
);

// 캐시 멀티 가져오기 
console.log(await multiCache.mget('key', 'key2'));

// 캐시 삭제
await multiCache.del('foo2');

// 캐시 멀티 삭제
await multiCache.mdel('foo', 'foo2');

마무리


이상으로 UseInterceptorscache-manager를 활용해서 간단하게 캐시를 구현해봤습니다.
하지만 아직 서버 컴퓨터의 RAM에 저장됩니다.

다음 편에 이어서 redis를 활용하는 법을 알아보도록 하겠습니다.

감사합니다.

이어서 계속...

2편 보러가기

참고

캐싱 개요

profile
인치

0개의 댓글