NestJS - DTO와 TypeORM

수현·2023년 11월 5일

NestJS

목록 보기
3/3

NestJS에서 DTO와 TypeORM을 어떻게 사용하는지 알아보자.

DTO

클라이언트와 서버 간에 데이터를 주고 받을 때 정해진 형식이 있다면 원하는 데이터만 추출하여 형태를 통일하고, 유효성을 검사하기에 효율적일 것이다. 이를 위해 사용하는 것이 DTO인데 Data Transfer Object로 말 그대로 데이터를 전달하는 객체이다.

NestJS에서는 DTO를 어떻게 사용하고 데이터를 처리하는지 알아보자.

NestJS에서의 DTO

TypeScript로 진행하는 프로젝트에서 DTO로 사용할만한 것은 interface, class가 있다. 하지만 공식문서에서는 class 사용을 권장하는데 그 이유는 interface와 class의 다음과 같은 차이에 있다.

  • intreface는 컴파일 타임에 동작한다.
    무슨 말이냐하면, interface는 ts에만 있는 것으로 JS로 컴파일되면서 런타임에는 존재하지 않는다. interface의 주요 역할은 타입 검사, 코드 가이드 정도 이다.

  • class는 런타임에도 동작한다.
    ts로 작성한 class는 실제 JS코드로 변환되어 런타임에 인스턴스를 생성하고 메서드를 호출할 수 있다.

따라서 class를 DTO로 사용하는 것은 런타임에서 데이터 구조를 활용할 수 있게 하고, NestJS의 Pipe와 같이 런타임에서 동작하는 도구가 유효성 검사를 하고 처리할 수 있게 한다.

예시 코드

// createBoard.dto.ts
export class CreateBoardDto {
  title: string;
  content: string;
}

// boards.controller.ts
@Controller('boards')
export class BoardsController {
  constructor(private boardsService: BoardsService) {}

  @Post()
  createBoard(@Body() createBoardDto: CreateBoardDto): Board {
    return this.boardsService.creatPost(createBoardDto);
  }
}

controller에서 라우팅될 때 @Body() 데코레이터와 DTO를 사용하여 원하는 데이터를 저장할 수 있다. HTTP request message의 body에 들어온 데이터가 해당 DTO에 맞게 추출 후 저장되는 것이다. 이렇게 추출한 DTO를 서비스로 넘겨주어 로직을 실행하게 된다.


TypeORM

TypeORM은 TypeScript로 작성된 ORM으로 NestJS와 잘 통합되며, JavaScript와도 사용할 수 있다. 또한 강력한 쿼리 빌더를 지원하여 사용하기 편리하다는 것이 장점이다. 그럼 TypeORM을 어떻게 사용하는지 알아보자.

TypeORM 설치 및 세팅

$ npm install --save @nestjs/typeorm typeorm mysql2

@nestjs/typeormtypeorm을 설치한다. 여기서는 mysql2를 설치했는데 사용하는 DB에 맞게 모듈을 설치하면 된다.

// app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'root',
      database: 'test',
      entities: [__dirname + '/../**/*.entity.{ts,js}'],
      // entities: [Board, User],
      synchronize: true,
    }),
  ],
})
export class AppModule {}

루트 모듈인 app.module.ts에 TypeORM을 사용하기 위한 설정을 한다. 지금을 이렇게 작성했지만 config를 만들어 파일을 분리하면 코드 관리가 더 용이할 것 같다.

  • entities: 엔터티를 이용해 DB table을 생성하게 되는데 엔터티 파일의 위치를 설정한다. 위와 같이 설정하면 모든 entity를 다 포함하게 되고, 하나씩 작성할 수도 있다.
  • synchronize: true로 설정하면 엔터티의 컬럼을 수정한 경우 애플리케이션을 다시 실행할 때 해당 table을 drop하고 다시 생성해준다.

Entity 생성 및 등록

TypeORM에서는 class 형태로 Entity를 정의하여 table을 생성한다. 그리고 class 안에 각각의 column들을 정의한다.

// boards.entity.ts
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { BoardStatus } from './board.model';

@Entity()
export class Board {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  title: string;

  @Column()
  content: string;

  @Column()
  status: BoardStatus;
}
  • @Entity() decorator는 Board가 엔터티임을 나타낸다. SQL로 보면 CREATE TABLE board로 볼 수 있다.

  • @PrimaryGeneratedColumn() decorator는 class의 id field가 기본키임을 나타내며 id를 자동으로 생성하도록 한다. SQL의 PRIMARY KEY AUTO_INCREMENT 제약조건과 같다.

  • @Column() decorator는 하나의 컬럼을 나타낸다.

// board.module.ts
@Module({
  imports: [TypeOrmModule.forFeature([Board])],
  controllers: [BoardsController],
  providers: [BoardsService],
})
export class BoardsModule {}

생성한 Entity를 다른 곳에서도 사용할 수 있도록 해당 모듈에 import를 해준다.

Repository 등록

repository는 엔터티와 함께 작동하며 엔터티에 대한 CRUD를 처리하는 역할을 한다. repository를 사용하려는 service class에 등록하여 사용할 수 있다.

// board.service.ts
import { Board } from './board.entity';
import { Repository } from 'typeorm';

@Injectable()
export class BoardsService {
  constructor(
    @InjectRepository(Board)
    private boardRepository: Repository<Board>,
  ) {}
...
}

service class의 contructor에서 의존성을 주입해줘야 한다. @InjectRepository(Entity) decorator를 붙여서 BoardsService에서 BoardRepository를 사용할 수 있도록(주입) 한다.
이제 Repository<Board>Board엔터티를 컨트롤할 수 있다.

TypeORM의 사용

이제 위에서 준비한 사항들로 서비스 로직에서 직접 사용해보자.

// boards.service.ts
@Injectable()
export class BoardsService {
  constructor(
    @InjectRepository(Board)
    private boardRepository: Repository<Board>,
  ) {}

  async getBoardById(id: number): Promise<Board> {
    const found = await this.boardRepository.findOneBy({ id: id });

    if (!found) {
      throw new NotFoundException(`Can't find Board with id ${id}`);
    }
    return found;
  }
}

필요한 로직을 함수로 정의할 수 있다. findOneBy와 같은 TypeORM의 EntityManager를 사용하여 DB를 manage할 수 있는데 공식문서에서 다양한 EntityManager를 확인하고 사용할 수 있다.
(DB와 상호작용하는 것은 시간이 걸리므로 값을 바로 받아서 사용하기 위해 async & await을 사용한다. async를 사용하기 때문에 반환값은 Promise!)

// boards.controller.ts
...

  @Get('/:id')
  getBoardById(@Param('id', ParseIntPipe) id: number): Promise<Board> {
    return this.boardsService.getBoardById(id);
  }
  
...

controller에서의 반환값도 Promise가 된다.


참고 자료

https://www.youtube.com/watch?v=3JminDpCJNE&t=5340s
https://docs.nestjs.com/techniques/validation
https://typeorm.io/entities

profile
실패와 성장을 기록합니다 🎞️

0개의 댓글