Nest.js

Nest.js - Interceptor 로 로그 및 응답 데이터 변형하기

jaewoo 2023. 11. 26. 14:41

 

Nest.js 에서 Interceptor 주요 기능과 특징

1. 컨트롤러 실행 전후 로직 : Interceptor는 컨트롤러 메서드가 실행되기 전과 후에 특정 로직을 수행할 수 있다. 이를 통해 요청을 가로채거나 응답을 수정할 수 있다. 

2. 응답변형 : 인터셉터를 사용하여 컨트롤러에서 반환된 응답을 변형하거나, 특정 형식으로 포멧팅할 수 있다.

3. 비동기처리:  Interceptor는 RxJS 의 Observable 을 사용한다. 이를 통해 비동기 데이터 스트림을 관리하고, 효율적인 비동기 처리를 할 수 있다. 

4. 로깅 및 모니터링: 요청과 응답에 대한 로깅을 수행하거나 실행시간을 측정하는 등의 모니터링 작업을 인터셉터에서 처리할 수 있다.

 

 

구현코드

일단 인터셉터를 구현하려면 NestInterceptor 를 implments 하고 사용하려는 엔드포인트에 UseInterceptor 를 하면 해당 엔드포인트에 인터셉터를 적용시킬 수 있다.

 

intercept 메소드를 구현해야하는데 해당 메소드는 두개의 파라미터를 받는다.

 

- ExecutionContext

  ExecutionContext는 현재 실행되고 있는 컨텍스트에 대한 상세정보를 제공한다. 이를 통해 현재 처리되고 있는 HTTP 요청과 관련된 다양한 정보를 접근할 수 있다.

intercept(context: ExecutionContext, next: CallHandler): Observable<any> {

	const request = conext.switchToHttp().getReqest();
    const response = context.switchToHttp().getResponse();
    ...

}

 

- CallHandler

  CallHandler 는 NestJS 의 요청 처리 파이프라인에서 다음으로 실행될 핸들러를 나타낸다. 이 객체를 사용하여 현재 요청의 처리를 계속 진행하거나 처리과정을 변경할 수 있다.

 CallHanlder는 handle 메소드는 Observable 을 반환한다. 이 Observable은 컨트롤러 메소드의 실행 결과를 나타내며, 이를 구독하고 처리하여 응답을 변경하거나 추가 로직을 실행할 수 있다.

 

intercept(context: ExecutionContext, next: CallHandler): Observable<any>{

	return next
    		.handle() //다음 핸들러(컨트롤러 메서드)의 처리를 진행함
            .pipe( 
            	// 이곳에서 RxJS 연산자를 사용하여 응답을 변형하거나 추가 처리를 진행할 수 있다.
            );

 

 

로그 인터셉터

import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common";
import { Observable, map, tap } from "rxjs";


@Injectable()
export class LogginInterceptor implements NestInterceptor{
    intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
        
        const request = context.switchToHttp().getRequest();
        const path = request.originalUrl;
        const now = new Date();

        console.log(`${path} TIME: ${now.toLocaleString('kr')}`);

        return next
            .handle()
            .pipe(
                tap((observable) => console.log(observable)),
                map((observable) => ({
                    message: '인터셉터에서 추가한 메시지',
                    data: observable
                }))
            );
    }

}

 

여기서 중요한 것은 CallHandler.handle() 이 실행되기 전까지는 핸들러에 도달하기 전 상태이다. handle() 이 실행되면 핸들러에 도달하고 도달하면서 컨트롤러에 선언한 기능들이 동작한다. 그 다음 응답을 보낼때는 pipe() 를 통해 처리하는데 여기서는 주로 map, tap 으로 웬만한 걸 처리할 수 있다.

 

map()

- map 연산자는 Observable 에서 방출된 각 값에 대해 제공된 함수를 실행하고, 그 결과를 새로운 Observable 로 변환한다.

이를 통해 응답데이터를 변환하거나 조작할 수 있다.

return next.handle().pipe(
	map(data => ({
    		...data, //기존에 컨트롤러에서 반환한 데이터
            addProperty: '추가된 응답값'
            })));

 

tap()

- tap 연산자는 Observable 에서 방출된 각 값에 대한 부수효과를 수행한다. tap 은 데이터 자체를 변형하지 않고 로깅, 디버깅 등의 용도로만 사용한다.

return next.handle().pipe(
		tap(() => console.log('응답 나가는 중'))
        );

 

Controller

  @Get('all')
    @UseInterceptors(LogginInterceptor)
    async allData(): Promise<string> {

        return '테스트';
    }

 

UserInterceptos 로 지정해줘야만 해당 엔드포인트에 인터셉터가 정상작동한다.