본문 바로가기

Backend/NestJS

NestJS - 대충 서비스 만들어보기 (9)

이번엔 꽤나 늦었지만 NestJS 에서 DB 를 연결해보겠습니다.

 

- Mysql

 

데이터베이스로는 심플하게 Mysql 을 사용하려고 합니다.

 

> docker run --name mysql-local -p 3306:3306/tcp -e MYSQL_ROOT_PASSWORD=xxx -d mysql:8

 

일단 로컬에서 진행하는 만큼 docker 로 간단하게 제 맥북에서 Mysql 8버전 container 를 실행했습니다.

패스워드는 자유롭게 넣어주시면 되겠습니다.

 

저는 예전부터 사내에서도 데이터베이스 클라이언트로 DBeaver 를 사용해왔습니다.

초기 버전에는 몇가지 버그도 있긴 했으나 어느덧 23버전까지 오면서 많은 부분의 개선이 있었으니 마땅한 툴을 사용하고 있지 않으셨다면

한번 써보시는것도 괜찮을 것 같습니다. 별도 라이센스 구매 같은것도 필요 없으니까요.

 

 

DBeaver 내에서 데이터베이스 생성과 관련된 부분은 생략하도록 하겠습니다. 저는 test 라는 이름의 데이터베이스를 생성했습니다.

 

- TypeORM

 

ORM 으로는 TypeORM 을 사용하려고 합니다. ORM 은 Object Relational Mapping 의 줄임말로 RDB 데이터를 객체로 매핑해주는

역할을 하며, 우리는 이를 통해 DB 데이터를 객체로서 쉽게 다룰 수 있습니다.

ORM 을 사용함으로써 기존 쿼리 작성에 쏟던 시간을 고스란히 비즈니스 로직에 좀 더 집중할 수 있게 되고 유지 보수에 좀 더 용이하다는 장점이 있지만 오직 이것만으로는 구현하기 어려운 부분들도 있고 코드가 더 복잡해지는 단점이 있을수도 있습니다.

ORM 에 대해선 여기선 이정도로만 보도록 하고 넘어가겠습니다.

 

Node.JS 진영에서 많이 쓰이는 ORM 으로는 TypeORM, Prisma, Sequelize 등이 있는데 요즘은 Prisma 인기가 좀 많아지는 것

같지만 일단 이번엔 TypeORM 으로 갑니다.

 

npm install typeorm @nestjs/typeorm mysql2

 

typeorm, @nestjs/typeorm, mysql2 라이브러리를 설치합니다.

그리고 먼저 config 설정을 해보도록 하겠습니다.

 

DATABASE_TYPE=mysql
DATABASE_HOST=localhost
DATABASE_PORT=3306
DATABASE_USERNAME=XXX
DATABASE_PASSWORD=XXX
DATABASE_SYNCHRONIZE=true

 

먼저 config/env 의 .development.env 파일에 위와 같은 설정을 추가합니다.

DATABASE_SYNCHRONIZE 설정은 typeorm 설정의 synchronize 에서 사용하기 위한 값인데, 이를 true 로 지정하면

NestJS 서비스가 실행되고 데이터베이스가 연결될 때 DB 를 NestJS 에 작성된 코드대로 초기화합니다.

production 에선 당연히 사용하면 안되는 옵션이지만, 개발간에는 편의를 위해 true 로 세팅합니다.

 

import { registerAs } from '@nestjs/config';

export const dbConfig = registerAs('db', () => ({
  type: process.env.DATABASE_TYPE,
  host: process.env.DATABASE_HOST,
  port: process.env.DATABASE_PORT,
  username: process.env.DATABASE_USERNAME,
  password: process.env.DATABASE_PASSWORD,
  synchronize: process.env.DATABASE_SYNCHRONIZE,
}));

 

config 하위에 dbConfig.ts 파일을 만들고 위와 같이 작성해주었습니다.

이전 포스팅에서 slackConfig 를 별도로 분리할 때 작성했던 것과 같은 요령입니다.

이렇게 하면 이제 어떠한 컴퍼넌트에서도 dbConfig 를 주입받아 사용할 수는 있지만, 별도의 서비스 컴퍼넌트를 하나 만들 필요가

있습니다.

 

import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [
    ...
    TypeOrmModule.forRoot({
      type: process.env.DATABASE_TYPE,
      host: process.env.DATBASE_HOST,
      ...
    }),
  ],
})

 

위처럼 typeorm 연결 설정을 app.module.ts 에 넣어줘야 하는데 뭐 저렇게 할수도 있겠습니다만

app.module.ts 에서 환경변수에 접근해 세팅하는 코드를 좀 더 깔끔히 정리하고 싶은 생각이 들어서요.

 

...
import { dbConfig } from './config/dbConfig';

@Module({
  imports: [
    AdsModule,
    SlackModule,
    ConfigModule.forRoot({
      envFilePath: path.resolve(
        __dirname,
        process.env.NODE_ENV === 'production'
          ? 'config/env/.production.env'
          : 'config/env/.development.env',
      ),
      load: [slackConfig, dbConfig],
      isGlobal: true,
      validationSchema,
    }),
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}

 

먼저 app.module.ts 에서 ConfigMoudle load 부분에 dbConfig 도 추가해줍니다.

 

import { Injectable, Inject } from '@nestjs/common';
import { ConfigType } from '@nestjs/config';
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { dbConfig } from './dbConfig';
import * as path from 'path';

@Injectable()
export class DbConfigService {
  constructor(
    @Inject(dbConfig.KEY) private config: ConfigType<typeof dbConfig>,
  ) {}

  public getTypeORMConfig(): TypeOrmModuleOptions {
    const type: any = this.config.type;

    return {
      type,
      host: this.config.host,
      port: Number(this.config.port),
      username: this.config.username,
      password: this.config.password,
      database: 'test',
      logging: false,
      entities: [path.join(__dirname, '/../**/*.entity{.ts,.js}')],
      synchronize: this.config.synchronize === 'true',
    };
  }
}

 

그리고 config 하위에 dbConfig.service.ts 파일을 만들어 위처럼 작성해주었습니다.

위에서 설정한대로 dbConfig 자체는 이미 주입해서 사용할 수 있으니 DbConfigService 는 dbConfig 를 주입받아

ORM 설정에 필요한 config 를 반환해주는 메서드를 작성했습니다.

 

...
import { TypeOrmModule } from '@nestjs/typeorm';
import { DbConfigService } from './config/dbConfig.service';

@Module({
  imports: [
    ...
    TypeOrmModule.forRootAsync({
      useFactory: (configService: DbConfigService) =>
        configService.getTypeORMConfig(),
      extraProviders: [DbConfigService],
      inject: [DbConfigService],
    }),
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}

 

변경된 app.module.ts 입니다. TypeOrmModule 을 통해 데이터베이스 연결 설정을 할 때 useFactory 를 사용해

동적으로 모듈을 생성할 수 있는데, 여기서 작성했던 DbConfigService 를 사용하는 것입니다.

이 상태로 앱을 띄우면 뭔가 에러가 나는 부분은 없지만 아무래도 실제 확인을 위해선 synchronize 옵션도 true 로 설정을 한 상태니

entity 를 하나 작성해 실제 테이블이 만들어지는지를 확인해보도록 하겠습니다.

 

import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';

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

  @Column({ length: 2, name: 'ad_type' })
  adType: string;

  @Column({ length: 10 })
  title: string;

  @Column({ length: 60 })
  thumbnail: string;

  @Column({ length: 2 })
  country: string;
}

 

기존 ads/entities 디렉토리는 원래 있었고, 여기에 ad.entity.ts 파일만 있고 내용은 비어있었는데 위와 같이 채워봅니다.

@Entity 데코레이터를 통해 이 클래스가 Entity 이며 RDB 이기 때문에 Table 임을 표시합니다.

@PrimaryGeneratedColumn 데코레이터를 통해 우리가 DB 설정시 PK 에서 자주 사용하는 auto generated 컬럼임을 나타냈고

나머지 컬럼들 또한 @Column 데코레이터를 통해 보이는 그대로입니다.

 

이제 앱을 띄운 후 DBeaver 를 통해 test database 를 확인해봅니다.

 

 

entity 코드에서 정의한대로의 컬럼을 포함한 Ad 테이블이 생성된 것을 볼 수 있습니다.

물론 위에 적었던 것처럼 production 에서는 synchronize 옵션을 사용하지 않기 때문에 이렇게 운영할 수는 없지만,

TypeORM 설정이 잘 동작하는 것은 확인할 수 있었습니다.

 

이번엔 ORM 설정 위주로 보느라 테이블 구성을 그냥 DTO 그대로 따라오다보니 별도의 인덱스 설정같은것도 들어가지 않았는데,

다음 포스팅에서 다시 테이블 설정을 하면서 기존의 Service 에 Repository 를 추가하는 내용까지 담아보도록 하겠습니다.