서론
Nest.js를 이용하여 여러 프로젝트를 진행하였다. 매 프로젝트마다 TypeORM, Mongoose 등 다양한 ORM을 이용하여 프로젝트를 진행하였는데, 여러 Nest.js 프로젝트들을 살펴보던 중 Prisma를 도입한 프로젝트가 상당히 많은 것을 확인하게 되었고, 도대체 어떤 장점이 있길래 기존 ORM을 대체하여 Prisma를 도입하였는지 궁금하게 되었다.
그래서, Prisma는 다른 ORM과 어떤 장단점이 존재하는지 확인해보고, 학습하게된 내용을 하나씩 정리하는 것을 목표로 글을 작성해보고자 한다.
ORM(Object Relational Mapping)
ORM(Object Relational Mapping)은 이름 그대로 객체(Object)와 관계형 데이터베이스(RDB, Relation Database)를 매핑하는 도구이다.
ORM을 도입하게 된다면, 데이터베이스의 데이터에 접근하기 위해 INSERT
, SELECT
SQL을 작성하는 것이 아니라, 객체 지향적인 방식으로 처리할 수 있게 된다. 이를 통해 개발자는 개발을 하면서 SQL이 아닌 코드를 통해 데이터베이스를 조작하게 되며, 이 과정에서 객체 지향 프로그래밍(OOP, Object Oriented Programming)의 장점을 극대화 하게 된다.
Node.js ORM은 JavaScript의 객체(Object)를 사용하고, 관계형 데이터베이스의 테이블(Table)을 사용한다. 그로인해, 데이터베이스의 데이터를 SQL이 아닌, JavaScript의 객체를 통해 접근하여 사용할 수 있게 된다.
ORM의 장점
ORM의 도입은 데이터베이스를 다루는 작업에 객체 지향적인 접근을 가능하게 만든다. SQL이 아닌, 순수 코드로만 작성되어 있기 때문에 코드의 가독성을 향상시키고, 재사용성과 유지보수의 편리성을 증가시키는 장점을 가지게 된다.
이를 통해, SQL을 사용하지 않고도 데이터베이스를 사용할 수 있으며, 복잡한 SQL 쿼리 작성에 대한 부담을 줄여주는 장점을 가지고 있다.
만약, 우리가 ORM을 도입하지 않고 개발을 진행하게 된다면, 비즈니스 로직에 집중하지 못하고 SQL을 작성하기 위한 리소스 투입과 러닝 커브가 더욱 커지게 될 것이다.
결론적으로, 개발자는 SQL이 아닌 실제 데이터가 어떻게 동작하는지에 대해 관심을 가져야하는데, ORM을 도입하게 된다면 이런 문제점을 명확하게 해결할 수 있게 된다.
ORM의 문제점
그러나 ORM에는 장점만이 존재하는 것은 아니다. 만약, 복잡한 쿼리를 사용하게 될 경우 ORM이 DBMS에서 고유하게 제공하는 기능이나 SQL의 전체적인 기능을 제공하지 않을 수도 있으며, 때로는 비정상적인 쿼리로 인해 성능 이슈가 발생할 수도 있다.
그리고 ORM은 SQL을 사용하지 않으므로, 추상화의 레벨을 높여주지만, 이로 인해 데이터베이스의 세부 사항에 대한 이해도가 떨어질 수 있게 된다.
ORM과 객체 지향 프로그래밍(OOP)
ORM은 객체 지향 프로그래밍(OOP)과 관계형 데이터베이스(RDB) 사이에서 패러다임의 불일치를 해결하는데 도움이 된다.
객체 지향 프로그래밍의 경우 데이터와 기능을 하나의 객체라는 단위로 묶는 것을 중점으로 두는 패러다임을 가지고 있는 반면, 관계형 데이터베이스는 데이터를 테이블에 구조화하여 저장하는 것을 중점으로 둔다. 여기서, 서로간의 패러다임의 불일치가 발생하게 된다.
만약, ORM을 사용하게 된다면 이 두 가지 접근 방식 사이의 차이를 최소화하면서 데이터베이스를 조작할 수 있게 될 것이다.
Prisma
Prisma는 Node.js와 TypeScript에서 사용할 수 있는 ORM(객체 관계 매핑, Object Relational Mapping)이다.
TypeORM vs Prisma: 타입 안정성(Type Safety)
Node.js의 ORM인 TypeORM과 Prisma를 비교한다면, TypeORM은 데이터베이스의 데이터를 조회하거나 사용하는 경우, 원하는 타입이 반환되지 않는 문제가 발생하기도 한다. 이는 프로그래밍에서 타입 안정성(Type Safety)을 보장하는 데 문제가 될 수 있다. → 여기서 타입 안정성(Type Safety)은 프로그램의 변수 또는 객체가 항상 예상한 타입의 값을 가지도록 하는 것을 뜻한다.
하지만, Prisma는 이런 문제를 해결하고, 타입 안정성을 보장함으로써 TypeORM보다 더욱 프로그래밍을 안전하게 만들 수 있게 된다.
이로인해, Prisma는 다른 ORM에 비해 실행 속도가 느리다는 단점이 존재한다. 하지만, 이런 단점이 존재하더라도, 개발을 보다 쉽게할 수 있도록 도와주는 개발 친화적인 측면에서는 많은 장점이 존재하게 된다. 이를 통해 개발자는 보다 편리하게 데이터베이스를 다룰 수 있게 될 것이다.
Query Engine
Prisma의 가장 큰 특징 중 하나는 데이터베이스와의 통신 방식에 존재한다. 다른 일부 ORM들은 어플리케이션 레벨에서 직접 SQL을 생성하고, 데이터베이스에게 전달하여 실행하는 반면, Prisma는 자체적으로 개발한 Query Engine을 사용한다는 특징이 존재한다.
해당하는 Query Engine은 Rust로 작성되어 있으며, 어플리케이션과 데이터베이스 사이에서 데이터 매핑의 역할을 수행하게 된다. 이런 방식을 통해 Prisma는 높은 수준의 타입 안정성을 보장하면서도, 효율적인 데이터베이스 작업을 가능하게 하는 장점이 존재하게 된다.
DataMapper ORM
DataMapper ORM은 객체와 데이터베이스 사이에 매퍼(Mapper)를 두는 패턴이다. 해당하는 패턴을 사용한다면 객체와 데이터베이스 사이의 결합을 느슨하게 할 수 있어, 코드의 유지보수성을 높일 수 있게 되는 장점을 가지게된다.
Prisma는 이 패턴을 따르고 있으며, 이를 통해 개발자가 코드를 쉽게 유지보수 할 수 있도록 도와주는 장점을 가지고 있다.
Active Record ORM
반면에 Active Record ORM은 객체가 자신을 데이터베이스에 저장하고, 자신의 정보를 데이터베이스에서 불러오는 책임을 가지는 패턴이다. 해당하는 패턴은 객체의 상태와 행동이 한 곳에서 관리되므로, 코드가 간결해지게 되는 장점이 존재하지만, 객체와 데이터베이스가 밀접하게 연결되어 있어, 코드의 변경에 대한 유연성이 떨어진다는 단점이 존재하게 된다.
Prisma 시작하기
# 전역 환경에서 Nest.js CLI를 설치한다.
$ npm install -g @nestjs/cli
# nestjs의 CLI를 이용하여 prisma-nestjs 프로젝트를 생성한다.
# 프로젝트를 pnpm Package Manager를 이용하여 생성한다.
$ nest new prisma-nestjs
# 생성한 Nest.js 프로젝트로 이동한다.
$ cd prisma-nestjs
# pnpm Package Manager를 이용하여 prisma와 @prisma/client 패키지를 설치한다.
$ pnpm add prisma @prisma/client
# prisma를 이용하여 prisma를 초기화한다.
$ npx prisma init
가장 먼저 Prisma를 Nest.js 프로젝트에서 실행하기 위해 Nest.js 프로젝트를 생성하고, 생성한 프로젝트에서 prisma를 초기화 하도록 구성하였다.
이런 방식으로 프로젝트를 생성하게 된다면, 아래와 같은 폴더 구조를 가지게 될 것이다.
prisma-nest
├── README.md
├── nest-cli.json
├── package.json
├── pnpm-lock.yaml
├── prisma
│ └── schema.prisma
├── src
│ ├── app.controller.spec.ts
│ ├── app.controller.ts
│ ├── app.module.ts
│ ├── app.service.ts
│ └── main.ts
├── .env
├── tsconfig.build.json
└── tsconfig.json
Prisma의 구성요소
Prisma는 다른 ORM과 달리 특별한 문법을 사용하여 모델(Model)과 스키마(Schema)를 정의한다. 이러한 문법은 Prisma에서 제공하는 schema.prisma
파일에서 사용된다.
Prisma의 특별한 문법을 사용하여 모델을 정의하는 것은 TypeORM과 Mongoose와 같이 다른 ORM을 사용하여 모델을 정의하는 것과 크게 다르지 않다. 즉, Prisma에서도 데이터의 구조와 관계를 쉽게 정의할 수 있다는 것이다.
schema.prisma
파일은 기본적으로 다음 세 가지 주요 구성요소로 이루어져 있다.
datasource
: 데이터베이스에 대해 정의하기 위해 사용한다. 데이터베이스의 종류와 연결 정보를 설정할 수 있다.generator
: Prisma 클라이언트를 어떤 방식으로 생성할 것인지 설정하기 위해 사용한다. 일반적으로 여기에서는 사용할 언어(ex: TypeScript)와 필요한 클라이언트 옵션을 지정한다.
→ 일반적인 Prisma Client 뿐만 아니라, 커뮤니티 generator 또한 사용할 수 있다.model
: 관계형 데이터베이스(RDB)의 테이블(Table)또는 NoSQL의 컬렉션(Collection)을 정의하기 위해 사용한다. 각 테이블이나 컬렉션의 필드와 데이터 유형과 관계 설정 등을 정의한다.
이러한 구성요소 들은 Prisma를 사용하여 데이터베이스와 상호작용할 때 중요한 역할을 하게된다. 다음 섹션에서는 이러한 구성요소들을 사용하여 실제로 Prisma와 데이터베이스를 어떻게 연동하는지 확인해보도록하자.
Docker-Compose
# docker-compose.yml
version: '3'
services:
mysql:
image: mysql:8.0
container_name: prisma_nest_mysql
ports:
- 3384:3306
environment:
MYSQL_ROOT_PASSWORD: 1234
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
Prisma를 사용하기 전에, Prisma가 연결할 수 있는 관계형 데이터베이스가 필요하다. 여기서는 Docker Compose를 이용하여 MySQL 데이터베이스 서버를 생성해보도록 하겠다.
위의 작성된 docker-compose.yml
파일을 생성하고, Docker Compose를 이용하여 MySQL을 도커에 실행해보자.
# docker compose를 이용하여 백그라운드에서 도커를 띄운다.
$ docker-compose up -d
여기서 포트 ‘3384’는 호스트 시스템의 포트이고, ‘3306’은 Docker 컨테이너 내의 MySQL 서버의 포트이다. 이들은 서로 매핑되어 있으므로, 호스트 시스템 즉, Docker를 업로드한 환경에서 ‘3384’ 포트를 통해 MYSQL 서버에 접근할 수 있게된다.
다음으로, 생성된 .env
파일에서 Docker Compose로 생성한 MySQL에 연결할 수 있도록 코드를 수정해야한다. .env
파일은 프로젝트의 루트 디렉토리에 위치해야 Prisma가 인식할 수 있다.
# .env
# 3384 포트 번호로 생성된 MySQL과 연결한다.
# root Id와 1234 패스워드로 연결한다.
# 데이터베이스는 prisma_nest_database로 설정한다.
DATABASE_URL="mysql://root:1234@localhost:3384/prisma_nest_database"
위에 DATABASE_URL 설정에 정의된 prisma_nest_database
는 prisma CLI를 이용하여 데이터베이스에 업로드(push)할 경우 자동으로 생성된다.
schema.prisma와 데이터 모델링
// schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql" // DB를 MySQL로 연결한다.
url = env("DATABASE_URL")
}
model Products {
id Int @id @default(autoincrement())
name String @db.VarChar(255)
price Int @db.Int
createdAt DateTime @default(now()) @db.DateTime(0)
updatedAt DateTime @updatedAt
@@map("Products")
}
schema.prisma
파일을 확인하면, Products
라는 테이블을 정의한 것을 확인할 수 있다. 이 Products
모델은 실제 데이터베이스의 Products
테이블과 매칭되게된다.
Products
모델에 정의된 각 필드의 의미는 아래와 같다.
id
: 기본키(Primary Key)이며, Auto Increment 설정을 가지고 있다.name
: Product의 이름을 저장하는varchar
타입의 컬럼이다.price
: Product의 가격을 저장하는Int
타입의 컬럼이다.createdAt
: 데이터 생성 시간을 저장하는DateTime
타입의 컬럼이다. 데이터 생성 시 현재 시간이 자동 저장된다.updatedAt
: 데이터 수정 시간을 저장하는DateTime
타입의 컬럼이다. 데이터 수정 시 현재 시간이 자동 저장된다.
그렇다면, 이제 이 테이블을 실제로 MySQL에 반영해보자. 그리고 Prisma Client 모듈도 생성해보도록하자.
# prisma.properties에 정의된 모델을 바탕으로 MySQL에 정보를 업로드한다.
# 해당 명령어를 실행하면, 실제 데이터베이스에 schema.prisma에 정의된 테이블과 컬럼이 생성된다.
# MySQL에 데이터베이스가 존재하지 않는다면, 해당 데이터베이스 또한 생성한다.
$ prisma db push
# prisma.properties에 정의된 모델을 바탕으로 prisma Client Module을 업로드한다.
# 해당 명령어를 실행하면, Prisma Client를 사용하여 데이터베이스와 상호작용할 수 있게 된다.
# 참고로, 'prisma db push'를 실행하면 이 명령어는 내부적으로 자동 실행된다.
$ prisma generate
MySQL에서 확인하였을 때, 해당 테이블이 원하는 컬럼을 가진채로 생성된 것을 확인할 수 있다.
Prisma Module 이해하기
Prisma는 prisma db push
명령어를 사용한다면, 개발자가 schema.prisma
파일에서 정의한 모델에 대한 클라이언트 API를 자동으로 생성한다. 이렇게 생성된 클라이언트 API는 node_modules
내부에 존재하는 prisma/client 폴더에 위치하게 된다. 이를 통해 개발자는 데이터베이스와 상호작용을 할 수 있게된다.
그렇다면, 이번에는 이전에 생성한 Products
모델에 대한 클라이언트 API가 어떻게 생성되었는지 확인해보도록 하자.
// ~/node_modules/.pnpm/@prisma+client@5.0.0_prisma@5.0.0/node_modules/.prisma/client/index.d.ts
export type PrismaPromise<T> = $Public.PrismaPromise<T>
export type ProductsPayload<ExtArgs extends $Extensions.Args = $Extensions.DefaultArgs> = {
name: "Products"
objects: {}
scalars: $Extensions.GetResult<{
id: number
name: string
price: number
createdAt: Date
updatedAt: Date
}, ExtArgs["result"]["products"]>
composites: {}
}
...
ProductsPayload
라는 타입이 생성되며, 해당하는 타입을 바탕으로 실제 개발을 시작할 수 있게 된다.
위의 코드에서 ProductPayload
라는 타입이 생성된 것을 확인할 수 있다. 해당하는 타입은 Products
테이블에 대한 데이터를 다룰 때 사용되며, 이를 통해 데이터의 안정성을 보장받을 수 있게 된다.
또한, Prisma는 JavaScript의 Promise
와는 약간 다른 PrismaPromise<T>
라는 제네릭 타입을 제공한다. 해당 타입은 Prisma의 Query Builder에 의해 반환되며, JavaScript의 Promise
에는 존재하지 않는 추가적인 기능을 제공한다. 예를 들어, 하나의 쿼리에서 여러 작업을 동시에 수행하는 기능이 존재하기도 한다.
Nest.js에서 Prisma 적용하기
이제 우리의 node_modules
폴더에 Prisma Mapper가 정상적으로 생성된 것을 확인해보았다. 그러면 이제 Nest.js 프로젝트에서 Prisma를 사용하기 위한 설정을 진행해보도록 하자.
폴더와 파일 생성
가장 먼저, 프로젝트의 root 폴더(src
)에서 prisma
라는 이름의 폴더를 생성한다. 그리고 해당하는 폴더 안에 prisma.service.ts
라는 파일을 생성하자. 이 파일은 Prisma Client를 Nest.js에서 사용하기 위한 서비스 파일이다.
// src/prisma/prisma.service.ts
import { Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
constructor() {
super({
log: ['query'],
});
}
async onModuleInit() {
await this.$connect();
}
}
이 서비스 파일에서 Prisma Client가 확장하여 새로운 서비스를 생성하고 있다. Nest.js의 모듈 시스템이 시작될 때 해당하는 서비스가 초기화되어 데이터베이스에 연결된다.
의존성 주입(DI, Dependency Injection)
그 다음, 생성한 PrismaService
를 app.module.ts
에 등록하여 Nest.js의 의존성 주입 시스템에 추가하도록 하자. 이렇게 함으로서 Nest.js 어플리케이션 전체에서 PrismaService
를 사용할 수 있게 된다.
// src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PrismaService } from './prisma/prisma.service';
@Module({
imports: [],
controllers: [AppController],
providers: [AppService, PrismaService],
})
export class AppModule {}
Controller와 Service
이제, app.controller.ts
파일에 새로운 API를 추가해보도록하자. 여기서는 GET /products
에 해당하는 API를 추가한다. 이 API는 AppService
를 통해 데이터를 가져온다. Nest.js에서는 Controller가 HTTP 요청을 처리하고, Service가 비즈니스 로직을 처리하는 역할을 수행한다.
// src/app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
@Get('/products')
async getProducts() {
return await this.appService.getProducts();
}
}
마지막으로, app.service.ts
파일에서 getProducts
메서드를 구현해보도록하자. 해당하는 메서드는 Prisma Client를 이용해 데이터베이스에서 Products
테이블의 정보를 조회한다.
// src/app.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from './prisma/prisma.service';
@Injectable()
export class AppService {
constructor(private readonly prismaClient: PrismaService) {}
getHello(): string {
return 'Hello World!';
}
async getProducts() {
return await this.prismaClient.products.findMany();
}
}
API 테스트
이제 생성한 API가 정상적으로 동작하는지 확인해보자. 아래의 명령어를 터미널에서 실행하면, GET /products
요청을 서버로 보낼 수 있다.
# localhost:3000/products 주소에 GET 요청을 보낸다.
curl -X GET "http://localhost:3000/products"
이렇게 하면, 서버가 Prisma를 이용하여 데이터베이스에 SELECT
쿼리를 실행하고 그 결과를 반환하는 것을 확인할 수 있게 된다. 이 과정에서 Prisma는 우리가 설정한 데이터베이스 스키마에 따라 타입 안정성(Type Safety)을 보장하며, 데이터베이스와 상호작용할 수 있게 만들어준다.
최종적으로 프로젝트는 아래와 같은 폴더 구조를 가지게 될 것이다.
prisma-nest
├── README.md
├── docker-compose.yml
├── nest-cli.json
├── package.json
├── pnpm-lock.yaml
├── prisma
│ └── schema.prisma
├── src
│ ├── app.controller.spec.ts
│ ├── app.controller.ts
│ ├── app.module.ts
│ ├── app.service.ts
│ ├── main.ts
│ └── prisma
├── .env
├── tsconfig.build.json
└── tsconfig.json
'분석과 탐구' 카테고리의 다른 글
Terraform으로 생성한 IAM Role은 비정상일까? (0) | 2024.10.13 |
---|---|
글또 8기 회고와 9기의 목표 (0) | 2023.12.10 |
아키텍처 패턴 그리고 헥사고날 아키텍처 (0) | 2023.07.02 |
제로부터 시작하는 DDD를 위한 이벤트스토밍 (0) | 2023.06.18 |
Amazon API Gateway의 WebSocket API란 무엇인가? (0) | 2023.05.21 |