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가지 적용 방식 중에 더 나은 것이 있는지는 아직 모르겠다.
취향에 따라 가장 가독성이 좋다고 판단되는 방식을 사용하면 될 것 같다.
'Node.js > NestJS' 카테고리의 다른 글
NestJS 커스텀 데코레이터 사용법 (5) | 2025.02.02 |
---|---|
NestJS 커스텀 프로바이더 사용법 (3) | 2025.01.05 |
NestJS 전역 CacheManager로 Redis 사용하기 (3) | 2024.07.06 |
[TIL][NestJS] @Param() 데코레이터 사용 시 주의사항 (+Typeorm) (0) | 2023.05.19 |
[NestJS] 각종 트러블 슈팅 (0) | 2023.05.18 |