02. Nest.js에서 Prisma 사용하기 (Postgres, AWS LightSail, DBeaver)

SuJeong·2022년 8월 21일
10
post-thumbnail

앞서 Nest 초기 세팅을 했으니 이제 데이터베이스를 세팅해야 한다.

DBMS는 PostgreSQL를, ORM은 Prisma, 클라우드에서 RDS를 사용하기 위해 AWS의 LightSail를 활용했다. 또한 DB Tool로 DBeaver를 사용했다.

천천히 정리해보자.

왜 PostgreSQL를?

나는 지난 프로젝트에서 NoSQL인 MongoDB을 주로 사용했고, 학생 때는 MySQL을 쓰긴 했지만 데이터베이스 자체를 공부하는 목적으로 사용했다.

NoSQL은 Not Only SQL의 약자로, RDB와는 다르게 테이블 간의 관계를 정의하지 않는 데이터베이스이다. 빅데이터의 등장으로 데이터와 트래픽의 증가로 인해 성능을 향상시킨 NoSQL이 많이 사용되고 있다.

MongoDB는 NoSQL 답게 사용하기 아주 간편하지만, 복잡한 데이터 간의 관계를 표현하기가 어렵고 서로 연결된 데이터를 한번에 가져오기가 까다로웠다.

이번 프로젝트엔 특히나 회원 테이블을 중심으로 다양한 관계가 유지되어야하기 때문에 NoSQL보다 RDBMS가 적합하다고 판단하였다.

RDBMS는 관계형 데이터베이스 관리 시스템으로, 대표적으로 MySQL과 PostgreSQL, Oracle 등이 있다.

사실상 비용이 비싼 Oracle 프로젝트에서 쉽게 사용하기 어렵기 때문에 처음부터 고려하지 않았다.

MySQL은 가장 많이 쓰는 RDBMS이자, update 성능이 Postgres보다 우수하며 간단한 처리 속도가 뛰어나다. 로드가 많거나 복잡한 쿼리는 성능이 낮은 편이다.

PostgreSQL은 update 속도가 느린 편이지만 확장성과 호환성이 좋아 데이터를 검증해야하는 시스템에서 사용하기 좋다. 또한 MVCC(다중 버전 동시성 제어)로, 로킹에 의한 병목 현상이 생기지 않아 동시성 제어에 있어서 우수하다.

사실 이런 설명을 읽더라도 어떤 DBMS가 우리 프로젝트에 어울리는지, 어떤 DBMS를 골라야할지 감이 전혀 잡히지 않았다.

게다가 엄청 큰 서비스면 모를까 과연 이게 차이가 있을것인가...

어쨌든 이런 식으로 DB를 선택하는 것은 의미가 없는 것 같았다. 그런데 문득 이미 사용해봐서 비교적 친숙한 MySQL에 비해 Postgres는 처음 들어보는 DBMS라는 생각이 들었다.

그래서 사용 스택을 좀 더 넓히기 위해 Postgres를 사용하기로 결정했다(...)

쓰다보면 친해지겠지...?

왜 AWS LightSail를?

데이터베이스는 클라우드 플랫폼을 통해 구축하여 개발자들이 같은 데이터베이스를 공유하고 액세스할 수 있도록 세팅을 해야한다.

처음엔 GCP의 SQL로 세팅을 했는데 어느날 갑자기 DB가 먹통이 되었다.

같은 구글 계정으로 다른 서비스를 같이 쓰다보니까 금방 요금이 부족해졌던 것이다. 왜냐하면 GCP는 현재 3개월 무료이지만, 정해진 요금이 있었다!

와! DB 첨부터 다시 구축해야하네! 신난다!

그래서 클라우드 컴퓨팅 서비스인 AWS를 이용하여 데이터베이스를 다시 구축하였다. AWS의 LightSail의 DB는 가장 낮은 요금제를 3개월 무료로 이용할 수 있다.

AWS LightSail은 가상 프라이빗 서버(VPS) 서비스이다. VPS는 하나의 물리서버를 여러 개의 가상 서버로 쪼개어 여러 명의 클라이언트가 쓰는 것을 말한다.

전용 서버와 유사하며 동일한 속도와 보안 서비스를 받을 수 있다. 다만 확장성이 없는게 단점인데, 어지간하면 확장성까지 고려할 필요는 없어보였다.

LightSail은 이미 구성되어 있는 VPS 클라우드 리소스를 활용하여 애플리케이션을 빠르게 구축할 수 있는 서비스를 제공해준다.

LightSail에는 instance와 container, database, storage 등 활용할 수 있는 서비스가 많은데, 그 중에서 Database만 사용해보도록 하자.

LightSail Database 사용법

LightSail DB를 등록하는 방법은 아주 간단하다. 진짜로 간단하다.

AWS에 로그인하고 lightsail을 검색해서 들어가면 해당 화면을 볼 수 있다.

페이지가 아기자기하다.

DB를 만들어보자.

메뉴 중 Database를 선택하고 create database를 선택한다.

사용할 DBMS를 골라준다.

우리 프로젝트에서는 postgreSQL를 사용하기 때문에 돌고래가 아니라 코끼리를 골라주었다.

요금제는 가장 낮은 것으로 유지해준다. 3개월 무료라고 되어있다.

이제 DB 이름을 적고 create database를 선택하면 끝이다.

기다리면 DB가 만들어지는데 조금 시간이 걸린다. 인내심을 가지고 기다려야 한다.

DB가 다 만들어지면 세부 정보들을 확인할 수 있다.

여기서 Endpointuser name, password, port를 잘 기록해두자. 나중에 해당 DB를 연결하려면 필요한 정보들이다.

마지막으로 외부에서 접근해야하니 networkingpublic mode로 바꿔준다.

GCP SQL보다 훨씬 간단한 방법으로 DB를 구축할 수 있다. 다만 DB 스팩을 자유롭게 설정할 수 있는 SQL에 비해 정해진 요금제를 따라야한다는게 단점이라고 할 수 있다. 그래도 3개월 공짜니까!

프리티어라도 너무 많이 쓰면 요금이 청구되니 주의해야한다...

왜 Prisma를?

ORM이란 Object Relational Mapping의 약자로, 객체와 관계형 데이터베이스의 데이터를 매핑해주는 것이다. 객체 지향 프로그래밍의 모델링된 객체 간의 관계를 바탕으로 SQL을 자동으로 생성하여 관계형 데이터베이스의 테이블을 만드는 것이다.

ORM으로 비즈니스 로직에 더 집중할 수 있고 재사용과 유지보수가 편해졌으며, DBMS에 대한 종속성이 줄어들었다는 장점이 있다. 확실히 MySQL이든 Postgres이든 ORM으로 모델링을 한다면 DBMS 자체에 대한 부담이 줄어들 것이다. 다만 ORM으로만 서비스를 구현하기 어렵다는 단점이 있다.

이전에 엘리스 프로젝트에서 사용했던 ORM인 Mongoose와 TypeORM은 아주 획기적이었다! ORM을 사용함으로써 새로운 DBMS를 사용하고자 할때 느껴지는 부담이 대폭 줄었다고 할 수 있다.

Nest에는 원래 TypeORM이 기본적으로 탑재되어있다.

솔직히 나는 TypeORM에서 부족함을 느끼지 못했다. 하지만 한창 사용할 때와는 다르게 다시 TypeORM의 초기 세팅을 하고자 하니 조금 피곤해지는 부분이 있었다. (TypeORM은 entity나 repository 등 설정해야하는 것이 꽤 있다)

지난 프로젝트 때 TypeORM을 써보기도 했고, 같이 프로젝트를 하게된 팀원의 적극 추천으로 이번엔 Prisma를 사용해보았다.

Prisma는 초기 세팅이 아주 쉽고 일부 사용법이 Git 사용법과 유사하다는 점, 그리고 UI를 제공한다는 장점이 있다!

정말 한번 쯤은 써볼 만한 좋은 ORM이다. 그래도 마음 고생은 꽤 시킨다 절대 얌전한 친구는 아니다.

추가로 typeORM에 있는 timezone 옵션이 prisma에는 없다. 아주 큰 단점이다.

Prisma 시작하기

먼저 종속성을 설치하고, 초기 세팅을 해준다.

$ npm install prisma --save-dev
$ npx prisma init

prisma/schema.prisma 파일이 아래처럼 자동으로 생성된다.

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

또한 .env 파일이 생성되는데, 그 파일 안에 DATABASE_URL가 정의되어 있다.

DATABASE_URL="postgresql://USER:PASSWORD@HOST:PORT/DATABASE"

앞으로 이 .env 파일도 관리를 해야하는데, 이 파일로 앞서 AWS LightSail에서 만든 DB를 연결할 수 있다. 그때 기록해둔 정보를 가져온다.

USER : user name
PASSWORD : password
HOST : Endpoint
PORT : port
DATABASE : postgres (대충 이걸로 하면 됨)

각각 정보를 넣어주면 된다. port는 5432가 기본이기 때문에 생략도 가능하다.

password에 특수문자가 포함된 경우 제대로 연결이 되지 않는데, url 특수문자처리를 해주어야한다.

여기서 인코딩을 할 수 있다.

Prisma로 간단한 Schema 만들기

prisma로 modeling을 하는 방법은 prisma 공식문서에 아주 잘 설명되어있다!

구조도를 참고해서 prisma CLI를 설치하고 schema를 만들어보자.

종속성을 추가하고 CLI에 접근한다.

$ npm install @prisma/client
$ npx prisma generate

이제 prisma.schema에서 데이터 모델링을 통해 schema를 생성할 수 있다.

여기 사용하기 좋은 vscode extension이 있다.

prisma 문법상 문제가 있는 경우 알려주어 modeling 할때 아주 유용한 extension이다. 모델링 하기 전에 미리 깔아두는게 좋다! 이거 초반에 안 깔고 했다가 에러 코드를 많이 봤다...

extension까지 설치했다면 prisma로 schema를 만들 준비가 정말 끝났다.

이제 미리 만들어둔방치한 ERD를 준비한다. 사실 ERD도 업데이트 해야하는데 지금 발견했다(...)

아래는 ERD를 보고 만든 완성한 예제 코드이다.

현재 사용하고 있는 스키마 코드와는 조금 다르다.

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id               String             @id @default(uuid())
  email            String?            @unique @db.VarChar(50)
  name             String?            @db.VarChar(20)
  password         String?            @db.VarChar(100)
  phone            String?            @unique @db.VarChar(20)
  profileImg       String?            @default("imageUrl")
  isDeleted        Boolean?           @default(false)
  birth            String?            @db.VarChar(9)
  provider         Provider?          @default(LOCAL)
  createdAt        DateTime?          @default(now()) @db.Date
  updatedAt        DateTime?          @updatedAt
  PillBookMark     PillBookMark[]
}

model PillBookMark {
  id      String  @id @default(uuid())
  user_id String?
  User    User?   @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: Cascade)
}

enum Provider {
  GOOGLE
  KAKAO
  LOCAL
}

model은 엔티티를 정의한다. 여기서는 UserPillBookMark 테이블을 정의하였다.

모델은 단 하나의 id 필드를 가질 수 있으며 @id로 정의할 수 있다. @default()로 id의 기본값을 정의해줄 수 있는데, 보통 autoincrement()나 uuid()를 사용한다.

autoincrement()는 정수 타입이고 uuid()는 문자 타입이니 이를 유의하여 타입을 지정해야한다.

같은 이메일로 여러 계정을 가지지 못하도록 email 필드에 @unique() 속성을 주어 고유하게 식별되도록 해주었다.

위에서 정의된 datasoure인 db로 직접 필드의 타입을 지정해줄 수 있다.

  • @db.VarChar() : 가변 길이 문자열로, ()안에 최대 길이를 설정해줄 수 있다.
  • @db.Date : 날짜와 시간 타입으로 지정한다.

createdAt의 경우, 레코드가 생성된 시간인 now()를 기본값으로 주었고 updatedAt의 경우, prisma에서 지원하는 @updatedAt를 사용하여 레코드가 마지막으로 업데이트된 시간을 자동으로 저장했다.

enum은 열거형으로 값을 제한하고 싶을 때 사용한다. provider의 경우 회원가입 종류를 저장하는데, local 회원가입과 google 로그인 / kakao 로그인 이 세가지 경우 밖에 없기 때문에 enum으로 제한해주었다.

@relation는 다른 테이블과의 관계를 나타낼 수 있다. 속성으로 다른 테이블에서 참조 받고 있는 외래키(user_id)와 fields와 참조하고 있는 다른 테이블(User)의 기본키인 reference를 기본적으로 가진다.

기본적으로 관계는 쌍방이어야하기 때문에, 참조하고 있는 테이블인 User에도 연결을 해주어야한다. User : PillBookMark 의 관계는 1:n의 관계이기 때문에 참조하고 있는 PillBookMark에 배열 표시를 넣어주었다.

또한 만약 user가 삭제될 경우 그 회원의 북마크는 자동으로 삭제되도록 cascade로 설정해주었다. 물론 soft delete라 user가 삭제될 일이 있을까 하지만...

코드를 모두 작성했다면 DB에 적용해야한다.

$ npx prisma db push
$ npx prisma db pull

이 둘은 prisma를 사용하면서 가장 많이 쓰게될 명령어이며, 내가 Git과 비슷하다고 생각한 이유이기도 하다. push 명령은 DB에 현재 코드를 반영하는 것이고 pull은 반대로 현재 DB에 적용된 코드를 prisma.schema에 가져오는 것이다. 협업할 때 굉장히 편리하다.

Migration

migration은 데이터베이스 스키마가 발전함에 따라, Prisma에서 작성한 스키마와 동기화된 상태로 유지하는 것이다. 즉 중간 세이브이다.

$ npx prisma migrate dev --name init

방법은 간단하다. 세이브할 이름(init)을 지정하고 migrate해주면 된다.

보통 migration을 하면 데이터베이스의 기존 데이터 유지할 수 있는데, 때때로 이런 데이터베이스가 초기화된다는 문구가 나오기도 한다.

ㅎㄷㄷ

개발 환경에서 데이터 손실을 감안하고 migrate를 시도해야하는 경우가 생기게 된다는 것이다. 물론 프로덕션 환경에서는 발생하지 않는 문제라고 하니 안심해도 될 것 같다.

그렇다고 migration을 너무 안하면 동기화가 잘 안되어서 하나씩 빼먹고 들어오거나 prisma 에러가 뜰 수 있으니 주의하는 것이 좋겠다.

이걸 어떻게 아냐구요?

거기엔 아주 슬픈 전설이 있어...

그리고, 만약 개발 도중 스키마를 변경했는데 계속 빨간줄이 뜨면서 에러가 뜬다면 vscode를 껐다 켜보자. 아마 높은 확률로 에러가 잡힐 것이다.

Prisma Studio 사용하기

Prisma의 장점 중 하나인 UI이다. 보통 DB Tool을 사용하여 DB를 관리하는데 Prisma는 독자적인 UI를 가지고 있어 관리에 활용할 수 있다.

$ npx prisma studio

위의 명령어를 입력하면 http://localhost:5555 에 UI가 생성된다.

사실 나는 이 UI를 딱히 활용하지 않았다. 다만 프론트엔드에서 특별히 DB tool을 사용하지 않고도 쉽게 DB에 접근하여 테스트를 해볼 수 있다는 점에서 아주 좋은 것 같다.

전역 Prisma Module 생성하고 설정하기

prisma client를 사용하여 데이터베이스에 쿼리를 보내기 위해 사전에 해줘야하는 작업이 있다.

$ nest g mo prisma
$ nest g s prisma

module과 service를 만들어준다. 이 부분은 Nest 공식문서에 자세히 나와있다. Nest 공식문서 만세

prisma.module.ts

import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';

@Global()
@Module({
  providers: [PrismaService],
  exports: [PrismaService],
})
export class PrismaModule {}

Module을 전역으로 설정해주면 다른 Module에서 prisma에 연결하는 서비스를 굳이 import하지 않아도 사용할 수 있다.

prisma.service.ts

import {
  INestApplication,
  Injectable,
  OnModuleDestroy,
  OnModuleInit,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService
  extends PrismaClient
  implements OnModuleInit, OnModuleDestroy
{
  constructor(private readonly configService: ConfigService) {
    super({
      datasources: {
        db: {
          url: configService.get('DATABASE_URL'),
        },
      },
    });
  }

  async onModuleInit() {
    await this.$connect();
  }
  
  async onModuleDestroy() {
    await this.$disconnect();
  }

  async enableShutdownHooks(app: INestApplication) {
    this.$on('beforeExit', async () => {
      await app.close();
    });
  }
}

onModuleInit을 설정해주면 Prisma는 데이터베이스에 대한 첫 번째 호출에서 빠르게 연결할 수 있다.

enableShutdownHooks는 종료 신호를 수신하고 종료 후크가 실행되기 전에 신호에 다른 리스너가 있는 경우 현재 프로세스를 죽이지 않는 설정을 해주는 것이다. 이걸 적용하기 위해서는 main.ts에도 설정이 필요하다.

main.ts

const prismaService = app.get(PrismaService);
await prismaService.enableShutdownHooks(app);

이제 prisma를 사용할 준비가 끝났다!

왜 DBeaver를?

Prisma의 경우 DB를 관리할 수 있는 UI를 제공하지만 보통 데이터베이스를 관리하기 위해 DB tool을 사용한다. postgres의 경우 pdamin을 자주 쓴다고 한다.

하지만 우리 프로젝트는 아니긴 한데, 두 개 이상의 데이터베이스를 사용하는 경우도 있다. 이럴때 여러 데이터베이스를 한번에 효율적으로 관리하기 위해서 사용하는 것이 바로 DBeaver이다.

데이터베이스에 쿼리를 보내기도 편하고 직관적인 인터페이스로, 사실 내가 이전부터 사용하던 툴이라서 이번에도 슬쩍 쓰게 되었다...^^

물론 Prisma Studio 덕에 반드시 사용할 필요는 없어보인다!

Dbeaver 다운로드 링크

DBeaver로 Postgres 연결하기

설치를 하고 Dbeaver를 열면 데이터베이스를 관리할 수 있는 화면이 나온다. 그럼 위에서 만든 DB를 등록해주자.

왼쪽 DB 리스트가 있는 부분에 마우스 오른쪽 버튼을 누르고 Create - Connection을 클릭한다.

그러면 창이 하나 뜨는데, 거기서 PostgreSQL를 선택하면 된다.

위에서 기록해둔 정보를 다시 써야한다. Host에는 Endpoint를, Username와 password도 써준다.

왼쪽 아래에 Test Connection을 클릭했을때 아래와 같이 Connected라고 나오면 제대로 입력했다고 할 수 있다.

완료를 눌러주면 DB가 등록된다.

Dbeaver의 자주 쓰는 단축키는 F5(새로 고침)과 F4(해당 table 보기)이다. SQL query를 쓰지 않고도 데이터를 삭제하거나 삽입하는 것도 가능하며, 엔티티 관계도도 볼 수 있다.

아주 유용하고 편리한 툴이니 DB를 효율적으로 관리하고 싶다면 꼭 사용하길!

회고

데이터베이스까지 세팅했으니 이제 초기 세팅은 정리가 끝났다! DB 세팅은 잘해두면 나중에 에러가 생기는 것을 많이 방지할 수 있다.

나중에 다른 프로젝트를 진행하게 된다면 참고할 수 있도록 작성하고 있다. 포스팅을 통해 현재 부족한 부분도 계속 추가하고 있으니 공부도 되고 좋은 것 같다.

reference

https://docs.nestjs.com/
https://aws.amazon.com/ko/lightsail/
https://www.prisma.io/
https://dbeaver.io/download/

profile
backend developer / 꾸준히 배우고 적용하는걸 좋아합니다!

2개의 댓글

comment-user-thumbnail
2022년 8월 22일

잘 읽고 갑니다 굿

답글 달기
comment-user-thumbnail
2022년 8월 23일

잘 읽고 갑니다 굿

답글 달기