Retrograde 두번째 이야기로 간단한 예제와 함께 프로젝트의 구조를 작성해 보겠습니다.
npx create-nx-workspace
프로젝트의 이름을 설정해 줍니다. - example
Nestjs를 이용한 microservice를 구성하기 위해 node -> nest를 선택해 줍니다.
Nx에서 제공하는 Integrated Monorepo를 사용할 예정입니다.
app의 이름을 설정해줍니다.
이번 예제에서는 Docker 설정과 Nx에서 제공하는 Cache사용을 위한 Cloud를 사용하지 않을 예정입니다.
완료 후 생성된 프로젝트의 구조입니다.
기본적인 모노레포 구성이 완료 되었습니다.
다음은 마이크로서비스 구성을 시작해 봅시다. 이번에 구성할 마이크로서비스는 NestJS 기반 Gateway Pattern을 이용해서 구성하겠습니다.
nx g mv --project api api-gateway # 기존 생성된 api앱 이름 api-gateway로 변경
nx g @nrwl/nest:app api-auth # 새로운 api-auth앱 생성
우선 만든 api 앱을 api-gateway로 변경하고 auth서비스를 추가하겠습니다. api-gateway의 경우 클라이언트 앱에 대한 단일 엔드포인트를 제공하며 내부 마이크로서비스로 요청을 매핑합니다.
생성된 api-auth앱으로 이동하여 리소스를 생성해줍니다. 이후 생성된 모듈을 임포트 해줍니다.
nest g resource auth
리소스 사용을 위해 패키지 설치해줍시다.
npm i @nestjs/mapped-types
api-auth의 main.ts에 들어가 기본적으로 설정되어 있는 포트를 변경해줍니다.
import { Logger } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app/app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const globalPrefix = 'api';
app.setGlobalPrefix(globalPrefix);
const port = process.env.PORT || 3310; // Port 변경
await app.listen(port);
Logger.log(
`🚀 Application is running on: http://localhost:${port}/${globalPrefix}`
);
}
bootstrap();
다음은 api-gateway의 main.ts를 수정해줍니다.
import { Logger } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app/app.module';
import { createProxyMiddleware } from 'http-proxy-middleware';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const globalPrefix = 'api';
app.setGlobalPrefix(globalPrefix);
const port = process.env.PORT || 3300;
app.use(
'/api/auth',
createProxyMiddleware({
target: 'http://localhost:3310',
changeOrigin: true,
})
);
await app.listen(port);
Logger.log(
`🚀 Application is running on: http://localhost:${port}/${globalPrefix}`
);
}
bootstrap();
app.use
메서드를 사용하여 /api/auth
엔드포인트에 대한 모든 요청을 http://localhost:3310
으로 전달하는 프록시 미들웨어를 추가합니다.
마지막으로 루트의 package.json을 수정해줍니다.
"scripts": {
"dev": "nx run-many --target=serve --projects=api-gateway,api-auth"
},
서버를 실행시키고 요청을 확인해 봅시다.
nx g @nx/js:lib prisma-schema-one --unitTestRunner=none --bundler=none --simple-name --minimal
nx g @nx/nest:lib prisma-client-one
서비스의 확장을 위해 클리이언트와 스키마를 분리하여 생성해주었습니다.
우선 스키마 설정부터 시작하겠습니다.
prisma-schema-one/
|-- prisma/
| |-- schema.prisma # 프리즈마 스키마파일 생성
|-- .eslintrc.json
|-- project.json
|-- tsconfig.json
|-- tsconfig.lib.json
불필요한 파일들을 삭제하고 prisma폴더 내에 schema.prisma를 생성해줍니다.
generator client {
provider = "prisma-client-js"
output = "../../../node_modules/@prisma/client/one"
}
datasource db {
provider = "postgresql"
url = env("DB_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
}
다음은 클라이언트입니다.
prisma-clinet-one/
|-- src/
| |-- lib/
| | |-- prisma-client-one.module.ts
| | |-- prisma.service.ts
| |-- index.ts
|-- .eslintrc.json
|-- jest.config.ts
|-- project.json
|-- README.md
|-- tsconfig.json
|-- tsconfig.lib.json
|-- tsconfig.spec.json
prisma.service.ts
import { INestApplication, Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client/one';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
async onModuleInit() {
await this.$connect();
}
// async enableShutdownHooks(app: INestApplication) {
// this.$on('beforeExit', async () => {
// await app.close();
// });
// }
}
Prisma 5 버전 이후로 enableShutdownHooks를 만들어서 이용하지 않고 nestjs에서 기본적으로 제공하는 enableShutdownHooks() 메소드를 이용하게 되어 삭제 해줍니다.
import { Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
@Module({
controllers: [],
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaClientOneModule {}
생성한 service를 모듈에 등록해줍니다.
// import { INestApplication } from '@nestjs/common';
// import { PrismaService } from './lib/prisma.service';
export * from './lib/prisma-client-one.module';
export * from './lib/prisma.service';
export { User, Prisma } from '@prisma/client/one';
// export async function registerPrismaShutdown(app: INestApplication) {
// // recommended by NestJS
// // https://docs.nestjs.com/recipes/prisma#issues-with-enableshutdownhooks
// const prismaService = app.get(PrismaService);
// await prismaService.enableShutdownHooks(app);
// }
마지막으로 index.ts입니다. 마찬가지로 app에 붙여서 사용하기 위해 작성한 registerPrismaShutdown를 삭제해줍니다. export할 것들을 정의해줍니다.
nx generate @nrwl/js:library --name=data-access-user
data-access-user/
|-- src/
| |-- lib/
| | |-- data-access-user.module.ts
| | |-- user.service.ts
| |-- index.ts
|-- .eslintrc.json
|-- jest.config.ts
|-- project.json
|-- README.md
|-- tsconfig.json
|-- tsconfig.lib.json
|-- tsconfig.spec.json
import { Injectable } from '@nestjs/common';
import {
PrismaService,
User,
Prisma,
} from '@retrograde/prisma-client-one';
@Injectable()
export class UserService {
constructor(private prisma: PrismaService) {}
async user(userWhereUniqueInput: Prisma.UserWhereUniqueInput) {
return this.prisma.user.findUnique({
where: userWhereUniqueInput,
});
}
async users(options: {
skip?: number;
take?: number;
cursor?: Prisma.UserWhereUniqueInput;
where?: Prisma.UserWhereInput;
orderBy?: Prisma.UserOrderByWithRelationInput;
}) {
const { skip, take, cursor, where, orderBy } = options;
return this.prisma.user.findMany({
skip,
take,
cursor,
where,
orderBy,
});
}
async createUser(data: Prisma.UserCreateInput) {
return this.prisma.user.create({
data,
});
}
async updateUser(options: {
where: Prisma.UserWhereUniqueInput;
data: Prisma.UserUpdateInput;
}) {
const { where, data } = options;
return this.prisma.user.update({
data,
where,
});
}
async deleteUser(where: Prisma.UserWhereUniqueInput) {
return this.prisma.user.delete({
where,
});
}
}
user.service.ts입니다. db와 테스트를 위한 기본적인 메소드를 작성해줍니다.
프리즈마 설정을 계속하기 위해 프로젝트의 DB를 설정해야 합니다. ORDBMS인 postgresql를 도커 컨테이너에 실행시켜 사용하도록 하겠습니다.
version: '3.9'
services:
postgres:
image: postgres
container_name: postgres
ports:
- '${DB_PORT}:5432'
volumes:
- pgdata:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
PGDATA: /var/lib/postgresql/data/pgdata
# networks:
# - app-network
# networks:
# app-network:
# driver: bridge
volumes:
pgdata:
프로젝트의 루트에 docker-compose.yaml을 작성해주었습니다. 개발 환경 변수를 위한 .local.env 파일도 생성해 줍니다.
docker-compose --env-file .local.env up -d
이제 남은 프리즈마 설정을 이어 가겠습니다.
generator client {
provider = "prisma-client-js"
output = "../../../node_modules/@prisma/client/one"
}
datasource db {
provider = "postgresql"
url = env("DB_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
}
이전에 작성해둔 schema를 살펴보면 DB_URL를 작성하는 곳이 있습니다. 저희가 루트에 만들어준 .local.env파일에 DB_URL변수를 추가해 줍니다.
DB_URL=postgresql://[username]:[password]@[host]:[port]/[database_name]
username을 설정하지 않았다면 기본적으로 postgres를 이용하면 됩니다.
npm i -D prisma env-cmd # prisma, env-cmd 패키지 설치
npm i @prisma/client # prisma/client 패키지 설치
"scripts": {
"dev": "nx run-many --target=serve --projects=api-gateway,api-auth",
"db:dockers:dev": "docker-compose -f docker-compose.yaml up -d --no-recreate --remove-orphans", // 도커 컴포즈 명령어
"db:migrate:dev": "npx env-cmd -f .local.env npx prisma migrate dev", // migrate
"db:studio": "npx env-cmd -f .local.env npx prisma studio" // prisma studio
},
"prisma": {
"schema": "libs/prisma-schema-one/prisma/schema.prisma"
}
Prisma CLI를 사용할 때 package.json에 스키마 경로를 설정해주어 명시적으로 경로를 지정하지 않게 합니다.
prisma format # 스키마 포맷
yarn db:migrate:dev
여기까지 Nx를 사용한 모노레포 위에 Nestjs 마이크로서비스(with Prisma)의 기본적인 설정을 완료 했습니다.
참고
https://www.prisma.io/docs
https://mobicon.tistory.com/586
Monolithic to Microservices Architecture with Patterns & Best Practices