Nx를 이용한 Nestjs 마이크로서비스 구성하기

atesi·2023년 9월 9일
1

Retrograde

목록 보기
2/3

Retrograde 두번째 이야기로 간단한 예제와 함께 프로젝트의 구조를 작성해 보겠습니다.

1. Nx Setup

npx create-nx-workspace

프로젝트의 이름을 설정해 줍니다. - example
Nestjs를 이용한 microservice를 구성하기 위해 node -> nest를 선택해 줍니다.
Nx에서 제공하는 Integrated Monorepo를 사용할 예정입니다.
app의 이름을 설정해줍니다.
이번 예제에서는 Docker 설정과 Nx에서 제공하는 Cache사용을 위한 Cloud를 사용하지 않을 예정입니다.
완료 후 생성된 프로젝트의 구조입니다.

기본적인 모노레포 구성이 완료 되었습니다.

2. mircoservice setup

다음은 마이크로서비스 구성을 시작해 봅시다. 이번에 구성할 마이크로서비스는 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"
  },

서버를 실행시키고 요청을 확인해 봅시다.

3. Prisma in Monorepo

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와 테스트를 위한 기본적인 메소드를 작성해줍니다.

4. Postgresql with Docker

프리즈마 설정을 계속하기 위해 프로젝트의 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

5. 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?
}

이전에 작성해둔 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

profile
Action!

0개의 댓글