Nest.js + TypeORM + PostgreSQL

Nest JS에 DB를 연동해 보자

Nest.js + TypeORM + PostgreSQL

들어가며

GitHub - majority-elite/dobiemon
Contribute to majority-elite/dobiemon development by creating an account on GitHub.

친목 디스코드 채널에서 사용하는 디스코드 봇 개발에 참여해 보았습니다. NestJS로 개발 중이고, 저는 여기서 영수증 기능의 백엔드 파트를 맡았습니다. NestJS 재밌네요.

대충 이런 기능

그런데 사용하다 보니, 위 임베드가 모바일에서 보면 정말 지저분하게 나옵니다. 모바일 디스코드는 임베드의 inline field를 지원하지 않기 때문이죠. 그래서, 정산 결과만 임베드로 보여주고 나머지 상세 내역은 링크를 통해 확인하도록 하기로 했습니다.

결론은 DB를 연결하기로 했고, TypeORM과  PostgreSQL를 사용해 보려고 합니다. DB 연결만 한다면 차후 여러 편의 기능을 추가하는 데에도 좋겠죠?

Postgres

설치

Linux

$ sudo apt install postgresql-14

MacOS

$ brew install postgresql@14

PSQL CLI 연결

기본 User인 postgres로 접속합니다.
Linux

$ sudo -u postgres psql

MacOS

$ psql postgres

SSL 활성화

TypeORM은 DB 사용에 SSL 연결을 사용합니다. 패키지 매니저를 통해 PostgreSQL을 설치했을 경우 SSL이 비활성화 상태이므로, 설정해 줍시다.

우선 PostgreSQL 데이터 디렉터리 위치를 확인해야 합니다.

# SHOW data_directory;

그러면 다음과 같이 위치를 출력합니다.

postgres=# show data_directory;
         data_directory
---------------------------------
 /path/to/postgresql
(1 row)

\q를 입력해서 psql CLI를 종료하고 해당 위치로 이동합니다.

$cd /path/to/postgresql

SSL 사용을 위해 인증서를 생성합니다.

$ openssl req \
    -nodes -new -x509 \
    -keyout server.key \
    -out server.crt \
    -subj '/C=국가코드 2글자/L=지역/O=조직명/CN=이름'

인증서의 올바른 권한인 400을 적용합니다. 인증서는 소유자 외에는 권한이 없어야 하며, 소유자도 읽기 권한만을 가져야 합니다. 다른 SSL 접속을 설정할 때도 동일한 사항입니다.

$ sudo chmod 400 server.{crt, key}

다시 psql CLI를 실행한 뒤 다음을 입력합니다.

# ALTER system ssl=on();
# ALTER system pg_reload_conf();

이제 정상적으로 SSL을 통한 PostgreSQL 접속이 가능합니다. 참고로, PostgreSQL 기본 포트는 5432입니다.

(선택) User 생성

postgres User는 DB의 모든 권한을 가집니다. 로컬에서 테스트할 때라면 몰라도, 실제 Production 결과물에서는 이런 사용자로 작업하면 안 되겠죠? PSQL CLI에 연결하고 아래 명령어를 실행합시다.

# CREATE USER 유저이름 WITH PASSWORD `비밀번호`;

차후 TypeORM Migration을 사용하기 위해 CreateDB 권한을 줍시다.

# ALTER USER 유저이름 CREATEDB;

TypeORM

ts-node와 TypeORM, pg를 설치해 줍니다.

$ yarn add ts-node
$ yarn add typeorm
$ yarn add pg

Entities

데이터베이스 Code First 접근을 위해서는 Entity를 작성할 필요가 있습니다. 저는 src/entities 디렉터리에 작성했습니다. 아래는 작성 예시입니다.

import {
  Column,
  CreateDateColumn,
  Entity,
  OneToMany,
  OneToOne,
  PrimaryGeneratedColumn,
} from 'typeorm';

@Entity()
export class MyEntity {
    @PrimaryGeneratedColumn() // auto increment PK
    id!: number;
    
    @CreateDateColumn()
    createdAt! Date;
    
    @OneToOne((type) => AnotherEntity, (anotherEntity) => anotherEntity.myEntity)
    /*
        타 Entity와 일대일 연결 관계를 생성합니다.
        타 Entity에는 본 Entity를 위한 Column을 하나 생성해야 합니다.
        이 경우에는 @OneToOne annotation이 붙은 myEntity Column이 필요합니다.
    */
    anotherEntity!: AnotherEntity;
    
    @OneToMany((type) => AnotherEntity, (anotherEntity) => anotherEntity.myEntity)
    /*
        타 Entity와 일대다 연결 관계를 생성합니다.
        타 Entity에는 본 Entity를 위한 Column을 하나 생성해야 합니다.
        이 경우에는 @ManyToOne annotation이 붙은 myEntity Column이 필요합니다.
    */
    anotherEntities!: AnotherEntity[];
    
    @Column({type: 'text', array: true}) // {array: true}는 postgres만 지원합니다.
    someArray?: string[];
}

더 자세한 예시는 공식 문서를 참고해 주세요.

App Module

app.module.ts을 다음과 같이 작성해 줍니다.

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config';
import { ConfigService } from '@nestjs/config';

@Module({
  imports: [
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      useFactory: (configService: ConfigService) => ({
        type: 'postgres',
        host: configService.get<string>('DB HOST 환경변수명'),
        port: configService.get<number>('DB PORT 환경변수명'),
        database: configService.get<string>('DB 이름 환경변수명'),
        username: configService.get<string>('DB User 이름 환경변수명'),
        password: configService.get<string>('DB User 비밀번호 환경변수명'),
        entities: [ MyEntity, AnotherEntity ],  // 사용할 Entity Class들 나열
        synchronize: true,        // 실행 시 자동으로 DB에 동기화
        autoLoadEntities: true,   // 실행 시 자동으로 Entity 불러오기
      }),
      inject: [ConfigService],
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

ORM Configuration

ormconfig.ts 파일을 생성해 줍니다. 저는 프로젝트 루트 디렉터리에 만들었습니다.

import { DataSource } from 'typeorm';
import * as dotenv from 'dotenv';

dotenv.config();

export const dataSource = new DataSource({
  type: 'postgres',
  host: process.env.[DB HOST 환경변수명],
  port: parseInt(process.env.[DB PORT 환경변수명]),
  username: process.env.[DB User 이름 환경변수명],
  password: process.env.[DB User 비밀번호 환경변수명],
  database: process.env.[DB 이름 환경변수명],
  extra: {
    ssl: {
      rejectUnauthorized: false,
    },
  },
  synchronize: true,
  logging: false,
  entities: ['./src/**/*.entity.ts'],
  migrations: ['./src/migrations/**/*.ts'],
});

Migration

이제 작성한 코드를 DB에 옮겨 봅시다. Migration 데이터 생성을 위해 다음을 실행합니다.
매개변수로 제시한 경로는 migration 스크립트를 작성할 위치입니다. 원하시는 대로 작성해 주세요.

$ yarn run typeorm migration:create ./src/migration

그러면 이런 내용의 파일이 생깁니다.

import { MigrationInterface, QueryRunner } from "typeorm"

export class BillMigration1683960674879 implements MigrationInterface {

    public async up(queryRunner: QueryRunner): Promise<void> {
    }

    public async down(queryRunner: QueryRunner): Promise<void> {
    }

}

파일명이랑 제목이 좀 이상해 보여도 수정하지 마세요! TypeORM에 필요한 Timestamp입니다.

package.jsonscripts에 다음 내용을 추가합니다.

"scripts": {
    "migration:up": "ts-node ./node_modules/.bin/typeorm migration:run -d ormconfig.ts",
    "migration:down": "ts-node ./node_modules/.bin/typeorm migration:revert ormconfig.ts"
}

이제 migration:up을 실행합니다.

$ yarn run migration:up

DB를 확인하면 migration이라는 이름의 테이블 하나가 생성됩니다.
이제 프로그램을 실행합니다.

$ yarn run start

쿼리 실행 이력이 출력됩니다. DB를 확인해 보면 테이블들이 Entity 내용대로 잘 생성되어 있을 겁니다.

참고

Nest.js + TypeORM + PostgreSQL 시작하기
삽질 해보기
MacOS PostgreSQL 설치 하고 테이블 생성, 조회하기
Intro PostgreSQL은 확장 가능성 및 표준 준수를 강조하는 객체-관계형 데이터베이스 관리 시스템의 하나 입니다. 오픈소스 RDBMS로서 사용율은 Oracle, MySQL, Microsoft SQL에 이어 4위를 기록하고 있습니다. 설치 설치를 위해 brew에 postgresql 을 검색해 보았습니다. brew search postgresql brew install postgresql 을 입력 해서 기본버전을 설치합니다. 설치가 완료되었습니다. 버전을 명시하지 않았더니 14 버전이 설치되었네요. postgres -V 서비스…
Enabling and Enforcing SSL/TLS for PostgreSQL Connections
Enabling SSL in PostgreSQL is very straightforward and here go through the steps and check/validate the connections are indeed using the safer SSL protocol.
TypeORM migration is not creating the required tables, instead it creates only a table called migration
I am building a backend application using typescript, typeORM and postgres, after generating and running my migrations instead of creating the tables of the entities I wrote, it only creates a single
How do I find PostgreSQL’s data directory?
I forgot how I started PostgreSQL the last time (it was months ago) and I don’t remember where the data directory is located. The postgres command seems to require the location of the data director...