diff --git a/config/artillery.test.yaml b/config/artillery.test.yaml new file mode 100644 index 0000000..3770e4a --- /dev/null +++ b/config/artillery.test.yaml @@ -0,0 +1,36 @@ +# config: +# target: 'http://localhost:8080' +# phases: +# - duration: 60 +# arrivalRate: 10 +# - duration: 120 +# arrivalRate: 20 +# - duration: 180 +# arrivalRate: 30 + +# scenarios: +# - flow: +# - get: +# url: '/posts/test' +# qs: +# page: '{{ $randomNumber(1,5) }}' +# headers: +# Accept: 'application/json' +config: + target: 'http://localhost:8080' + phases: + - duration: 60 + arrivalRate: 10 + - duration: 120 + arrivalRate: 20 + - duration: 180 + arrivalRate: 30 + +scenarios: + - flow: + - get: + url: '/posts/optimized-test' # 최적화된 엔드포인트 + qs: + page: '{{ $randomNumber(1,5) }}' + headers: + Accept: 'application/json' diff --git a/config/prometheus-config.yaml b/config/prometheus-config.yaml index e33aef1..d7b76dd 100644 --- a/config/prometheus-config.yaml +++ b/config/prometheus-config.yaml @@ -4,7 +4,7 @@ scrape_configs: - job_name: 'prometheus' scrape_interval: 1m static_configs: - - targets: ['localhost:9090'] + - targets: ['prometheus:9090'] - job_name: 'node' static_configs: diff --git a/docker-compose.yaml b/docker-compose.yaml index 2825d40..c8f71be 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -18,6 +18,7 @@ services: POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: ${POSTGRES_DB} + TZ: Asia/Seoul networks: - loki @@ -58,8 +59,8 @@ services: - ./grafana-data:/var/lib/grafana restart: always environment: - - GF_SECURITY_ADMIN_USER=${GF_USER} - - GF_SECURITY_ADMIN_PASSWORD=${GF_PASSWORD} + GF_SECURITY_ADMIN_USER: ${GF_USER} + GF_SECURITY_ADMIN_PASSWORD: ${GF_PASSWORD} env_file: - .env networks: @@ -97,7 +98,7 @@ services: - loki postgres_exporter: - image: wrouesnel/postgres_exporter:latest + image: bitnami/postgres-exporter:latest container_name: postgres-exporter environment: DATA_SOURCE_NAME: 'postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres/${POSTGRES_DB}?sslmode=disable' diff --git a/package-lock.json b/package-lock.json index 08383c6..f724e20 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@aws-sdk/client-s3": "^3.583.0", "@nestjs/axios": "^3.0.2", + "@nestjs/cache-manager": "^2.2.2", "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.2.2", "@nestjs/core": "^10.0.0", @@ -32,6 +33,8 @@ "aws-sdk": "^2.1628.0", "bcrypt": "^5.1.1", "body-parser": "^1.20.2", + "cache-manager": "^5.7.4", + "cache-manager-redis-yet": "^5.1.3", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "cookie-parser": "^1.4.6", @@ -4013,6 +4016,18 @@ "rxjs": "^6.0.0 || ^7.0.0" } }, + "node_modules/@nestjs/cache-manager": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@nestjs/cache-manager/-/cache-manager-2.2.2.tgz", + "integrity": "sha512-+n7rpU1QABeW2WV17Dl1vZCG3vWjJU1MaamWgZvbGxYE9EeCM0lVLfw3z7acgDTNwOy+K68xuQPoIMxD0bhjlA==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^9.0.0 || ^10.0.0", + "@nestjs/core": "^9.0.0 || ^10.0.0", + "cache-manager": "<=5", + "rxjs": "^7.0.0" + } + }, "node_modules/@nestjs/cli": { "version": "10.3.2", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.3.2.tgz", @@ -5988,9 +6003,10 @@ } }, "node_modules/@redis/client": { - "version": "1.5.16", - "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.5.16.tgz", - "integrity": "sha512-X1a3xQ5kEMvTib5fBrHKh6Y+pXbeKXqziYuxOUo1ojQNECg4M5Etd1qqyhMap+lFUOAh8S7UYevgJHOm4A+NOg==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.0.tgz", + "integrity": "sha512-aR0uffYI700OEEH4gYnitAnv3vzVGXCFvYfdpu/CJKvk4pHfLPEy/JSZyrpQ+15WhXe1yJRXLtfQ84s4mEXnPg==", + "license": "MIT", "dependencies": { "cluster-key-slot": "1.1.2", "generic-pool": "3.9.0", @@ -6014,25 +6030,28 @@ } }, "node_modules/@redis/json": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.6.tgz", - "integrity": "sha512-rcZO3bfQbm2zPRpqo82XbW8zg4G/w4W3tI7X8Mqleq9goQjAGLL7q/1n1ZX4dXEAmORVZ4s1+uKLaUOg7LrUhw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.7.tgz", + "integrity": "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==", + "license": "MIT", "peerDependencies": { "@redis/client": "^1.0.0" } }, "node_modules/@redis/search": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.6.tgz", - "integrity": "sha512-mZXCxbTYKBQ3M2lZnEddwEAks0Kc7nauire8q20oA0oA/LoA+E/b5Y5KZn232ztPb1FkIGqo12vh3Lf+Vw5iTw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.2.0.tgz", + "integrity": "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==", + "license": "MIT", "peerDependencies": { "@redis/client": "^1.0.0" } }, "node_modules/@redis/time-series": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.5.tgz", - "integrity": "sha512-IFjIgTusQym2B5IZJG3XKr5llka7ey84fw/NOYqESP5WUfQs9zz1ww/9+qoz4ka/S6KcGBodzlCeZ5UImKbscg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.1.0.tgz", + "integrity": "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==", + "license": "MIT", "peerDependencies": { "@redis/client": "^1.0.0" } @@ -10078,6 +10097,52 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true }, + "node_modules/cache-manager": { + "version": "5.7.4", + "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-5.7.4.tgz", + "integrity": "sha512-7B29xK1D8hOVdrP0SAy2DGJ/QZxy2TqxS8s2drlLGYI/xOTSJmXfatks7aKKNHvXN6SnKnPtYCi0T82lslB3Fw==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.1", + "lodash.clonedeep": "^4.5.0", + "lru-cache": "^10.2.2", + "promise-coalesce": "^1.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/cache-manager-redis-yet": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/cache-manager-redis-yet/-/cache-manager-redis-yet-5.1.3.tgz", + "integrity": "sha512-V/IcEBqNQkwlPz/p4AsJLCBFmv3KVasQPSuQZx4ykPCbBm3ybIDoqf6dXuOKaBwIk7CQwoNxTR8HJ5Bssv1iYg==", + "license": "MIT", + "dependencies": { + "@redis/bloom": "^1.2.0", + "@redis/client": "^1.5.17", + "@redis/graph": "^1.1.1", + "@redis/json": "^1.0.6", + "@redis/search": "^1.1.6", + "@redis/time-series": "^1.0.5", + "cache-manager": "^5.7.2", + "redis": "^4.6.15" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/cache-manager/node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/cache-manager/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, "node_modules/cacheable-lookup": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", @@ -14081,8 +14146,7 @@ "node_modules/ip-address/node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "devOptional": true + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" }, "node_modules/ipaddr.js": { "version": "1.9.1", @@ -15595,6 +15659,12 @@ "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", "devOptional": true }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "license": "MIT" + }, "node_modules/lodash.defaults": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", @@ -16675,14 +16745,6 @@ "winston": "^3.0.0" } }, - "node_modules/nest": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/nest/-/nest-0.1.6.tgz", - "integrity": "sha512-21378J/6pHB9EZMId5yz9DaxVj778pCHhzfXhRLLrLWZydpLgbsyxL5B5QyTSWJBk5L8XbiGgMvNZMb/qBySYg==", - "engines": { - "node": "*" - } - }, "node_modules/netmask": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", @@ -18099,6 +18161,15 @@ "node": ">=10" } }, + "node_modules/promise-coalesce": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/promise-coalesce/-/promise-coalesce-1.1.2.tgz", + "integrity": "sha512-zLaJ9b8hnC564fnJH6NFSOGZYYdzrAJn2JUUIwzoQb32fG2QAakpDNM+CZo1km6keXkRXRM+hml1BFAPVnPkxg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=16" + } + }, "node_modules/promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", @@ -18660,16 +18731,20 @@ } }, "node_modules/redis": { - "version": "4.6.14", - "resolved": "https://registry.npmjs.org/redis/-/redis-4.6.14.tgz", - "integrity": "sha512-GrNg/e33HtsQwNXL7kJT+iNFPSwE1IPmd7wzV3j4f2z0EYxZfZE7FVTmUysgAtqQQtg5NXF5SNLR9OdO/UHOfw==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/redis/-/redis-4.7.0.tgz", + "integrity": "sha512-zvmkHEAdGMn+hMRXuMBtu4Vo5P6rHQjLoHftu+lBqq8ZTA3RCVC/WzD790bkKKiNFp7d5/9PcSD19fJyyRvOdQ==", + "license": "MIT", + "workspaces": [ + "./packages/*" + ], "dependencies": { "@redis/bloom": "1.2.0", - "@redis/client": "1.5.16", + "@redis/client": "1.6.0", "@redis/graph": "1.1.1", - "@redis/json": "1.0.6", - "@redis/search": "1.1.6", - "@redis/time-series": "1.0.5" + "@redis/json": "1.0.7", + "@redis/search": "1.2.0", + "@redis/time-series": "1.1.0" } }, "node_modules/redis-commands": { @@ -19718,11 +19793,6 @@ "node": ">= 10.x" } }, - "node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" - }, "node_modules/sqs-consumer": { "version": "5.8.0", "resolved": "https://registry.npmjs.org/sqs-consumer/-/sqs-consumer-5.8.0.tgz", @@ -20570,6 +20640,12 @@ "b4a": "^1.6.4" } }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", diff --git a/package.json b/package.json index bc08b95..f96efea 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "dependencies": { "@aws-sdk/client-s3": "^3.583.0", "@nestjs/axios": "^3.0.2", + "@nestjs/cache-manager": "^2.2.2", "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.2.2", "@nestjs/core": "^10.0.0", @@ -43,6 +44,8 @@ "aws-sdk": "^2.1628.0", "bcrypt": "^5.1.1", "body-parser": "^1.20.2", + "cache-manager": "^5.7.4", + "cache-manager-redis-yet": "^5.1.3", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "cookie-parser": "^1.4.6", diff --git a/src/app.module.ts b/src/app.module.ts index 5d926a1..181e226 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -56,16 +56,6 @@ const typeOrmModuleOptions = { ChatModule, RedisModule, NewsModule, - ClientsModule.register([ - { - name: 'REDIS_SERVICE', - transport: Transport.REDIS, - options: { - host: 'localhost', - port: 6379, - }, - }, - ]), NotificationModule, ], providers: [AppService], diff --git a/src/post/post.controller.ts b/src/post/post.controller.ts index acc9c1e..2e95876 100644 --- a/src/post/post.controller.ts +++ b/src/post/post.controller.ts @@ -9,6 +9,7 @@ import { Query, ValidationPipe, UseInterceptors, + Inject, } from '@nestjs/common'; import { PostService } from './post.service'; import { CreatePostDto } from './dto/create-post.dto'; @@ -26,7 +27,19 @@ export class PostController { private readonly postService: PostService, private readonly s3Service: S3Service, ) {} + @Get('/test') + async search(@Query('page') page: number = 1) { + // const posts = await this.postService.posts(page); + const posts = await this.postService.posts(); + return posts; + } + @Get('/optimized-test') + async optimizedSearch(@Query('page') page: number = 1) { + const posts = await this.postService.optimizedPosts(page); + + return posts; + } // region별 조회 @Get('/region/:regionId') @ApiParam({ type: 'number', name: 'regionId', example: 1 }) diff --git a/src/post/post.module.ts b/src/post/post.module.ts index b5d7191..fdcd23b 100644 --- a/src/post/post.module.ts +++ b/src/post/post.module.ts @@ -16,12 +16,14 @@ import { UserModule } from 'src/user/user.module'; import { DetectiveOffice } from 'src/office/entities/detective-office.entity'; import { JwtModule } from '@nestjs/jwt'; import { File } from 'src/s3/entities/s3.entity'; +import { RedisModule } from 'src/redis/redis.module'; @Module({ imports: [ S3Module, AuthModule, UserModule, + RedisModule, TypeOrmModule.forFeature([ DetectivePost, Region, diff --git a/src/post/post.service.ts b/src/post/post.service.ts index 1ef1da4..b23d17f 100644 --- a/src/post/post.service.ts +++ b/src/post/post.service.ts @@ -1,7 +1,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { CreatePostDto } from './dto/create-post.dto'; import { DetectivePost } from './entities/detective-post.entity'; -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { DataSource, Repository } from 'typeorm'; import { Region } from './entities/region.entity'; import { Career } from './entities/career.entity'; @@ -15,6 +15,8 @@ import { S3Service } from '../s3/s3.service'; import { RegionEnum } from './type/region.type'; import * as AWS from 'aws-sdk'; import { File } from 'src/s3/entities/s3.entity'; +import { CACHE_MANAGER } from '@nestjs/cache-manager'; +import { Cache } from 'cache-manager'; @Injectable() export class PostService { @@ -31,10 +33,66 @@ export class PostService { @InjectRepository(File) private readonly fileRepo: Repository, + @Inject(CACHE_MANAGER) private cacheManager: Cache, ) { this.lambda = new AWS.Lambda(); } + async posts() { + const posts = await this.detectivePostRepo + .createQueryBuilder('detectivePost') + .leftJoinAndSelect('detectivePost.detective', 'detective') + .leftJoinAndSelect('detective.user', 'user') + .leftJoinAndSelect('detective.detectiveOffice', 'office') + .select([ + 'detectivePost.id', + 'detectivePost.categoryId', + 'detectivePost.regionId', + 'user.name', + 'office.name', + ]) + .getRawMany(); + + return posts; + } + async optimizedPosts( + page: number, + ): Promise<{ posts: Partial[]; totalCount: number }> { + const pageSize = 20; + const skip = (page - 1) * pageSize; + const cacheKey = `posts_page_${page}`; + + // 캐시에서 데이터 가져오기 + const cachedData = await this.cacheManager.get<{ + posts: Partial[]; + totalCount: number; + }>(cacheKey); + if (cachedData) { + return cachedData; + } + + // 캐시가 없으면 데이터베이스에서 가져오기 + const [posts, totalCount] = await this.detectivePostRepo + .createQueryBuilder('detectivePost') + .leftJoinAndSelect('detectivePost.detective', 'detective') + .leftJoinAndSelect('detective.user', 'user') + .leftJoinAndSelect('detective.detectiveOffice', 'office') + .select([ + 'detectivePost.id', + 'detectivePost.categoryId', + 'detectivePost.regionId', + 'user.name', + 'office.name', + ]) + .skip(skip) + .take(pageSize) + .getManyAndCount(); + + // 캐시에 데이터 저장 + const result = { posts, totalCount }; + await this.cacheManager.set(cacheKey, result); // 1시간 동안 캐시 유지 + return result; + } //! 출력값 타입 손 봐야함 // 지역별 조회 diff --git a/src/redis/redis.module.ts b/src/redis/redis.module.ts index 396534b..2f6d4bf 100644 --- a/src/redis/redis.module.ts +++ b/src/redis/redis.module.ts @@ -2,10 +2,24 @@ import { Module } from '@nestjs/common'; import { RedisIoAdapter } from './redis-io.adapter'; import { ClientsModule, Transport } from '@nestjs/microservices'; import { RedisController } from './redis.controller'; +import { CacheModule, CacheStore } from '@nestjs/cache-manager'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { redisStore } from 'cache-manager-redis-yet'; +import { RedisClientOptions } from 'redis'; @Module({ imports: [ RedisIoAdapter, + CacheModule.registerAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: async (configService: ConfigService) => ({ + store: redisStore as unknown as CacheStore, + host: configService.get('REDIS_HOST'), + port: configService.get('REDIS_PORT'), + }), + isGlobal: true, + }), ClientsModule.register([ { name: 'REDIS_SERVICE', @@ -19,6 +33,6 @@ import { RedisController } from './redis.controller'; ], controllers: [RedisController], providers: [RedisIoAdapter], - exports: [RedisIoAdapter], + exports: [RedisIoAdapter, ClientsModule, CacheModule], }) export class RedisModule {} diff --git a/src/user/entities/user.entity.ts b/src/user/entities/user.entity.ts index fc98636..39e33a7 100644 --- a/src/user/entities/user.entity.ts +++ b/src/user/entities/user.entity.ts @@ -21,7 +21,7 @@ export class User { @PrimaryGeneratedColumn('increment', { type: 'bigint', unsigned: true }) id: number; - @Column({ type: 'varchar', length: 8, nullable: false }) + @Column({ type: 'varchar', length: 15, nullable: false }) name: string; @Index('user_email_index')