1. Interceptor ์๊ฐ

- ๊ทธ๋ฆผ์ ์ ๋ณด๋ฉด Interceptor๊ฐ ์์ฒญํ ๋๋ ์๊ณ , ์๋ตํ ๋๋ 2๊ฐ๊ฐ ์๋ค.
- ์ฆ, Interceptor๋ฅผ ์ฌ์ฉํ๋ฉด, ์์ฒญ์ ๋ณ๊ฒฝํ ์๋ ์๊ณ , ์๋ต์ ๋ณ๊ฒฝํ ์๋ ์๋ค.

Interceptor ๋?
- ์ธํฐ์
ํฐ๋
@Injectable()๋ฐ์ฝ๋ ์ดํฐ๋ก ์ฃผ์์ด ๋ฌ๋ฆฐ ํด๋์ค๋ค. NestInterceptor์ธํฐํ์ด์ค๋ฅผ ์์(implements)๋ฐ๋๋ค.- NestJS์์ ์ ์ผํ๊ฒ ์์ฒญ์ด ๋ค์ด์ฌ ๋ ๊ทธ๋ฆฌ๊ณ ์๋ต์ด ๋๊ฐ๋ ๋ชจ๋ ๋ก์ง์ ์คํ ํ ์ ์๋ ๋ฏธ๋ค์จ์ด๋ค
- ์ถ๊ฐ์ ์ผ๋ก 1๊ฐ์ Interceptor๋ก ์์ฒญ๊ณผ ๋ณ๊ฒฝ์ ๋ชจ๋ ์ ์ดํ ์ ์๋ค.
- ๊ณต์๋ฌธ์ : https://docs.nestjs.com/interceptors
Interceptors์๋ AOP(Aspect Oriented Programming) ๊ธฐ์ ์์ ์๊ฐ์ ๋ฐ์ ์ผ๋ จ์ ์ ์ฉํ ๊ธฐ๋ฅ์ด ์์ต๋๋ค. ์ด๋ฅผ ํตํด ๋ค์์ ์ํํ ์ ์์ต๋๋ค.
- ๋ฉ์๋๋ฅผ ์คํํ๊ธฐ ์ , ํ์ ์ถ๊ฐ ๋ก์ง์ ์์ฑํ ์ ์๋ค. (bind extra logic)
- ํจ์์์ ๋ฐํ๋ ๊ฒฐ๊ณผ๋ฅผ ๋ณํํ ์ ์๋ค.
- ํจ์์์ ๋์ง ์๋ฌ๋ฅผ ๋ณํํ ์ ์๋ค.
- ๊ธฐ๋ณธ์ผ๋ก ์์ฑํ ํจ์ ๋ก์ง์๋ค ์ถ๊ฐ ๊ธฐ๋ฅ์ ๋ฃ์ ์ ์๋ค.
- (extend the basic function behavior)
- ์ด๋ค ํจ์์ ๊ธฐ๋ฅ์ ์์ ํ ์ค๋ฒ๋ผ์ด๋ ํ ์ ์๋ค. (e.g. ์บ์ฑ ๋ชฉ์ ์ผ๋ก)
๊ฒฐ๋ก ์ Interceptor๋ ์์ฒญ๊ณผ ์๋ต์ ๋ชจ๋ ํธ๋ค๋งํ ์ ์๊ธฐ์, ์ํ๋ค๋ฉด Interceptor๋ฅผ ์ ์ฉํ ํจ์์ ๋ชจ๋ ์ ์ฒด ๊ธฐ๋ฅ์ ๋ค ๋ณ๊ฒฝ์ ํ ์๋ ์๋ค.
Interceptor Response ํธ๋ค๋ง์ ๊ธฐ๋ณธ์ ์ผ๋ก RxJS๋ฅผ ์ฌ์ฉํ๋ค
1.1 Interceptor ๊ตฌํ๋ฐฉ๋ฒ
1import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'2import { Observable } from 'rxjs'3import { tap } from 'rxjs/operators'45@Injectable()6export class LoggingInterceptor implements NestInterceptor {7intercept(context: ExecutionContext, next: CallHandler): Observable<any> {8console.log('Before...')910const now = Date.now()11return next12.handle() //13.pipe(tap(() => console.log(`After... ${Date.now() - now}ms`)))14}15}
2. Interceptor๋ฅผ ์ด์ฉํด ๋ก๊ฑฐ ๊ตฌํ
common/interceptor/log.interceptor.ts ํ์ผ์ ๋ง๋ ๋ค.
1import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'2import { Observable, tap } from 'rxjs'34@Injectable()5export class LogInterceptor implements NestInterceptor {6intercept(context: ExecutionContext, next: CallHandler<any>): Observable<any> {7/**8* ์์ฒญ์ด ๋ค์ด์ฌ๋ REQ ์์ฒญ์ด ๋ค์ด์จ ํ์์คํฌํ๋ฅผ ์ฐ๋๋ค.9* [REQ] {์์ฒญ path} {์์ฒญ ์๊ฐ}10*11* ์์ฒญ์ด ๋๋ ๋ (์๋ต์ด ๋๊ฐ๋) ๋ค์ ํ์์คํฌํ๋ฅผ ์ฐ๋๋ค.12* [RES] {์์ฒญ path} {์๋ต ์๊ฐ} {์ผ๋ง๋ ๊ฑธ๋ ธ๋์ง ms}13*/14const now = new Date()1516const req = context.switchToHttp().getRequest()1718/***19* /posts20* /common/image21*/22const path = req.originalUrl2324// [REQ] {์์ฒญ path} {์์ฒญ ์๊ฐ}25console.log(`[REQ] ${path} ${now.toLocaleString('kr')}`)2627/***28* return next.handle()์ ์คํํ๋ ์๊ฐ29* ๋ผ์ฐํธ์ ๋ก์ง์ด ์ ๋ถ ์คํ๋๊ณ ์๋ต์ด ๋ฐํ๋๋ค.30* observable๋ก31*/32return next.handle().pipe(33tap(34// [RES] {์์ฒญ path} {์๋ต ์๊ฐ} {์ผ๋ง๋ ๊ฑธ๋ ธ๋์ง ms}35observable =>36console.log(37`[RES] ${path} ${new Date().toLocaleString('kr')} ${38new Date().getMilliseconds() - now.getMilliseconds()39}ms`,40),41),42)43}44}
๊ทธ๋ฆฌ๊ณ posts ์ปจํธ๋กค๋ฌ์ ์์ ์์ฑํ Interceptor๋ฅผ ์ถ๊ฐํด์ค๋ค.
1// posts.controller.ts ์๋ต2/*** 1) GET /posts3* ๋ชจ๋ post๋ฅผ ๋ค ๊ฐ์ ธ์จ๋ค4*/5@Get()6@UseInterceptors(LogInterceptor)7getPosts(@Query() query: PaginatePostDto) {8return this.postsService.paginatePosts(query)9}
2.1 RxJS
- https://rxjs.dev/guide/operators#creation-operators-1
- Interceptor์ ๋ํด์ ๋ ๊น๊ฒ ์๊ธฐ ์ํด์ RxJS์ ๊ณต์๋ฌธ์์์ API๋ค์ ์ฐพ์์ ๊ณต๋ถํ๋ฉด ๋๋ค.
3. Transaction Interceptor ์์ฑ
common/interceptor/transaction.interceptor ํ์ผ์ ๋ง๋ ๋ค.
1import {2CallHandler,3ExecutionContext,4Injectable,5InternalServerErrorException,6NestInterceptor,7} from '@nestjs/common'8import { Observable, catchError, tap } from 'rxjs'9import { DataSource } from 'typeorm'1011@Injectable()12export class TransactionInterceptor implements NestInterceptor {13constructor(private readonly dataSource: DataSource) {}1415async intercept(context: ExecutionContext, next: CallHandler<any>): Promise<Observable<any>> {16const req = context.switchToHttp().getRequest()1718// ํธ๋์ญ์ ๊ณผ ๊ด๋ จ๋ ๋ชจ๋ ์ฟผ๋ฆฌ๋ฅผ ๋ด๋นํ ์ฟผ๋ฆฌ ๋ฌ๋๋ฅผ ์์ฑํ๋ค.19const qr = this.dataSource.createQueryRunner()2021// ์ฟผ๋ฆฌ ๋ฌ๋์ ์ฐ๊ฒฐํ๋ค.22await qr.connect()2324/*** ์ฟผ๋ฆฌ ๋ฌ๋์์ ํธ๋์ญ์ ์ ์์ํ๋ค.25* ์ด ์์ ๋ถํฐ ๊ฐ์ ์ฟผ๋ฆฌ ๋ฌ๋๋ฅผ ์ฌ์ฉํ๋ฉด, ํธ๋์ญ์ ์์์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ก์ ์ ์คํ ํ ์ ์๋ค.26*/27await qr.startTransaction()2829req.queryRunner = qr3031return next.handle().pipe(32catchError(async e => {33await qr.rollbackTransaction()34await qr.release()3536throw new InternalServerErrorException(e.message)37}),38tap(async () => {39await qr.commitTransaction()40await qr.release()41}),42)43}44}
4. QueryRunner ์ปค์คํ ๋ฐ์ฝ๋ ์ดํฐ ์์ฑ & Transaction Interceptor ์ ์ฉ
common/decorator/query-runner.decorator.ts ํ์ผ์ ๋ง๋ ๋ค.
1import { ExecutionContext, InternalServerErrorException, createParamDecorator } from '@nestjs/common'23export const QueryRunner = createParamDecorator((data, context: ExecutionContext) => {4const req = context.switchToHttp().getRequest()56if (!req.queryRunner) {7throw new InternalServerErrorException(8`QueryRunner Decorator๋ฅผ ์ฌ์ฉํ๋ ค๋ฉด TransactionInterceptor๋ฅผ ์ ์ฉํด์ผ ํฉ๋๋ค.`,9)10}1112return req.queryRunner13})
posts ์ปจํธ๋กค๋ฌ์ Transaction Interceptor์ ์ ์ฉํ๋ค.
1// ์๋ต2import { DataSource, QueryRunner as QR } from 'typeorm'3import { TransactionInterceptor } from 'src/common/interceptor/transaction.interceptor'4import { QueryRunner } from 'src/common/decorator/query-runner.decorator'56// ์๋ต7@Post()8@UseGuards(AccessTokenGuard)9@UseInterceptors(TransactionInterceptor)10async postPosts(11@User('id') userId: number, //12@Body() body: CreatePostDto,13@QueryRunner() qr: QR,14// ๊ธฐ๋ณธ๊ฐ์ true๋ก ์ค์ ํ๋ ํ์ดํ15// @Body('isPublic', new DefaultValuePipe(true)) isPublic: boolean,16) {17const post = await this.postsService.createPost(userId, body, qr)1819for (let i = 0; i < body.images.length; i++) {20await this.postsImagesService.createPostImage(21{22post,23order: i,24path: body.images[i],25type: ImageModelType.POST_IMAGE,26},27qr,28)29}30return this.postsService.getPostById(post.id, qr)31}
posts ์๋น์ค์ qr ํ๋ผ๋ฏธํฐ๋ฅผ ๋ฃ์ด ์์ ํ๋ค.
1// posts.service.ts ์๋ต2// **** 6) ID๋ณ ํฌ์คํธ ๊ฐ์ ธ์ค๊ธฐ3async getPostById(id: number, qr?: QueryRunner) {4const repository = this.getRepository(qr)5const post = await repository.findOne({6...DEFAULT_POST_FIND_OPTIONS,7// PostsModel์ id๊ฐ ์ ๋ ฅ๋ฐ์ id์ ๊ฐ์์ง ํํฐ๋ง8where: {9id,10},11})12if (!post) {13throw new NotFoundException()14}15return post16}