Nest JS와 Pipe 그리고 TypeORM

바그다드·2023년 11월 24일
0
post-thumbnail

Nestjs란?

Node.js에 기반한 웹 API 프레임워크로 Node의 과도한 유연함으로 인한 단점에 비해 많은 기본 기능을 제공함과 동시에 확장성을 지니고 있다.
IOC, DI, AOP와 같은 OOP개념을 도입하고 있으며, 기본적으로 typescipt를 채택하고 있다.
Node기반의 웹 AIP 프레임워크 Express를 래핑하여 동작한다.

NestJS 프로젝트 생성

  • node.js 설치 후 진행
    LTS로 설치하자(안정적인 버전)
  1. nest cli 설치
npm i -g @nestjs/cli
  1. 프로젝트 초기화
nest new project-name
npm install  // 필요한 패키지를 설치
npm run start // 서버 실행
npm run start:dev // 개발 모드 실행

dev모드로 실행하면 --watch모드로 실행되어 소스코드 변경시 서버 자동으로 재시작된다.

cli로 컴포넌트 생성하기

1. 모듈 생성

nest g module boards
  • nest g(generate) module(컴포넌트이름) boards()폴더이름
    위의 cli는 boards라는 폴더에 module 컴포넌트를 생성(generate)한다는 뜻이다.

예시코드

아래의 코드에는 컨트롤러나 서비스 객체가 이미 등록되어 있는데, 각 컴포넌트를 생성한 후 직접 입력해줘야 한다.

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm'
import { BoardsController } from './boards.controller';
import { BoardsService } from './boards.service';
// import { Board } from './board.entity';
import { BoardRepository } from './board.repository';
import { Board } from './board.entity';

@Module({
  imports: [
    TypeOrmModule.forFeature([Board]) // 엔티티 등록
  ],
  controllers: [BoardsController], // 컨트롤러 등록
  providers: [BoardsService, BoardRepository] // Provider 등록
})
export class BoardsModule {}

provider란?

객체가 의존하는 대상으로, nest에서 서비스, 리포지토리, 팩토리, 헬퍼 등이 프로바이더가 될 수 있다.
객체가 필요로하는 기능을 제공하는 대상(provider)으로 생각하면 될 것 같다.

루트 모듈에 등록

  • imports 속성에 배열형태로 담아주면 된다.
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm/dist';
import { BoardsModule } from './boards/boards.module';
import { typeORMConfig } from './configs/typeorm.config';

@Module({
  // import해줄 module을 담아주면 됨
  imports: [
    BoardsModule
  ],
})
export class AppModule {}

2. 컨트롤러 생성

nest g controller boards --no-spec
  • --no-spec
    기본적으로 cli를 통해 컨트롤러나 서비스를 생성하면 테스트 파일을 자동으로 만들어주는데, 이 속성을 포함하면 따로 테스트파일을 생성하지 않는다.

예시코드

@Controller('boards')
export class BoardsController {
    // 접근 제한자를 생성자 파라미터에 선언하면 
    // 접근 제한자가 사용된 생성자 파라미터는 암묵적으로 클래스 프로퍼티로 선언이 됨
    // 원래는 스프링처럼 필드에 변수를 선언하고 생성자에서 변수에 주입을 받아야 함
    constructor(private boardService: BoardsService){}

    @Post()
    @UsePipes(ValidationPipe)
    createBoard(@Body() createBoard: CreateBoardDto) : Promise<Board>{
        // console.log("createBoard");
        
        return this.boardService.creatBoard(createBoard);
    }
}
  • 생성자에 접근제한자(위에서는 private)를 붙여주면 암묵적으로 해당 프로퍼티를 필드로 등록하고, 주입해준다. 물론 주입받는 컴포넌트가 모듈에 등록되어 있어야 한다.
    - 스프링에서는 @Autowired로 속성에 바로 주입받는 것과 비슷하다.
  • @UsePipes(ValidationPipe)
    Pipe를 사용하겠다는 것을 나타내는 데코레이터이다. 설명은 아래의 Pipe항목을 참고하자.

3. 서비스 생성

nest g service boards --no-spec

예시코드

@Injectable()
export class BoardsService {
    constructor(
        private boardRepository: BoardRepository
    ){}

    creatBoard(createBoardDto: CreateBoardDto): Promise<Board>{
        return this.boardRepository.createBoard(createBoardDto);
    }
  • @Injectable() 데코레이터를 붙여주면 대상 클래스는 주입 가능한 형태(provider)가 되고, module파일에 provider를 등록해주면, 생성자를 통해 DI를 받을 수 있다.

여기까지 기본적인 형태에 대해 알아보았고, 아래에서 Pipe와 DB통신을 위한 TypeORM에 대해 알아보자.

Pipe

  • @Injectable을 가진 클래스
    컨트롤러에 들어가기 전에 data transformation(형변환)이나, data validation(유효성 검사)을 수행하고, 라우트 핸들러가 처리하는 인수에 대해 작동하는 컴포넌트
    프록시나 데코레이터 역할을 하는 객체이다.
    • handler/parameter/global level pipe로 나뉨(클래스, 파라미터, 어플리케이션)
  • nest에서 기본적으로 제공하는 pipe
    ValidationPipe
    ParseIntPipe
    ParseBoolPipe
    ParseArrayPipe
    ParseUUIDPipe
    DefaultValuePipe
  • Pipe 사용 및 정의를 위한 모듈
    pipe를 사용하기 위해 아래의 cli를 입력해주자.

Custom Pipe

  • 커스텀 파이프를 만들기 위해서는 PipeTransform이라는 인터페이스를 구현해주면 되는데, transform 메서드를 구현해야 한다.
import { BadRequestException, PipeTransform } from "@nestjs/common";
import { BoardStatus } from "../board.model";

export class BoardStatusValidationPipe implements PipeTransform{
    readonly StatusOptions =[
        BoardStatus.PRIVATE,
        BoardStatus.PUBLIC
    ]

    transform(value: any) {
        value = value.toUpperCase();
        
        if(!this.isStatusValid(value)){
            throw new BadRequestException(`${value}는 유효하지 않은 상태입니다.`);
        }

        return value;
    }
    
    private isStatusValid(status:any){
        const index = this.StatusOptions.indexOf(status);
        return index !== -1;
    }
}
  • 기본적으로 transform의 파라미터로는 value와 ArgumentMetadata를 받는데, value는 타겟, ArgumentMetadata는 타겟의 메타데이터를 말한다.

Validator

데이터를 입력할 때 파라미터로 지정한 타입과 매칭되지 않는 타입의 데이터가 들어오면 500에러가 발생한다. 이 경우 그저 서버 에러가 되어버리기 때문에 클라이언트측에서 이를 알아채지 못하고 다시 같은 문제가 반복될 가능성이 있다.
이런 문제에 대해 nest에서 제공하는 class-validator를 활용하면 controller로 데이터가 전달되기 전에 입력 데이터에 대한 유효성 검사를 진행하고 400번대 에러와 메세지를 발생시킬 수 있다.

Class validator 설치

$ npm i --save class-validator class-transforme

관련 데코레이터

class validator에서 제공하는 데코레이터는 관련 문서를 확인하자.

TypeORM

TypeORM은 node에서 실행되는 typescript기반의 ORM을 뜻한다. 스프링에는 JPA가 있다.

  • typeorm을 사용하기 위해 필요한 모듈
npm install pg typeorm @nestjs/typeorm --save

코드를 보면 pg, typeorm, @nestjs/typeorm를 설치하고 있는데, 각 모듈은 아래와 같다
typeorm: typeorm모듈
pg : Postgres모듈
@nestjs/typeorm: NestJs에서 TypeORM을 사용하기 위해 연동시켜주는 모듈
이고, TypeORM을 사용하기 위해 설치해줘야 한다. 이것도 공식 문서에 나와있다.

0. DB정보 입력

db에 접근하기 위해 config파일에 정보를 입력해주자.

import { TypeOrmModuleOptions } from "@nestjs/typeorm";

export const typeORMConfig: TypeOrmModuleOptions ={
    type: 'postgres',
    host: 'localhost',
    port: 5432,
    username: 'postgres',
    password: '1234',
    database: 'board-app',
    entities: [__dirname + '/../**/*.entity.{js,ts}'], // 엔티티 등록
    synchronize: true
}

루트 모듈에 등록

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm/dist';
import { BoardsModule } from './boards/boards.module';
import { typeORMConfig } from './configs/typeorm.config';

@Module({
  // import해줄 module을 담아주면 됨
  imports: [
    TypeOrmModule.forRoot(typeORMConfig), // typeorm 설정파일 등록
    BoardsModule
  ],
})
export class AppModule {}

이제 TypeORM 사용을 위한 준비는 끝났다.

1. Entity 생성

import {BaseEntity, PrimaryGeneratedColumn, Column, Entity} from 'typeorm'
import { BoardStatus } from './board-status.enum';

@Entity() // 테이블과 매핑하는 데코레이터, 속성값으로 테이블의 이름을 따로 지정할 수 있음
export class Board{
    @PrimaryGeneratedColumn() // 기본키, 값 자동생성
    id: number;

    @Column()
    title: string;

    @Column()
    description: string;

    @Column()
    status: BoardStatus;
}

자세한 설명은 Repository 생성에서 같이 한다.

2. Repository 생성

이제 db관련 로직을 처리하기 위해 Repository를 생성해주자.

import {Injectable} from '@nestjs/common'
import {InjectRepository} from '@nestjs/typeorm'
import {Repository} from 'typeorm'
import { Board } from './board.entity';
import { CreateBoardDto } from './dto/create-board.dto';
import { BoardStatus } from './board-status.enum';
import { log } from 'console';

@Injectable()
export class BoardRepository{
    constructor(
        @InjectRepository(Board)
        private readonly boardRepository: Repository<Board>
    ){}
    
    async createBoard(createBoardDto: CreateBoardDto): Promise<Board>{
        const{title, description} =createBoardDto;

        const board = this.boardRepository.create({
            title,
            description,
            status: BoardStatus.PUBLIC
        })

        await this.boardRepository.save(board);
        return board;
    }
  • board를 생성하는 로직이다.
  • @InjectRepository(엔티티)
    생성자에 해당 데코레이터를 넣어 Repository객체를 주입받을 수 있다.

typeorm에서 제공하는 api는 TypeORM 공식문서를 참고하자.

참고로 db에서 데이터를 지우는 메서드는 아래 두가지 방법이 있는데, 차이는 아래와 같다.

  • remove()
    아이템을 지우는 메서드, 만약 대상이 존재하지 않으면 에러 발생
  • delete()
    아이템이 있으면 지우고 없으면 영향을 미치지 않음

Repository 패턴

  • db관련 작업을 repository에서 하는 것을 말한다. 서비스 객체는 최대한 다른 기술에 의존성을 갖지 않고 순수 로직만 남기는 것이 좋기 때문에, typeorm이나 db에 의존하게 되는 로직들은 Repository에 분리하는 것이다.
    • 공식문서에는 따로 Repository패턴이 작성되어 있지 않아서 다른 글들을 참고했다.
      기존에는 '@EntityRepository(엔티티)' 이러한 형태로 repository 패턴을 구현했는데, 이 데코레이터는 업데이트를 하면서 deprecated되어 다른 방식으로 활용해야 한다.

typeorm에는 BaseEntity 클래스를 활용한 ActiveRecord 패턴과, Repository<엔티티>객체를 활용한 DataMapper패턴이 있는데, 여기서는 DataMapper패턴을 사용하고 있다.

  • 참고로 ActiveRecord에서는
    'export class Board extends BaseEntity{'처럼 BaseEntity를 상속받아 엔티티를 정의하고, 해당 엔티티를 초기화해서 사용하면 된다.

3. 모듈에 등록

엔티티와 리포지토리를 Injectable 객체로 만들어줬으니 모듈에 등록해주자.

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm'
import { BoardsController } from './boards.controller';
import { BoardsService } from './boards.service';
import { BoardRepository } from './board.repository';
import { Board } from './board.entity';

@Module({
  imports: [
    TypeOrmModule.forFeature([Board]) // 엔티티 등록
  ],
  controllers: [BoardsController],
  providers: [BoardsService, BoardRepository] // 리포지토리 등록
})
export class BoardsModule {}

참고

  • uuid를 nest에서 사용하려면 cli로 다운받아야함
npm install uuid --save

알아야할 개념들

  • CQRS(Command and Query Responsibility Segregation, 명령과 조회의 책임 분리)
    데이터베이스에 변형을 가하는 명령과 데이터 읽기 요청을 처리하는 조회 로직을 분리함으로써 성능, 확장성, 보안을 강화하는 것
  • 클린 아키텍처
    양파(Onion) 아키텍처, 육각형 아키텍처에서 발전한 클린 아키텍처는 SW의 계층을 분리하고 저수준의 계층이 고수준의 계층에 의존하도록 하는 것.의존의 방향이 바뀐다면 DIP(Dependency Inversion Principle, 의존성 역전 법칙)적용으로 안정적인 소프트웨어를 작성할 수 있게 한다.

보일러 플레이트 코드는 여기 참고

profile
꾸준히 하자!

0개의 댓글