Node.js/NestJS

NestJS API 응답에서 불필요한 프로퍼티를 제외하는 방법

왈왈디 2024. 8. 3. 21:43
728x90

0. 응답에서 일부 프로퍼티를 제외해야 하는 이유

서버에서 클라이언트로 응답을 보낼 때,

불필요한 프로퍼티를 제외하고 보내거나,

민감한 정보를 반드시 제외하고 보내야 하는 상황들이 발생한다.

 

예를 들어, 유저 정보를 응답할 때

비밀번호는 반드시 제외되어야 하는 경우 등이다.

 

혹은 클라이언트에서는 유저의 닉네임만 필요한데,

유저 객체의 프로퍼티가 15개나 된다면

나머지를 모두 제외하고 보내고 싶은 상황 등이 있다.

1. @Exclude(), @Expose() 데코레이터

class-transformer 패키지에서 제공하는

@Exclude(), @Expose() 데코레이터를 응답 Dto에서 사용하면

응답으로 내보낼 혹은 제외할 프로퍼티를 편리하게 지정할 수 있다.

 

응답이 직렬화될 때 dto에서 설정한 Exclude, Expose가 적용되도록 하는 방법에는 또 여러가지가 있으나,

그 전에 Exclude, Expose 데코레이터를 적절히 사용하는 방식을 알아보자.

 

@Exclude() 데코레이터 단독으로 사용하기

import { Exclude } from 'class-transformer';

export class GetUserResponseDto {
  id: number;
  firstName: string;
  lastName: string;

  @Exclude()
  password: string;

  constructor(partial: Partial<GetUserResponseDto>) {
    Object.assign(this, partial);
  }
}

 

제외하고 싶은 프로퍼티에 @Exclude() 데코레이터를 붙이면

응답에서 제외된다.

위 경우 응답에 password는 포함되지 않는다.

 

가장 간편한 방식이지만 이 방식에서는

dto에 명시하지 않은 프로퍼티가 응답에 함께 포함될 수 있다.

class 전체에 @Exclude() 데코레이터 사용하기

import { Exclude, Expose } from 'class-transformer';

@Exclude()
export class GetUserResponseDto {
  @Expose()
  id: number;
  
  @Expose()
  firstName: string;
  
  @Expose()
  lastName: string;

  constructor(partial: Partial<GetUserResponseDto>) {
    Object.assign(this, partial);
  }
}

 

class 전체에 @Exclude() 데코레이터를 사용하면,

@Expose()로 명시하지 않은 프로퍼티는 모두 응답에서 제외된다.

 

위와 같이 작성하면, 객체에 password 프로퍼티가 존재했더라도,

dto를 거치면 응답에 포함되지 않는다.

 

응답하고 싶은 프로퍼티의 수가 적고,

그 외의 프로퍼티는 신경쓰고 싶지 않을 때 유용한 방식이다.

 

다만 @Expose()를 누락하면 응답에 포함되지 않으니 주의해야 한다.

 

특히, 다른 dto를 extends 하여 생성한 dto의 경우

부모 dto의 프로퍼티에 @Expose() 데코레이터를 사용해야 함을 잊기 쉽다.

import { Exclude, Expose } from 'class-transformer';

export class PersonDto {
  gender: string;
  
  @Expose()
  isCriminal: boolean;

  constructor(partial: Partial<PersonDto>) {
    Object.assign(this, partial);
  }
}

@Exclude()
export class GetUserResponseDto extends PersonDto {
  @Expose()
  id: number;
  
  @Expose()
  firstName: string;
  
  @Expose()
  lastName: string;

  constructor(partial: Partial<GetUserResponseDto>) {
    super()
    Object.assign(this, partial);
  }
}

 

위 경우 PersonDto의 gender는 응답에 포함되지 않고, isCriminal만 포함된다.

 

@Expose() 옵션으로 응답으로 내보낼 key 변경하기

두 데코레이터는 여러 옵션을 제공하는데,

그 중 가장 자주 사용하는 옵션은 @Expose({name: '노출될 이름' }) 옵션이다.

 

직렬화 되기 전 서버 내에서 사용할 프로퍼티 key 값과

직렬화되어 응답으로 내보낼 때 사용할 프로퍼티 key값을 구분하여 사용할 수 있다.

 

import { Exclude, Expose } from 'class-transformer';

@Exclude()
export class GetUserResponseDto {
  @Expose()
  id: number;
  
  @Expose({ name: 'first_name'})
  firstName: string;
  
  @Expose({ name: 'last_name'})
  lastName: string;

  constructor(partial: Partial<GetUserResponseDto>) {
    Object.assign(this, partial);
  }
}

 

위 경우 객체의 프로퍼티 key는 firstName이고, 

클라이언트가 응답받는 JSON에서는 first_name으로 지정된다.

 

DB에서 사용하고 있는 모델의 필드명과

클라이언트에서 원하는 프로퍼티 key가 다를 때 유용하다.

 

2. 적용 방식 (1) @UseInterceptors(ClassSerializerInterceptor) & new 생성자 함수

dto에 Exclude, Expose 데코레이터를 사용하여

응답에 포함시킬 프로퍼티를 정했다면,

응답을 직렬화할 때 데코레이터가 적용되도록 해야 한다.

 

첫번째 방식은 NestJS 공식문서에서 제안하는 (NestJS 공식 문서)

@UseInterceptors(ClassSerializerInterceptor)new 객체 생성자 함수를 사용하여

객체를 instance화 하여 내보내는 방식이다.

 

controller 메서드에 @UseInterceptors(ClassSerializerInterceptor)를 지정하고,

API 응답을 작성한 dto의 인스턴스로 보내면 된다.

import { ClassSerializerInterceptor, UseInterceptors } from '@nestjs/common';

@UseInterceptors(ClassSerializerInterceptor)
@Get()
findOne(): GetUserResponseDto {
  return new GetUserResponseDto({
    id: 1,
    firstName: 'Kamil',
    lastName: 'Mysliwiec',
    password: 'password',
  });
}

 

우리가 생성한 dto로 객체를 인스턴스화하지 않고

plain한 객체 리터럴을 그냥 응답하면,

dto에서 지정한 @Exclude(), @Expose() 데코레이터가 적용되지 않는다.

 

객체를 인스턴스화 하지 않고 그대로 반환거나,

@UseInterceptors(ClassSerializerInterceptor)를 누락하는 실수를 할 때가 많으니,

둘 다 잊지 않도록 주의해야 한다.

 

@UseInterceptors(ClassSerializerInterceptor)는

controller 상단에 지정하여 모든 메서드에 적용되도록 하는 것이 편리하다.

 

3. 적용 방식 (2) plainToInstance

@UseInterceptors(ClassSerializerInterceptor)와 생성자 함수를 함께 사용하는 것이 번거롭게 느껴진다면

class-transformer 패키지에서 제공하는

plainToInstance() 메서드를 사용하는 방법도 있다.

import { plainToInstance } from 'class-transformer';

@Get()
findOne(): GetUserResponseDto {
  return plainToInstance(GetUserResponseDto, {
    id: 1,
    firstName: 'Kamil',
    lastName: 'Mysliwiec',
    password: 'password',
  });
}

 

plainToInstance(dto class, 리터럴 객체)를 사용하여 응답값을 반환하면

@UseInterceptors(ClassSerializerInterceptor) 없이도 dto 클래스의 프로퍼티에 지정한

데코레이터들이 적용되어 직렬화된 응답이 보내진다.

 

@Exclude(), @Expose(), plainToInstance()

모두 class-transformer 패키지라서, (class-transformer github)

plainToInstance() 메서드를 사용하면

다른 인터셉터를 사용하지 않고도 @Exclude(), @Expose() 프로퍼티 데코레이터들이 적용되는 것이다.

 

@nestjs/common의 @UseInterceptors(ClassSerializerInterceptor)

 내부적으로 class-transformer instanceToPlain 메서드를 사용한다. 

NestJS 공식 문서 - Serialization

 

또, new 객체() 인스턴스화 방식을 사용하기 위해서는

dto class에 항상 constructor() 생성자 함수를 지정해줘야 하는데,

plainToInstance를 사용하면 생성자 함수 없이도 인스턴스가 생성된다.

 

4. 적용 방식 (3) @UseInterceptors(ClassSerializerInterceptor) & @SerializeOptions({type: 타입})

 

세번째 방식은 @nestjs/common에서 제공하는

@UseInterceptors(ClassSerializerInterceptor) 데코레이터와 @SerializeOptions() 데코레이터를

함께 사용하는 방식이다.

import { plainToInstance, SerializeOptions } from 'class-transformer';

@Get()
@UseInterceptors(ClassSerializerInterceptor)
@SerializeOptions({type: GetUserResponseDto})
findOne() {
  return {
    id: 1,
    firstName: 'Kamil',
    lastName: 'Mysliwiec',
    password: 'password',
  };
}

 

@SerializeOptions() 데코레이터는 다양한 옵션들을 설정할 수 있는데,

그 중 type을 우리가 작성한 dto로 지정하면

@Exclude(), @Expose() 프로퍼티 데코레이터들이 잘 적용된다.

 

5. 결론

위 3가지 적용 방식 중에 더 나은 것이 있는지는 아직 모르겠다.

취향에 따라 가장 가독성이 좋다고 판단되는 방식을 사용하면 될 것 같다.

728x90