본문 바로가기
Back-end/기초부터 따라하는 nest.js

시즌 2 #12. 기초부터 따라하는 Nest.js 2 : Entity와 DTO (Data Transfer Object)

by hsloth 2024. 7. 16.

지난 포스팅에서는 Repository에 대해 배우고, PrismaService로 DB에 쿼리를 날리는 방법을 배웠습니다.

https://suloth.tistory.com/208

 

시즌 2 #11. 기초부터 따라하는 Nest.js 2 : Repository (DB에 쿼리 날리기)

지난번 포스팅에서는 Prisma를 이용해서 Nest.js에 DB를 연동하였습니다.https://suloth.tistory.com/207 시즌 2 #10. 기초부터 따라하는 Nest.js 2 : Prisma와 DB 연결 (MySQL)지난번 포스팅에서는 Pipe에 대해서 배웠

suloth.tistory.com

 

이번 포스팅에서는 Entity를 정의하고, 해당 Entity로 Dto를 만들어보는 시간을 가져보겠습니다.


과제 정답


 

지난번 과제는 다음과 같습니다.

유저를 생성하기 위해서 this.prisma.user.create 구문을 사용했잖아요?

그러면 생성된 유저의 정보를 조회하기 위해서 find 구문을 사용해서 유저의 id로 유저를 조회하는 API를 한 번 만들어봅시다.

 

먼저, UserRepository에서 다음 함수를 작성해줍시다.

// user.repository.ts
  ...
  
  async findOneUser(userId: number): Promise<any> {
    const user = await this.prisma.user.findUnique({
      where: {
        id: userId,
      },
    });

    return user;
  }

 

유저의 id 값은 unique이니 findUnique문을 사용했습니다. findFirst문을 사용해도 크게 상관없습니다.

 

그리고 UserService에는 다음 함수를 추가해줍시다.

// user.service.ts
  ...
  
  async getUserInfo(userId: number): Promise<any> {
    const user = await this.userRepository.findOneUser(userId);

    // 유저가 없으면 에러를 뱉어야겠죠?
    if (!user) {
      throw '에러'; // 원래는 문자열이 아니라 에러 객체를 throw하는 것이 정석
    }

    return user;
  }

 

userId를 인자로 받아서 유저를 찾습니다.

그리고 유저가 없으면 에러를 뱉어야 하는데 일단, 에러를 다루는 법에 대해서는 자세히 배우지 않았으니 문자열을 throw하도록 작성했습니다.

throw는 에러를 던질때 사용하는 문법입니다. (에러는 "던진다"는 표현을 사용합니다)

 

만약 유저를 찾을 수 있으면 user를 리턴합니다.

 

UserController는 다음과 같이 추가해줍시다.

// user.controller.ts
  ...
  
  @Get('/:userId/info')
  async getUserInfo(
    @Param('userId', ParseIntPipe) userId: number,
  ): Promise<any> {
    const user = await this.userService.getUserInfo(userId);

    return user;
  }

 

저는 Param을 이용해서 userId를 받아오게끔 작성하였습니다.

 

그리고 서버를 실행시켜줍시다.

npm run start

 

 

이제 브라우저에 들어가서 http://127.0.0.1:3000/user/:userId/info 를 입력해줍시다.

그리고 url의 userId에는 DB에서 최근에 만들었던 유저의 id를 입력해줍시다. 저의 경우는 9네요.

유저의 id는 지난 포스팅에 DB접속해서 유저확인하는 법을 적어두었으니 참고하시면 됩니다.

 

근데 여기서 password는 보이지 말아야겠죠? ㅋㅋㅋ 아무리 암호화가 되었더라도?

이런 부분은 이번 포스팅에서 같이 다뤄볼 예정입니다.

 


Entity 정의


Entity란 무엇일까요?

Entity는 데이터 모델링에서 사용하는 객체라고 생각하시면 됩니다.

쉽게 말해서 데이터베이스의 테이블을 Class로 만든 것이라고 할 수 있습니다.

 

그러면, Entity를 만들어 볼까요?

 

일단 우리는 지난 시간에 User, Post, Comment 테이블을 만들었습니다. 그러므로, 이 세 테이블을 각각 Class로 만들면 됩니다.

 

먼저, src폴더에 dtos폴더를 만들어주고, 그 안에 entities 폴더를 만들어주세요.

그리고 그안에 user.entity.ts , post.entity.ts , comment.entity.ts 파일을 생성해주세요.

 

 

여기서 주의할 점!

tsconfig.json 파일에서 strictPropertyInitialization: false 옵션을 추가해줘야합니다...!

이 옵션을 추가해주어야 class를 생성자 없이 선언할 수 있게됩니다.

// tsconfig.json
{
  "compilerOptions": {
    "module": "commonjs",
    "strict": true,
    "declaration": true,
    "removeComments": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "allowSyntheticDefaultImports": true,
    "target": "ES2021",
    "sourceMap": true,
    "outDir": "./dist",
    "baseUrl": "./",
    "incremental": true,
    "skipLibCheck": true,
    "strictNullChecks": true,
    "noImplicitAny": false,
    "strictBindCallApply": false,
    "strictPropertyInitialization": false,
    "forceConsistentCasingInFileNames": false,
    "noFallthroughCasesInSwitch": false
  }
}

 

 

UserEntity 정의

user.entity.ts 파일에서 UserEntity class를 정의해보도록 하겠습니다.

다음과 같이 정의할 수 있습니다.

// user.entity.ts
export class UserEntity {
  id: number;

  email: string;

  password: string;

  nickname: string;

  createdAt: Date;

  updatedAt: Date | null;

  deletedAt: Date | null;
}

 

schema.prisma 파일을 보시면서 nullable을 생각하시면서 작성하면 도움이 될겁니다.

 

 


DTO


Dto는 Data Transfer Object의 약자로, 데이터를 전달하는 객체를 뜻합니다.

비즈니스 로직같은 복잡한 코드는 없고, 순수하게 전달해야하는 데이터만을 담고 있습니다. 클라이언트와 서버가 통신할 때, Controller가 Service와 통신할 때 (호출이긴 합니다만..), ServiceRepository와 통신할 때(이것도  호출) 모두 DTO를 통해 데이터를 주고받도록 할 수 있습니다.

 

그러면, 이러한 DTO... 왜 쓰는 걸까요?

일단 DTO를 사용하는 이유는 다음과 같습니다.

  1. DTO를 사용함으로서 전달할 데이터를 명확히 하여 쓸데없는 데이터를 사전에 걸러낼 수 있다.
  2. DTO에는 Validator라는 것을 붙일 수 있어서 데이터에 대한 검증까지 진행할 수 있다.
  3. Entity에 존재하는 속성들을 유연하게 사용할 수 있다.
  4. 추가적으로, Nest.js의 경우 DTO에 Swagger 관련 작업을 할 수 있어서 자동적으로 Swagger가 만들어지게 할 수 있다.

DTO는 Class입니다. Class를 함수의 인자나 리턴 타입에 명시함으로서 전달할 데이터만 명확히 정의할 수 있습니다.

또한, Class의 각 속성(property)에는 Class validator를 붙일 수 있어서 객체의 값에 대한 유효성 검증을 진행할 수 있습니다. 예를들면, 지금 들어온 데이터가 Int인지, Float인지 혹은 Boolean 인지, Date인지 등의 검사 뿐만 아니라, 정규 표현식을 통한 검증도 가능합니다.

마지막으로는 Entity에 존재하는 속성들을 유연하게 선택 혹은 제거하여 DTO를 구성할 수 있어, User 정보를 가져올 때, password 같은 민감한 정보는 전달되지 않도록 유연하게 정의할 수 있습니다.

 

+ 추가로, Nest.js에서는 DTO에 Swagger관련 처리를 할 수 있어서 API 문서에서 각 속성에 대한 설명을 볼 수 있게끔 할 수 있습니다.

 

Swagger 관련 패키지 설치

DTO를 사용하기 전에, Swagger 관련 패키지를 설치해주어야합니다. 스웨거의 자동 작성을 위해서 스웨거 패키지에 있는 PickType, OmitType 등으로 DTO를 구성해야 좋거든요.

npm install @nestjs/swagger swagger-ui-express

fastify 기반의 nest.js 용 swagger 패키지는 따로 있습니다. 궁금하시면 Nest.js 공식문서를 참고해주세요.

 

 

그러면, DTO를 정의해봅시다.

우리가 과제에서 진행했던 유저를 조회하는 API를 한 번 봐볼까요?

UserRepostiory의 findOneUser 함수를 봐봅시다.

// user.repository.ts
  ...
  
  async findOneUser(userId: number): Promise<any> {
    const user = await this.prisma.user.findUnique({
      where: {
        id: userId,
      },
    });

    return user;
  }

 

findOneUser를 보면, 리턴 타입이 Promise<any> 타입이죠?

이 타입을 password를 제외한 리턴타입으로 바꿔보겠습니다.

 

먼저, dtos 폴더에 user폴더를 만들고, 그 안에 find-user.dto.ts 파일을 만들어줍시다.

 

그리고 다음과 같이 코드를 작성해줍시다.

// find-user.dto.ts
import { PickType } from '@nestjs/swagger';
import { UserEntity } from '../entities/user.entity';

export class FindOneUserOutputDto extends PickType(UserEntity, [
  'id',
  'email',
  'nickname',
  'createdAt',
] as const) {}

 

이런식으로 작성하면 됩니다.

PickType을 extends 받아서 사용하고, UserEntity에서 유저 정보 조회에 필요한 속성인 id, email, nickname, createdAt(가입날짜) 만을 선택해서 가져옵니다.

 

여기서 주의할 점은 PickType을 import할 때, @nestjs/mapped-types가 아닌 @nestjs/swagger로 import 해야한다는 점입니다.

 

이제 다시 UserRepository로 돌아가서 findOneUser 함수의 리턴 타입을 FindOneUserOutputDto로 수정해줍시다.

 

엇, 그런데 이상하지 앟나요? return 쪽에 빨간줄이 그어져있네요!

이유는 간단합니다. 우리는 DTO를 통해서 id, email, nickname, createdAt을 리턴하기로 명시해놨는데, 지금 user 변수에는 User에 대한 모든 데이터들이 다 담겨있기 때문입니다.

간단하게 prisma 문법을 손봐줍시다.

 

prisma 함수에서 select 속성을 추가하여 id와 email, nickname, createdAt 만을 select하도록 명시해주었습니다.

 

그런데도 아직 return에 빨간 줄이 쳐져있네요?

그 이유는 바로 find문은 아무것도 SELECT되지 않을 수 있기 때문입니다.

그래서 user 변수가 null 값을 가질 수도 있죠. 그래서 리턴 타입을 Promise<FindOneUserOutputDto | null> 로 바꿔줍시다.

 

최종 코드는 다음과 같습니다.

// user.repository.ts
  ...
  
  async findOneUser(userId: number): Promise<FindOneUserOutputDto | null> {
    const user = await this.prisma.user.findUnique({
      select: {
        id: true,
        email: true,
        nickname: true,
        createdAt: true,
      },
      where: {
        id: userId,
      },
    });

    return user;
  }

 

 

그러면, UserService와 UserController도 마찬가지로 수정해줍시다.

리턴타입만 수정해주면 됩니다.

// user.service.ts
  ...
  
  async getUserInfo(userId: number): Promise<FindOneUserOutputDto> {
    const user = await this.userRepository.findOneUser(userId);

    // 유저가 없으면 에러를 뱉어야겠죠?
    if (!user) {
      throw '에러'; // 원래는 문자열이 아니라 에러 객체를 throw하는 것이 정석
    }

    return user;
  }

 

여기서 원래라면, user 변수는 FindOneUserOutputDto | null 타입이기 때문에 그냥 return 하면 빨간줄이 뜨면서 에러가 발생해야합니다.

하지만, 중간에 if문을 통해서 user가 null인 경우 에러를 던지는 로직을 작성하였으므로, return 에서 쓰인 user는 FindOneUserOutputDto 타입으로 추론이 되어 빨간줄이 뜨지 않습니다.

 

 

 

 

UserController도 바꿔보도록 하죠.

// user.controller.ts
  ...
  
  @Get('/:userId/info')
  async getUserInfo(
    @Param('userId', ParseIntPipe) userId: number,
  ): Promise<FindOneUserOutputDto> {
    const user = await this.userService.getUserInfo(userId);

    return user;
  }

 

끝입니다!

 

이제 서버를 재시작 해보고 (ctrl+c 로 서버를 끈 다음 다시 npm run start)

 

브라우저에서 http://127.0.0.1:3000/user/:userId/info 경로를 주소창에 입력해주면 (DB에서 자신의 userId를 찾아서 사용해주세요)

아래와 같은 화면이 뜨게됩니다.

password 정보나 기타 등등의 정보가 사라졌죠? 이런식으로 DTO를 활용해서 필요한 정보만을 전달하도록 설계할 수 있습니다.

 


과제


과제가 있습니다! 조금 많은데요...

  1. UserEntity를 만든 것처럼, PostEntity, CommentEntity 만들어오기
  2. 게시글 생성, 조회(게시글 하나 상세조회) API를 DTO를 활용해서 만들어오기

입니다.

 

2개... 인데 2개가 아니네요.

총 4개인데, 2번의 API 만드는게 아마 조금 어려우실 겁니다.

제가 이번 포스팅에서 했던 것들을 잘 따라서 하신다면, 잘 마무리하실 수 있을 겁니다.

그러면 오늘도 화이팅!!!

 

 

오늘도 긴 글 읽어주셔서 감사합니다.

이상으로 Entity와 DTO에 대한 포스팅을 마치겠습니다!