NestJS API 응답에서 불필요한 프로퍼티를 제외하는 방법
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 메서드를 사용한다.
또, 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가지 적용 방식 중에 더 나은 것이 있는지는 아직 모르겠다.
취향에 따라 가장 가독성이 좋다고 판단되는 방식을 사용하면 될 것 같다.