Interceptor(인터셉터)란?
인터셉터는 메서드 실행 전후에 추가 로직을 실행할 수 있는 강력한 기능이다. 미들웨어나 가드와는 달리 응답을 변경하거나 예외를 처리하는 등 컨트롤러의 요청/응답 흐름을 세밀하게 제어할 수 있도록 설계되어 있다.
인터셉터 주요기능
- 요청 전후 처리
- 인터셉터는 컨트롤러의 메서드가 호출되기 전과 후에 실행되어, 예를 들어 로깅, 캐싱, 응답 변환, 예외 처리 등을 수행할 수 있다.
- 응답 변경
- 메서드의 응답 데이터를 변경하거나 추가적인 데이터를 붙이는 등의 작업을 할 수 있다.
- 예외 처리
- 메서드에서 발생한 예외를 잡아서, 클라이언트에게 일관된 에러 응답을 전달하는 역할도 수행할 수 있다.
- 비동기 작업 관리
- RxJS의 Observable을 활용하여 비동기 작업 흐름을 제어하고, 메서드 실행 후 후속 작업을 추가할 수 있다.
인터셉터를 이용해 로거 구현하기
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common";
import { Observable, tap } from "rxjs";
@Injectable()
export class LogInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler<any>): Observable<any> {
/**
* 요청이 들어올 때 Req 요청이 들어온 타임스탬프를 찍는다.
* [REQ] {요청 path} {요청 시간}
*
* 응답 시 다시 타임스탬프를 찍는다.
* [RES] {요청 path} {응답 시간} {얼마나 걸렸는지 ms}
*
* return 전까지는 인터셉터전 요청에서 실행
*/
const now = new Date();
const req = context.switchToHttp().getRequest();
// /posts
// /common/image
const path = req.originalUrl;
// [REQ] {요청 path} {요청 시간}
console.log(`[REQ] ${path} ${now.toLocaleString('kr')}`)
/**
* return next.handle()을 실행하는 순간
* 라우트의 로직이 전부 실행되고 응답이 반환된다.
* observable로 반환
*/
return next
.handle()
.pipe(
tap(
// [RES] {요청 path} {응답 시간} {얼마나 걸렸는지 ms}
() => console.log(`[RES] ${path} ${new Date().toLocaleDateString('kr')} ${new Date().getMilliseconds() - now.getMilliseconds()} ms`),
),
);
}
}
인터셉터는 DI 시스템에 의해 주입될 수 있도록 @Injectable() 그리고 implements NestInterceptor 로 구현이 가능하다.
next.handle().pipe() 전의 로직들은 클라이언트에서 서버로 통신된 후 컨트롤러 메서드가 실행되기 전에 실행된다.
이후에 메서드가 실행된 후 next.handle().pipe()내의 로직이 실행된다.
클라이언트에서 서버로 요청 -> next.handle().pipe() 이전 intercept 내의 로직들 -> 컨트롤러 메서드 실행 -> next.handle().pipe() 내부로직 실행 -> 서버에서 클라이언트로 응답
// 1) GET /posts
// 모든 posts를 가져온다
@Get()
@UseInterceptors(LogInterceptor)
getPosts(
@Query() query: PaginatePostDto,
) {
return this.postsService.paginatePosts(query);
}
작성된 인터셉터는 원하는 컨트롤러 메서드에 @UseInterceptors(인터셉터) 로 로직을 구현하면 된다.
[REQ] /posts?order__createdAt=DESC&take=1 3/10/2025, 4:00:00 PM
[RES] /posts?order__createdAt=DESC&take=1 3/10/2025 32 ms
위와 같이 요청과 응답 그리고 그 차이를 볼 수 있다.
Transaction Interceptor 생성하기
@Post()
@UseGuards(AccessTokenGuard)
async postPosts(
@User('id') userId: number,
@Body() body: CreatePostDto,
// @Body('title') title: string,
// @Body('content') content: string,
) {
/**
* 트랜잭션과 관련된 모든 쿼리를 담당할
* 쿼리 러너를 생성한다.
*/
const qr = this.dataSource.createQueryRunner();
// 쿼리 러너에 연결한다.
await qr.connect();
/**
* 쿼리 러너에서 트랜잭션을 시작한다.
* 이 시점부터 같은 쿼리 러너를 사용하면, 트랜잭션 안에서 데이터베이스 액션을 실행할 수 있다.
*/
await qr.startTransaction();
try {
const post = await this.postsService.createPost(userId, body, qr);
for(let i = 0; i < body.images.length; i++) {
await this.postsImagesService.createPostImage({
post,
order: i,
path: body.images[i],
type: ImageModelType.POST_IMAGE,
}, qr);
}
await qr.commitTransaction();
await qr.release();
return this.postsService.getPostById(post.id);
} catch (error) {
/**
* 어떤 에러든 에러가 발생하면,
* 트랜잭션을 종료하고 원래 상태로 되돌린다.
*/
await qr.rollbackTransaction();
await qr.release();
throw new InternalServerErrorException(error);
}
}
기존 post를 생성하는 postPosts 컨트롤러 메서드는 불필요한 QueryRunner까지 가지고 있었다.
이 부분을 인터셉터를 이용해서 수정해보도록 하자.
/* eslint-disable @typescript-eslint/no-misused-promises */
import { CallHandler, ExecutionContext, Injectable, InternalServerErrorException, NestInterceptor } from "@nestjs/common";
import { catchError, Observable, tap } from "rxjs";
import { DataSource } from "typeorm";
@Injectable()
export class TransactionInterceptor implements NestInterceptor {
constructor(
private readonly dataSource: DataSource,
){}
async intercept(context: ExecutionContext, next: CallHandler<any>): Promise<Observable<any>> {
const req = context.switchToHttp().getRequest();
/**
* 트랜잭션과 관련된 모든 쿼리를 담당할
* 쿼리 러너를 생성한다.
*/
const qr = this.dataSource.createQueryRunner();
// 쿼리 러너에 연결한다.
await qr.connect();
/**
* 쿼리 러너에서 트랜잭션을 시작한다.
* 이 시점부터 같은 쿼리 러너를 사용하면, 트랜잭션 안에서 데이터베이스 액션을 실행할 수 있다.
*/
await qr.startTransaction();
req.queryRunner = qr;
return next.handle().pipe(
catchError(
async (e) => {
await qr.rollbackTransaction();
await qr.release();
throw new InternalServerErrorException(e.message);
}
),
tap(async () => {
await qr.commitTransaction();
await qr.release();
})
)
}
}
인터셉터에 적용할 TransactionInterceptor를 위와 같이 구현하였다.
QueryRunner를 연결하고 실행하는 부분은 어느 로직에서든 공통이며, 이후 메서드가 실행이 완료된 후 Response하기 전에 next.handle().pipe()내의 로직이 실행된다.
그 안에는 트랜잭션을 롤백(rollback)하거나 커밋(commit)하는 로직을 작성할 수 있다.
QueryRunner 커스텀 데코레이터 만들기 & Transaction Interceptor 적용하기
Transaction Interceptor 적용하기
@Post()
@UseInterceptors(TransactionInterceptor)
@UseGuards(AccessTokenGuard)
async postPosts(
...
) {
...
}
우선 구현한 TransactionInterceptor를 적용한다.
req.queryRunner = qr;
TransactionInterceptor에서 QueryRunner를 받아 Req 요청객체에 프로토타입을 만들어 적용시켰는데, 이것을 postPosts 컨트롤러 메서드에서 받아와야할 필요가 있다.
QueryRunner 커스텀 데코레이터 만들기
import { createParamDecorator, ExecutionContext, InternalServerErrorException } from "@nestjs/common";
import { QueryRunner } from "typeorm";
export const QueryRunnerDecorator = createParamDecorator((data, context: ExecutionContext) => {
const req = context.switchToHttp().getRequest();
if(!req.queryRunner) {
throw new InternalServerErrorException(
`QueryRunner Decorator를 사용하려면 TransactionInterceptor를 적용해야 합니다.`
);
}
return req.queryRunner as QueryRunner;
})
커스텀 데코레이터는 User 데코레이터를 만들었을 때를 떠올리면 된다.
반환을 TransactionInterceptor에서 작성한 req.queryRunner 를 하면 된다.
@Post()
@UseInterceptors(TransactionInterceptor)
@UseGuards(AccessTokenGuard)
async postPosts(
@User('id') userId: number,
@Body() body: CreatePostDto,
@QueryRunnerDecorator() qr: QueryRunner,
// @Body('title') title: string,
// @Body('content') content: string,
) {
...
}
위와 같이 적용한다.
완성된 postPosts 메서드
// 3) POST /posts
// post를 생성한다.
//
// DTO - Data Transfer Object (데이터 전송 객체)
@Post()
@UseInterceptors(TransactionInterceptor)
@UseGuards(AccessTokenGuard)
async postPosts(
@User('id') userId: number,
@Body() body: CreatePostDto,
@QueryRunnerDecorator() qr: QueryRunner,
// @Body('title') title: string,
// @Body('content') content: string,
) {
const post = await this.postsService.createPost(userId, body, qr);
for(let i = 0; i < body.images.length; i++) {
await this.postsImagesService.createPostImage({
post,
order: i,
path: body.images[i],
type: ImageModelType.POST_IMAGE,
}, qr);
}
return this.postsService.getPostById(post.id, qr);
}
이전과는 다르게 QueryRunner 부분이 없어져 굉장히 깔끔한 컨트롤러 메서드가 되었다.
'NestJS' 카테고리의 다른 글
[NestJS] Middleware (0) | 2025.03.11 |
---|---|
[NestJS] Exception Filter (0) | 2025.03.10 |
[NestJS] Transaction (0) | 2025.03.10 |
[NestJS] 파일 업로드 (선 업로드 방식) (0) | 2025.03.07 |
[NestJS] 파일 업로드 (클래식 방식) (0) | 2025.03.07 |