현대 웹 개발에서 성능 최적화는 더 이상 선택사항이 아닌 필수입니다.
특히, API 호출을 최적화하여 응답 시간을 단축하고 서버 부하를 줄이는 것은 매우 중요합니다.
이번에 NestJS
와 Redis
를 결합하여 API 호출을 최적화해보도록 하겠습니다.
컴퓨팅에서 캐시는 일반적으로 데이터 하위 집합을 저장하는 고속 데이터 스토리지 계층입니다.
이를 활용해 클라이언트에서 동일한 요청이 있을 경우 DB에 접근하는 것보다 더 빠르게 요청을 처리할 수 있습니다.
캐싱된 데이터는 RAM(Random Access Memory)과 같이 빠르게 엑세스할 수 있는 하드웨어에 저장됩니다.
서비스의 사용자 수가 증가함에 따라 서버로 부터 많은 HTTP 요청을 받게 됩니다.
만약 여러 사용자가 동일한 요청을 계속 보내면 어떻게 될까요?
이는 병목 현상을 초래할 수 있습니다.
또한 일반적으로 관계형 데이터베이스는 구조화된 데이터를 다룸에 따라 신뢰성은 높으나 속도에 최적화되어 있지는 않습니다. 따라서 캐시를 활용해 동일한 여러 응답에 대한 최적화를 진행하면 더 빠른 응답을 할 수 있게 됩니다.
먼저 Nest.js
프로젝트를 생성하여
nest new redis-nest-test
nest g mo 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개 생성하여 응답하는 예제입니다.이제 user.module.ts
에 user.controller.ts
를 import하도록 합니다.
import { Module } from '@nestjs/common';
import { UserController } from './user.controller';
@Module({
imports: [],
controllers: [UserController],
providers: [],
})
export class UserModule {}
위의 테스트 결과에서 확인해보면 2.29s
정도 소요된 것을 확인해볼 수 있습니다.
캐시를 활용해 이제 최적화해보도록 하겠습니다.
먼저 첫 시작으로 NestJs에서 제공하는 cache-manager
를 구현하는 것부터 시작하겠습니다. 그러면 캐시가 서버의 RAM에 저장될 것입니다.
그 후 확장 가능한 캐싱 솔루션을 위해 Redis
를 활용할 겁니다.
캐싱을 구현하는 방법에는 UseInterceptors
및 cache-manager
를 활용하는 방법이 있습니다.
먼저 간단한 UseInterceptors
를 먼저 확인해보도록 하겠습니다.
yarn add @nestjs/cache-manager cache-manager
UseInterceptors
데코레이터 활용하기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 {}
한가짐 집고 넘어갈 점은
isGlobal
을true
로 설정하는 겁니다.
이렇게 하면 특정 서비스나 컨트롤러에서 사용하는 경우 캐싱 모듈을 다시 가져오지 않아도 됩니다.
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
테스트 해보기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
의 다양한 옵션예제에서는 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');
이상으로 UseInterceptors
와 cache-manager
를 활용해서 간단하게 캐시를 구현해봤습니다.
하지만 아직 서버 컴퓨터의 RAM에 저장됩니다.
다음 편에 이어서 redis
를 활용하는 법을 알아보도록 하겠습니다.
감사합니다.
이어서 계속...
참고