728x90

물론 response dto의 class를 만들고, 해당 클래스로 변환해서 응답하는 방법도 있다.

하지만 nest에서는 다른 방법도 많이 사용하는 것 같다.

 

우선 지금 GET /users/:id를 봐보자.

비밀번호가 그대로 나온다.

현재 인코딩을 하지는 않았지만, 인코딩을 한 비밀번호라도 사용자의 응답에는 노출되면 안 될 것이다.

 

이 비밀번호를 제거할 것인데, 방법은 2가지이다.

 

엔티티에 규칙을 추가하는 방법, 중간에 가로채는 방법이다.

 

우선 엔티티에 규칙을 먼저 추가해보자.

 

엔티티에 규칙을 추가

 

무려 nest 공식문서가 추천하는 방법이다.

 

우선 다음과 같이 제외할 properties에 @Exclude()를 붙여준다.

@Entity('users')
@Unique(['email'])
export class Users{

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    name: string;

    @Column()
    email: string;

    @Column()
    @Exclude()
    password: string;

    @AfterInsert()
    logInsert(){
        console.log(`user inserted id: ${this.id}`);
    }
}

 

그리고 해당 컨트롤러의 핸들러에 다음과 같은 데코레이터를 붙여준다.

@UseInterceptors(ClassSerializerInterceptor)

    @Get("/:id")
    @UseInterceptors(ClassSerializerInterceptor)
    findUser(@Param('id') id: number): Promise<Users> {
        return this.usersService.findOne(id)
    }

 

이렇게 해야 @Exclude()가 작동한다.

 

이러고 바로 요청을 해보면

바로 password가 빠진 것을 볼 수 있다.

 

하지만 이 방법은 안타까운 부분이 있다.

다른 권한 혹은 경로로 요청을 할 때마다 보여줘야 하는 속성이 다른 경우가 있다.

관리자 권한으로 요청을 하면 이메일이 보여야 하지만, 일반 유저로 요청하면 이메일이 보이면 안되는 이런 경우 말이다.

이런 상황에서는 @Exclude()로 완벽하게 해결 할 수 없다.

 

Interceptor

원하는 상황에서 원하는 property만 넘길 수 있는 Interceptor를 만들어보자.

더 자유롭게 사용 가능하지만, 구현 난이도는 조금 더 높다.

 

AOP의 느낌으로 컨트롤러에서 client에게 가는 데이터를 가로채서 변환하는 방법이다.

Inteceptor는 NestInterceptor를 구현해서 만든다.

구현해야 하는 메서드는

intercept(context: ExecutionContext, next: CallHandler)

inercept는 자동으로 컨트롤러가 호출되면 전과 후에 호출되는 메서드이며, context는 도착한 request의 정보를 담고 있으며, next는 controller에서도 request를 처리해야 하는 handler의 정보를 담고 있다.

 

우선 간단하게 interceptor가 어떻게 동작하는지 보자.

import{
    UseInterceptors,
    NestInterceptor,
    ExecutionContext,
    CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { plainToClass } from 'class-transformer';

export class SerializeInterceptor implements NestInterceptor {

    intercept(context: ExecutionContext, next: CallHandler<any>): Observable<any> {
        console.log("before");
        console.log(context);

        return next.handle().pipe(
            map((data: any) => {
                console.log("after");
                console.log(data);
                return data;
            })
        );
    }

}

이렇게 일단 로그만 출력하는 interceptor를 만들고,

 

    @Get("/:id")
    @UseInterceptors(SerializeInterceptor)
    findUser(@Param('id') id: number): Promise<Users> {
        console.log("i'm handler");
        return this.usersService.findOne(id)
    }

Get users에 interceptor 달아서 한 번 요청해보았다.

 

우선 가장 먼저 interceptor가 실행된다.

가장 위에 있는 부분이며, ExecutionContextHost에는 

이렇게 http 요청에 대한 정보들이 포함되어 있다.

 

그 다음으로 handler메서드가 실행되고, interceptor의 로그가 출력된다.

 

결국 아래의 그림과 같이 handler의 실행 전후를 감싸고 있는 구조인 것이다.

 

그럼 이제 이 interceptor를 사용해서 json을 변환해보자.

 

우선 해당 response를 명세할 dto는 필요하긴 하다.

중간에 interceptor가 가로채서 dto로 바꿔준다고 생각하면 된다.

 

import {Expose} from "class-transformer";

export class UserDto {
    @Expose()
    id: number;

    @Expose()
    email: string;
}

 

우선 무조건 UserDto로 변환하게 만들어보면

export class SerializeInterceptor implements NestInterceptor {

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

        return next.handle().pipe(
            map((data: any) => {
                
                
                return data;
            }),
        );
    }

}

여기의 map 안에서 해당 데이터의 properties 중에 UserDto에 해당하는 것만 가져오게 만드는 것이다.

 

export class SerializeInterceptor implements NestInterceptor {
    intercept(context: ExecutionContext, next: CallHandler<any>): Observable<any> {
        return next.handle().pipe(
            map((data: any) => {
                return plainToInstance(UserDto, data, {
                    excludeExtraneousValues: true
                });
            }),
        );
    }
}

이렇게 plainToInstance로 UserDto의 속성들만 뽑아낸다.

 

한 번 다시 요청해보자.

이렇게 원하는 속성들만 나오는 것을 볼 수 있다.

 

일단 원하는 속성들만 추출하도록 만들었으니, 이제 이 interceptor를 dto를 동적으로 받을 수 있도록 리펙토링 해보자.

 

우선 이 SerializeInterceptor에 무언가 값을 받을 수 있도록 해야겠다.

@UseInterceptors(new SerializeInterceptor(UserDto))

이런식으로 UserInterceptor에서 SerializeInterceptor를 생성하고, UserDto를 넘기는 방법 말이다.

 

그리고 SerializeInterceptor에서 생성자로 dto를 받아, 해당 dto로 변환하도록 만든다.

export class SerializeInterceptor implements NestInterceptor {
    constructor(private dto: any){}
    intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
        return next.handle().pipe(
            map((data: any) => {
                return plainToInstance(this.dto, data, {
                    excludeExtraneousValues: true,
                });
            }),
        );
    }
}

 

그리고 현재 데코레이터에 new SerializeInterceptor로 넘기고 있기에, 이거도 함수로 만들어서 코드를 줄여준다.

export function Serialize(dto: any){
    return UseInterceptors(new SerializeInterceptor(dto));
}

이렇게 SerializeInterceptor를 담은 UseInterceptor를 넘겨주고, 이 Serialize를 데코레이터로 넘기면 된다.

 

@Serialize(UserDto)

 

마지막으로 저 dto 자리의 type이 any이기 때문에, 사실 어떤 값(1, true)를 넣어도 동작하게 된다.

따라서 저기에 class만 넣을 수 있도록 만들어야 한다.

 

interface ClassConstructor{
    new (...args: any[]): any;
}

export function Serialize(dto: ClassConstructor){
    return UseInterceptors(new SerializeInterceptor(dto));
}

이렇게 생성자가 존재한다는 조건을 걸어주면 클래스만 인자로 받을 수 있게 된다.

 

이제 한 번 직접 실행해보자.

 

 

이렇게 원하는 응답만 오는 것을 볼 수 있다.

 

그럼 여기서 갑자기 궁금한 부분이 생긴다.

과연 데코레이터는 싱글톤일까?

 

알아보기위해 우선 데코레이터를 2군데에 작성했다.

export function Serialize(dto: ClassConstructor){
    console.log("function", Math.random().toString(36));
    return UseInterceptors(new SerializeInterceptor(dto));
}

 

그리고 해당 함수를 거칠 때마다 랜덤값을 출력하도록 하니

 

서버를 실행할 때만 이렇게 로그가 발생하고, 요청간에는 발생하지 않았다.

이것을 보아 싱글톤은 아니고, 핸들러마다 하나씩 생성되어 사용되는 것을 알 수 있었다.

+ Recent posts