이전장에서는 class-validator를 배웠습니다. 말 그래도 검증을 하는 것입니다. class-validator을 개발한 똑같은 개발자가 class-transformer을 만들었습니다. 이것은 변형을 하는 것입니다. 우리는 가장 많이 사용하는 expose
와 exclude
를 배우도록 하겠습니다.
언제 사용을 필요로 하냐면, response 데이터 중에서 노출되기 싫은 데이터의 경우가 해당되니다.
{
"id": 2,
"updatedAt": "2024-01-27T20:40:11.525Z", // 불필요
"createdAt": "2024-01-27T17:40:51.930Z", // 불필요
"title": "NestJS Lecture",
"content": "첫번쨰 content",
"likeCount": 0,
"commentCount": 0,
"author": {
"id": 1,
"updatedAt": "2024-01-26T05:58:10.800Z", // 불필요
"createdAt": "2024-01-26T05:58:10.800Z", // 불필요
"nickname": "codefactory",
"email": "codefactory@codefactory.ai",
"password": "$2b$10$iRxnEPp0qZuDmsiZW78uO.uV9rLWYL7Ws9pOtLdABvBtGYz5Bh9fK", // 불필요
"role": "USER" // 불필요
}
},
물론 user service 레이어에서 find()
뒤에 특정 컬럼만 가져오는 것을 골라서 할 수 있습니다. 하지만 이런 경우, post에서 전체 조회시 relation
을 이용해서 user를 가져오는데 마찬가지로 특정 컬럼만 가져오도록 또 다시 매핑을 해야합니다. 귀찮은 작업을 2번을 하는 겁니다.
async getAllUsers() {
return await this.usersRepository.find(); // 단점
}
.
.
async getAllPosts() {
return await this.postsRepository.find({ // 단점
relations: [
'author',
],
});
}
따라서 nest.js에서는 class-transformer를 사용해서 응답값을 변환하는 것을 추천하는 것입니다. 먼저 엔티티에 보여주고 싶지 않은 프로퍼티에 transformer를 붙이고, 컨트롤러에 @UseInterceptor(ClassSerializerInterceptor)
를 등록하면 됩니다.
@Column()
@IsString({
message: stringValidationMessage
})
@Length(3, 8, {
message: lengthValidationMessage
})
@Exclude() // 추가
password: string;
@Get()
@UseInterceptors(ClassSerializerInterceptor) // 변경
getUsers() {
return this.usersService.getAllUsers();
}
{
"id": 1,
"updatedAt": "2024-01-26T05:58:10.800Z",
"createdAt": "2024-01-26T05:58:10.800Z",
"nickname": "codefactory",
"email": "codefactory@codefactory.ai",
"role": "USER"
}
즉, 다음과 같은 방법으로 등록을 하게되면 원하고자 하는 프로퍼티를 제어할 수 있습니다. 여기서 serialization에 관해서 정리를 하겠습니다. 컨트롤러에서 가져온 데이터는 object 형태입니다. 이 object를 JSON으로 serialization할 때 데이터의 포맷을 변경
해줄 수 있는 것입니다. 따라서 무엇인가를 노출하거나 안보이게 만들 수 있는 것입니다.
serialization(직렬화) <--> deserialization(역직렬화)
- 현재 시스템이서 사용되는(Nest.js) 데이터의 구조를 다른 시스템에서도 쉽게 사용 할 수 있는 포맷으로 변환
- class의 object에서 JSON 포맷으로 변경
Exclude -> ExcludeOptions를 보면, 다음과 같이 정의되어 있습니다. 이게 어떤 의미인지 알아봅시다.
FE --> BE(request)
plain object(JSON) -> class instance(DTO)
BE --> FE(response)
class instance(DTO) -> plain object(JSON)
toClassOnly: class로 변환될 때만, request
toPlainOnly: plain으로 변환될 떄만, response
@Column()
@IsString({
message: stringValidationMessage
})
@Length(3, 8, {
message: lengthValidationMessage
})
@Exclude({
toPlainOnly: true, // request 허용, response X
})
password: string;
[
{
"id": 1,
"updatedAt": "2024-01-26T05:58:10.800Z",
"createdAt": "2024-01-26T05:58:10.800Z",
"nickname": "codefactory",
"email": "codefactory@codefactory.ai",
"role": "USER"
},
{
"id": 2,
"updatedAt": "2024-01-26T06:48:51.110Z",
"createdAt": "2024-01-26T06:48:51.110Z",
"nickname": "codefactory1",
"email": "codefactory1@codefactory.ai",
"role": "USER"
},
{
"id": 3,
"updatedAt": "2024-01-27T18:34:48.009Z",
"createdAt": "2024-01-27T18:34:48.009Z",
"nickname": "codefactory19",
"email": "codefactory19@codefactory.ai",
"role": "USER"
}
]
현재 비밀번호는 보이지 않습니다. 하지만 post 전체 조회를 보면 비밀번호가 노출되어 있습니다.
{
"id": 4,
"updatedAt": "2024-01-27T18:08:07.556Z",
"createdAt": "2024-01-27T18:08:07.556Z",
"title": "첫번째 title",
"content": "첫번쨰 content",
"likeCount": 0,
"commentCount": 0,
"author": {
"id": 1,
"updatedAt": "2024-01-26T05:58:10.800Z",
"createdAt": "2024-01-26T05:58:10.800Z",
"nickname": "codefactory",
"email": "codefactory@codefactory.ai",
"password": "$2b$10$iRxnEPp0qZuDmsiZW78uO.uV9rLWYL7Ws9pOtLdABvBtGYz5Bh9fK", // 노출
"role": "USER"
}
},
{
"id": 5,
"updatedAt": "2024-01-27T18:10:58.378Z",
"createdAt": "2024-01-27T18:10:58.378Z",
"title": "첫번째 title",
"content": "첫번쨰 content",
"likeCount": 0,
"commentCount": 0,
"author": {
"id": 1,
"updatedAt": "2024-01-26T05:58:10.800Z",
"createdAt": "2024-01-26T05:58:10.800Z",
"nickname": "codefactory",
"email": "codefactory@codefactory.ai",
"password": "$2b$10$iRxnEPp0qZuDmsiZW78uO.uV9rLWYL7Ws9pOtLdABvBtGYz5Bh9fK", // 노출
"role": "USER"
}
},
이 문제 또한 post 전체를 불러오는 controller에 @UseInterceptors(ClassSerializerInterceptor)
를 붙이면 해결이 됩니다. 하지만 사람은 언제가 실수를 할 수 있기 때문에 붙이는 것을 까먹을 수도 있습니다. 따라서 AppModule에 붙여 전역적으로 관리하도록 만들겠습니다. 먼저 user, post에 붙어있는 @@UseInterceptors(ClassSerializerInterceptor)
를 제거합니다.
app.module.ts를 보면 다음과 같이 4가지를 작성할 수 있게 나옵니다.
providers: [AppService, {
provide: APP_INTERCEPTOR,
useClass: ClassSerializerInterceptor, // 다른 모듈에서도 ClassSerializerInterceptor를 적용받는다.
}],
포스트맨으로 테스트를 해보겠습니다.
{
"id": 4,
"updatedAt": "2024-01-27T18:08:07.556Z",
"createdAt": "2024-01-27T18:08:07.556Z",
"title": "첫번째 title",
"content": "첫번쨰 content",
"likeCount": 0,
"commentCount": 0,
"author": {
"id": 1,
"updatedAt": "2024-01-26T05:58:10.800Z",
"createdAt": "2024-01-26T05:58:10.800Z",
"nickname": "codefactory",
"email": "codefactory@codefactory.ai",
"role": "USER"
}
},
{
"id": 5,
"updatedAt": "2024-01-27T18:10:58.378Z",
"createdAt": "2024-01-27T18:10:58.378Z",
"title": "첫번째 title",
"content": "첫번쨰 content",
"likeCount": 0,
"commentCount": 0,
"author": {
"id": 1,
"updatedAt": "2024-01-26T05:58:10.800Z",
"createdAt": "2024-01-26T05:58:10.800Z",
"nickname": "codefactory",
"email": "codefactory@codefactory.ai",
"role": "USER"
}
},
[
{
"id": 1,
"updatedAt": "2024-01-26T05:58:10.800Z",
"createdAt": "2024-01-26T05:58:10.800Z",
"nickname": "codefactory",
"email": "codefactory@codefactory.ai",
"role": "USER"
},
{
"id": 2,
"updatedAt": "2024-01-26T06:48:51.110Z",
"createdAt": "2024-01-26T06:48:51.110Z",
"nickname": "codefactory1",
"email": "codefactory1@codefactory.ai",
"role": "USER"
},
{
"id": 3,
"updatedAt": "2024-01-27T18:34:48.009Z",
"createdAt": "2024-01-27T18:34:48.009Z",
"nickname": "codefactory19",
"email": "codefactory19@codefactory.ai",
"role": "USER"
}
]
이번에는 반대가 되는 기능을 해보겠습니다. 임시로 작성하는 것이기 때문에 눈으로 보기만 하셔도 됩니다. 테스트를 위한 프로퍼티를 생성합니다.
get nicknameAndEmail() {
return this.nickname + '/' + this.email;
}
포스트맨으로 전체를 불러와도 nicknameAndEmail은 보이지 않습니다. @Expose
를 추가해 보이게끔 만들어 줍니다.
[
{
"id": 1,
"updatedAt": "2024-01-26T05:58:10.800Z",
"createdAt": "2024-01-26T05:58:10.800Z",
"nickname": "codefactory",
"email": "codefactory@codefactory.ai",
"role": "USER"
},
{
"id": 2,
"updatedAt": "2024-01-26T06:48:51.110Z",
"createdAt": "2024-01-26T06:48:51.110Z",
"nickname": "codefactory1",
"email": "codefactory1@codefactory.ai",
"role": "USER"
},
{
"id": 3,
"updatedAt": "2024-01-27T18:34:48.009Z",
"createdAt": "2024-01-27T18:34:48.009Z",
"nickname": "codefactory19",
"email": "codefactory19@codefactory.ai",
"role": "USER"
}
]
@Expose() // 추가
get nicknameAndEmail() {
return this.nickname + '/' + this.email;
}
이후에 포스트맨을 실행하면 다음과 같이 잘 나오게 됩니다.
[
{
"id": 1,
"updatedAt": "2024-01-26T05:58:10.800Z",
"createdAt": "2024-01-26T05:58:10.800Z",
"nickname": "codefactory",
"email": "codefactory@codefactory.ai",
"role": "USER",
"nicknameAndEmail": "codefactory/codefactory@codefactory.ai" // 등장
},
{
"id": 2,
"updatedAt": "2024-01-26T06:48:51.110Z",
"createdAt": "2024-01-26T06:48:51.110Z",
"nickname": "codefactory1",
"email": "codefactory1@codefactory.ai",
"role": "USER",
"nicknameAndEmail": "codefactory1/codefactory1@codefactory.ai" // 등장
},
{
"id": 3,
"updatedAt": "2024-01-27T18:34:48.009Z",
"createdAt": "2024-01-27T18:34:48.009Z",
"nickname": "codefactory19",
"email": "codefactory19@codefactory.ai",
"role": "USER",
"nicknameAndEmail": "codefactory19/codefactory19@codefactory.ai" // 등장
}
]
@Expose
를 사용하는 방법의 예시를 알려주겠습니다. 만약 특정 entity 클래스가 보안적으로 매우 중요하면, 전체 클래스를 @Exclude
를 걸고, 특정 보여줄 프로퍼티만 @Expose
를 추가하면 됩니다.
@Entity()
@Exclude()
export class UsersModel extends BaseModel {
.
.
// 특정 프로퍼티만 @Expose 추가
}