๐ŸŽ‰ berenickt ๋ธ”๋กœ๊ทธ์— ์˜จ ๊ฑธ ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค. ๐ŸŽ‰
Back
NestJs
28-interceptor

1. Interceptor ์†Œ๊ฐœ

nestjs-request-life-cycle

  • ๊ทธ๋ฆผ์˜ ์ž˜ ๋ณด๋ฉด Interceptor๊ฐ€ ์š”์ฒญํ•  ๋–„๋„ ์žˆ๊ณ , ์‘๋‹ตํ•  ๋•Œ๋„ 2๊ฐœ๊ฐ€ ์žˆ๋‹ค.
  • ์ฆ‰, Interceptor๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด, ์š”์ฒญ์„ ๋ณ€๊ฒฝํ•  ์ˆ˜๋„ ์žˆ๊ณ , ์‘๋‹ต์„ ๋ณ€๊ฒฝํ•  ์ˆ˜๋„ ์žˆ๋‹ค.

Interceptors_1

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 ๊ตฌํ˜„๋ฐฉ๋ฒ•

1
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'
2
import { Observable } from 'rxjs'
3
import { tap } from 'rxjs/operators'
4
5
@Injectable()
6
export class LoggingInterceptor implements NestInterceptor {
7
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
8
console.log('Before...')
9
10
const now = Date.now()
11
return next
12
.handle() //
13
.pipe(tap(() => console.log(`After... ${Date.now() - now}ms`)))
14
}
15
}

2. Interceptor๋ฅผ ์ด์šฉํ•ด ๋กœ๊ฑฐ ๊ตฌํ˜„

common/interceptor/log.interceptor.ts ํŒŒ์ผ์„ ๋งŒ๋“ ๋‹ค.

common/interceptor/log.interceptor.ts
1
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'
2
import { Observable, tap } from 'rxjs'
3
4
@Injectable()
5
export class LogInterceptor implements NestInterceptor {
6
intercept(context: ExecutionContext, next: CallHandler<any>): Observable<any> {
7
/**
8
* ์š”์ฒญ์ด ๋“ค์–ด์˜ฌ๋•Œ REQ ์š”์ฒญ์ด ๋“ค์–ด์˜จ ํƒ€์ž„์Šคํƒฌํ”„๋ฅผ ์ฐ๋Š”๋‹ค.
9
* [REQ] {์š”์ฒญ path} {์š”์ฒญ ์‹œ๊ฐ„}
10
*
11
* ์š”์ฒญ์ด ๋๋‚ ๋•Œ (์‘๋‹ต์ด ๋‚˜๊ฐˆ๋•Œ) ๋‹ค์‹œ ํƒ€์ž„์Šคํƒฌํ”„๋ฅผ ์ฐ๋Š”๋‹ค.
12
* [RES] {์š”์ฒญ path} {์‘๋‹ต ์‹œ๊ฐ„} {์–ผ๋งˆ๋‚˜ ๊ฑธ๋ ธ๋Š”์ง€ ms}
13
*/
14
const now = new Date()
15
16
const req = context.switchToHttp().getRequest()
17
18
/***
19
* /posts
20
* /common/image
21
*/
22
const path = req.originalUrl
23
24
// [REQ] {์š”์ฒญ path} {์š”์ฒญ ์‹œ๊ฐ„}
25
console.log(`[REQ] ${path} ${now.toLocaleString('kr')}`)
26
27
/***
28
* return next.handle()์„ ์‹คํ–‰ํ•˜๋Š” ์ˆœ๊ฐ„
29
* ๋ผ์šฐํŠธ์˜ ๋กœ์ง์ด ์ „๋ถ€ ์‹คํ–‰๋˜๊ณ  ์‘๋‹ต์ด ๋ฐ˜ํ™˜๋œ๋‹ค.
30
* observable๋กœ
31
*/
32
return next.handle().pipe(
33
tap(
34
// [RES] {์š”์ฒญ path} {์‘๋‹ต ์‹œ๊ฐ„} {์–ผ๋งˆ๋‚˜ ๊ฑธ๋ ธ๋Š”์ง€ ms}
35
observable =>
36
console.log(
37
`[RES] ${path} ${new Date().toLocaleString('kr')} ${
38
new Date().getMilliseconds() - now.getMilliseconds()
39
}ms`,
40
),
41
),
42
)
43
}
44
}

๊ทธ๋ฆฌ๊ณ  posts ์ปจํŠธ๋กค๋Ÿฌ์— ์œ„์— ์ž‘์„ฑํ•œ Interceptor๋ฅผ ์ถ”๊ฐ€ํ•ด์ค€๋‹ค.

posts.controller.ts
1
// posts.controller.ts ์ƒ๋žต
2
/*** 1) GET /posts
3
* ๋ชจ๋“  post๋ฅผ ๋‹ค ๊ฐ€์ ธ์˜จ๋‹ค
4
*/
5
@Get()
6
@UseInterceptors(LogInterceptor)
7
getPosts(@Query() query: PaginatePostDto) {
8
return 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 ํŒŒ์ผ์„ ๋งŒ๋“ ๋‹ค.

common/interceptor/transaction.interceptor
1
import {
2
CallHandler,
3
ExecutionContext,
4
Injectable,
5
InternalServerErrorException,
6
NestInterceptor,
7
} from '@nestjs/common'
8
import { Observable, catchError, tap } from 'rxjs'
9
import { DataSource } from 'typeorm'
10
11
@Injectable()
12
export class TransactionInterceptor implements NestInterceptor {
13
constructor(private readonly dataSource: DataSource) {}
14
15
async intercept(context: ExecutionContext, next: CallHandler<any>): Promise<Observable<any>> {
16
const req = context.switchToHttp().getRequest()
17
18
// ํŠธ๋žœ์žญ์…˜๊ณผ ๊ด€๋ จ๋˜ ๋ชจ๋“  ์ฟผ๋ฆฌ๋ฅผ ๋‹ด๋‹นํ•  ์ฟผ๋ฆฌ ๋Ÿฌ๋„ˆ๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.
19
const qr = this.dataSource.createQueryRunner()
20
21
// ์ฟผ๋ฆฌ ๋Ÿฌ๋„ˆ์— ์—ฐ๊ฒฐํ•œ๋‹ค.
22
await qr.connect()
23
24
/*** ์ฟผ๋ฆฌ ๋Ÿฌ๋„ˆ์—์„œ ํŠธ๋žœ์žญ์…˜์„ ์‹œ์ž‘ํ•œ๋‹ค.
25
* ์ด ์‹œ์ ๋ถ€ํ„ฐ ๊ฐ™์€ ์ฟผ๋ฆฌ ๋Ÿฌ๋„ˆ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด, ํŠธ๋žœ์žญ์…˜ ์•ˆ์—์„œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์•ก์…˜์„ ์‹คํ–‰ ํ•  ์ˆ˜ ์žˆ๋‹ค.
26
*/
27
await qr.startTransaction()
28
29
req.queryRunner = qr
30
31
return next.handle().pipe(
32
catchError(async e => {
33
await qr.rollbackTransaction()
34
await qr.release()
35
36
throw new InternalServerErrorException(e.message)
37
}),
38
tap(async () => {
39
await qr.commitTransaction()
40
await qr.release()
41
}),
42
)
43
}
44
}

4. QueryRunner ์ปค์Šคํ…€ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ ์ƒ์„ฑ & Transaction Interceptor ์ ์šฉ

common/decorator/query-runner.decorator.ts ํŒŒ์ผ์„ ๋งŒ๋“ ๋‹ค.

common/decorator/query-runner.decorator.ts
1
import { ExecutionContext, InternalServerErrorException, createParamDecorator } from '@nestjs/common'
2
3
export const QueryRunner = createParamDecorator((data, context: ExecutionContext) => {
4
const req = context.switchToHttp().getRequest()
5
6
if (!req.queryRunner) {
7
throw new InternalServerErrorException(
8
`QueryRunner Decorator๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด TransactionInterceptor๋ฅผ ์ ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.`,
9
)
10
}
11
12
return req.queryRunner
13
})

posts ์ปจํŠธ๋กค๋Ÿฌ์— Transaction Interceptor์„ ์ ์šฉํ•œ๋‹ค.

posts.controller.ts
1
// ์ƒ๋žต
2
import { DataSource, QueryRunner as QR } from 'typeorm'
3
import { TransactionInterceptor } from 'src/common/interceptor/transaction.interceptor'
4
import { QueryRunner } from 'src/common/decorator/query-runner.decorator'
5
6
// ์ƒ๋žต
7
@Post()
8
@UseGuards(AccessTokenGuard)
9
@UseInterceptors(TransactionInterceptor)
10
async 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
) {
17
const post = await this.postsService.createPost(userId, body, qr)
18
19
for (let i = 0; i < body.images.length; i++) {
20
await this.postsImagesService.createPostImage(
21
{
22
post,
23
order: i,
24
path: body.images[i],
25
type: ImageModelType.POST_IMAGE,
26
},
27
qr,
28
)
29
}
30
return this.postsService.getPostById(post.id, qr)
31
}

posts ์„œ๋น„์Šค์— qr ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๋„ฃ์–ด ์ˆ˜์ •ํ•œ๋‹ค.

posts.service.ts
1
// posts.service.ts ์ƒ๋žต
2
// **** 6) ID๋ณ„ ํฌ์ŠคํŠธ ๊ฐ€์ ธ์˜ค๊ธฐ
3
async getPostById(id: number, qr?: QueryRunner) {
4
const repository = this.getRepository(qr)
5
const post = await repository.findOne({
6
...DEFAULT_POST_FIND_OPTIONS,
7
// PostsModel์˜ id๊ฐ€ ์ž…๋ ฅ๋ฐ›์€ id์™€ ๊ฐ™์€์ง€ ํ•„ํ„ฐ๋ง
8
where: {
9
id,
10
},
11
})
12
if (!post) {
13
throw new NotFoundException()
14
}
15
return post
16
}