NestJS-Class Transformer

jaegeunsong97·2024년 1월 28일
1

NestJS

목록 보기
19/37

🖊️Exclude Annotation

이전장에서는 class-validator를 배웠습니다. 말 그래도 검증을 하는 것입니다. class-validator을 개발한 똑같은 개발자가 class-transformer을 만들었습니다. 이것은 변형을 하는 것입니다. 우리는 가장 많이 사용하는 exposeexclude를 배우도록 하겠습니다.

언제 사용을 필요로 하냐면, 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)를 등록하면 됩니다.

  • users/entities/user.entity.ts
@Column()
@IsString({
  	message: stringValidationMessage
})
@Length(3, 8, {
  	message: lengthValidationMessage
})
@Exclude() // 추가
password: string;
  • users.controller.ts
@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 Annotation 탐구

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
  • users.entity.ts
@Column()
@IsString({
  	message: stringValidationMessage
})
@Length(3, 8, {
  	message: lengthValidationMessage
})
@Exclude({
  	toPlainOnly: true, // request 허용, response X
})
password: string;

🖊️Class Serializer AppModule 적용

[
    {
        "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가지를 작성할 수 있게 나옵니다.

  • app.module.ts
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"
    }
]

🖊️Expose Annotation

이번에는 반대가 되는 기능을 해보겠습니다. 임시로 작성하는 것이기 때문에 눈으로 보기만 하셔도 됩니다. 테스트를 위한 프로퍼티를 생성합니다.

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 추가
}
profile
블로그 이전 : https://medium.com/@jaegeunsong97

0개의 댓글