본 글은 NestJS 공식문서 Serialization 내용을 기반으로 작성하였습니다.

Nest에서 Serialization은 “컨트롤러가 반환한 객체를 네트워크 응답으로 내보내기 직전”에, 규칙에 따라 변환/정제(sanitize) 하는 단계이다. 비밀번호 같은 민감 필드를 제거하거나, 엔티티의 일부 속성만 노출하는 용도로 쓰기 딱 좋다.
엔티티의 감추고 싶은 필드를 제거하기 위해 수동으로 매번 `delete user.password` 같은 코드를 쓰면 반복/누락/실수가 생기기 쉽다. Nest는 이 문제를 선언적(declarative) 으로 풀 수 있게 `ClassSerializerInterceptor` 를 제공한다.
TL;DR
- Plain object (예: DB에서 조회한 User 결과 그대로) 를 리턴하면 민감 필드가 응답에 섞일 수 있다.
- 백엔드에서는 응답 전용 DTO를 두고, 그 DTO 기준으로만 외부에 노출하도록 직렬화(serialize) 레이어를 구성해야 한다.
- `ClassSerializerInterceptor` 와 `class-transformer` 를 활용하여, DTO 기반 응답 필터링을 만들 수 있다.
목표
- DB 모델(User)에는 `password` 가 존재
- API 응답에는 `password` 가 절대 노출되지 않도록
- 응답 스펙은 DTO로 고정
Nest의 `ClassSerializerInterceptor` 는 무엇을 하나?
- Nest 내장 기능으로, `class-transformer` 를 이용하는 인터셉터
- 기봉 동작은 핸들러 반환값에 `instanceToPlain()` 을 적용하여 응답 직렬화를 수행
- 덕분에 `@Exclude()`, `@Expose()`, `@Transform()` 같은 데코레이터 규칙이 응답에 반영됨
- 단, `StreamableFile` 응답에는 직렬화 반영 X
1) `ClassSerializerInterceptor` 를 사용하여 직렬화
DTO에서 `@Exclude()` 로 특정 필드 제외 (블랙리스트)
// user.entity.ts (응답 DTO로 정의)
import { Exclude } from 'class-transformer';
export class UserEntity {
id: number;
firstName: string;
lastName: string;
@Exclude()
password: string;
constructor(partial: Partial<UserEntity>) {
Object.assign(this, partial);
}
}
이렇게 하면 주어진 유저 엔티티의 응답 DTO에서 `password` 를 제외하겠다는 뜻이다.
`ClassSerializerInterceptor` 는 컨트롤러 단위로 적용하거나, 전역으로 적용할 수 있다.
컨트롤러에 적용
import { UseInterceptors, ClassSerializerInterceptor } from '@nestjs/common';
@UseInterceptors(ClassSerializerInterceptor)
@Get()
findOne(): UserEntity {
return new UserEntity({
id: 1,
firstName: 'John',
lastName: 'Doe',
password: 'password',
});
}
이렇게 하면 응답에서 `password` 가 제외되어 반환된다.
전역 적용
// main.ts
import { ClassSerializerInterceptor } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
...
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));
...
`ClassSerializerInterceptor` 를 애플리케이션 전역 단위로 적용하면, 해당 타입 반환 시 항상 규칙이 적용되도록 할 수 있다.
⚠️ 단, 반드시 Class Instance (해당 DTO) 를 반환해야 제대로 직렬화가 적용된다. 컨트롤러/전역 방식 모두 해당 응답을 `UserEntity` 로 반환할 때 Plain Object를 반환하는 것이 아니라, 해당 DTO 인스턴스로 반환해야 한다. 예를 들어, 일반적인 Object 로 반환하지 않고, `plainToInstance()` 등의 유틸을 동반해야 한다는 뜻이다.
`ClassSerializerInterceptor` 에는 아래와 같은 추가적인 기능/옵션들이 있다.
추가 기능 1) 계산 필드 만들기
`@Expose()` 를 활용하여 특정 필드들을 연산(함수화)하여 나온 결과를 반환시킬 수 있다. 이 때, 이 Function은 getter 함수여야 한다.
@Expose()
get fullName(): string {
return `${this.firstName} ${this.lastName}`;
}
이렇게 하면, `fullName` 필드에 함수의 결과 값이 반환되어 DTO에 노출된다.
추가 기능 2) 응답 형태 다듬기
`@Tranform()` 데코레이터를 통해, Object 전체를 반환하는 것이 아니라 특정 필드만을 반환할 수 있다.
@Transform(({ value }) => value.name)
role: RoleEntity;
`role` 필드에 들어가는 값은 `RoleEntity` (모델 Object) 전체가 아니라, `name` 값 하나만을 가지게 된다.
추가 기능 3) Pass 옵션
`@SerializeOptions` 데코레이터는 `ClassSerializerInterceptor` 에 적용될 수 있는 옵션을 설정할 수 있는 데코레이터이다. 예를 들어, 아래와 같은 데코레이터를 적용하면, `_` 를 Prefix(접두어)로 가지는 모든 필드들이 제외(Exclude)된다.
@SerializeOptions({
excludePrefixes: ['_'],
})
@Get()
findOne(): UserEntity {
return new UserEntity();
}
예를 들어, `_password` 필드는 DTO에 노출되지 않는다.
추가 기능 4) Plain Object 타입화 옵션
아까 위에서 이야기한 주의점을 해결할 수 있는 옵션이다. 해당 데코레이터가 붙어있는 함수의 응답 값을 직접 해당 DTO로 직렬화해주기 때문에, Plain Object를 Return하더라도 DTO를 거쳐서 직렬화가 적용된 결과물을 받게 된다.
@UseInterceptors(ClassSerializerInterceptor)
@SerializeOptions({ type: UserEntity })
@Get()
findOne(@Query() { id }: { id: number }): UserEntity {
if (id === 1) {
return {
id: 1,
firstName: 'John',
lastName: 'Doe',
password: 'password',
};
}
return {
id: 2,
firstName: 'Kamil',
lastName: 'Mysliwiec',
password: 'password2',
};
}
2) `class-tranformer` 와 `@Expose()` + `excludeExtraneousValues` 로 직렬화 (화이트리스트)
이 방식은 "DTO에 명시한 것만" 응답에 남기는 패턴이다. `ClassSerializerInterceptor`를 사용할 때 블랙리스트 방식으로 특정 필드들을 "제외" 하다보면 실수로 누락할 수가 있다. 그러나 화이트리스트는 필요한 것만 지정하기 때문에 좀더 안전하게 다룰 수 있다.
DTO 작성 (화이트리스트)
// user.dto.ts
import { Expose } from 'class-transformer';
export class UserPublicDto {
@Expose() id: string;
@Expose() email: string;
@Expose() name: string;
}
변환 시 `excludeExtraneousValues: true` 적용
// users.controller.ts
import { plainToInstance } from 'class-transformer';
@Get(':id')
async getOne(@Param('id') id: string) {
const user = await this.prisma.user.findUnique({ where: { id } });
return plainToInstance(UserPublicDto, user, {
excludeExtraneousValues: true,
});
}
`plainToInstance` 는 기본적으로 응답값(타입)에 없는 필드도 다 넣어버리게 되는데, 이 옵션을 켜면 `@Expose()` 가 적용되지 않은 값들은 제거된다.
위 방식들의 조합을 통해 `password` 민감 필드 노출을 막는 것은 물론이고, DTO에서 명시하지 않은 어떤 필드도 응답으로 나가지 않게 된다. 즉, 민감정보 노출사고를 막는 데에 가장 안전한 형태가 된다.
정리
- DTO는 응답 스펙을 정의
- DB에서 조회된 모델을 반환할 때엔 노출되지 않아야 하는 민감 데이터를 반드시 점검
- 외부로 내보낼 때에는 반드시 DTO를 거쳐서 필터링하는 것을 추천
- 운영 환경에서는 화이트리스트(`@Expose` + `excludeExtraneousValues`) 를 사용하는 것을 권장
'💻 나는 개발자다 > 🐈 NestJS' 카테고리의 다른 글
| ConfigModule + validate()로 서버 부팅 막기 (0) | 2026.01.13 |
|---|---|
| Docker 배포 시 JSON 파일이 누락된 문제 해결기 (0) | 2025.04.23 |
| [Swagger] BasicAuth 문서 접근 보안 (2) | 2025.01.08 |
| class-validator 의 @IsBoolean 데코레이터와 enableImplicitConversion 설정된 Transform 의 문제 (1) | 2024.02.29 |