NestJS에서 DTO와 TypeORM을 어떻게 사용하는지 알아보자.
클라이언트와 서버 간에 데이터를 주고 받을 때 정해진 형식이 있다면 원하는 데이터만 추출하여 형태를 통일하고, 유효성을 검사하기에 효율적일 것이다. 이를 위해 사용하는 것이 DTO인데 Data Transfer Object로 말 그대로 데이터를 전달하는 객체이다.
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은 TypeScript로 작성된 ORM으로 NestJS와 잘 통합되며, JavaScript와도 사용할 수 있다. 또한 강력한 쿼리 빌더를 지원하여 사용하기 편리하다는 것이 장점이다. 그럼 TypeORM을 어떻게 사용하는지 알아보자.
$ npm install --save @nestjs/typeorm typeorm mysql2
@nestjs/typeorm와 typeorm을 설치한다. 여기서는 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하고 다시 생성해준다.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는 엔터티와 함께 작동하며 엔터티에 대한 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엔터티를 컨트롤할 수 있다.
이제 위에서 준비한 사항들로 서비스 로직에서 직접 사용해보자.
// 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