물론 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));
}
그리고 해당 함수를 거칠 때마다 랜덤값을 출력하도록 하니
서버를 실행할 때만 이렇게 로그가 발생하고, 요청간에는 발생하지 않았다.
이것을 보아 싱글톤은 아니고, 핸들러마다 하나씩 생성되어 사용되는 것을 알 수 있었다.
'Node > Nest' 카테고리의 다른 글
Nest에서 사용자 로그인 검사하기 (0) | 2025.07.10 |
---|---|
Nest에서 비밀번호 암호화해서 보관하기 (4) | 2025.07.10 |
Nest에서 repository를 이용해 CRUD 구현하기 (0) | 2025.07.07 |
Nest에서 typeorm을 통해 Entity 작성하기 (0) | 2025.07.06 |
Nest에서 멀티 모듈 생성하는 방법 (1) | 2025.07.06 |