블로그 이미지
윤영식
Full Stacker, Application Architecter, KnowHow Dispenser and Bike Rider

Publication

Category

Recent Post

2021. 9. 24. 19:48 React/Architecture

MS-4/5/6 글을 통해 Gateway의 공통 기능 구현을 위한 설정을 적용한다.

  • Login 할때 사용자 정보는 ORM 기반으로 처리한다.
  • 인증을 JWT 기반으로 처리한다.
  • Login 화면을 React 기반 구현한다.

 

NestJS에 Prisma ORM 설정

gateway-api의 공통 기능은 다음과 같고, JWT처리를 위한 사용자 정보 조회를 위해 Prisma ORM을 적용한다. Micro Service도 Prisma를 사용할 것이다.

  • api gateway: TCP 통신으로 micro service의 API를 연결한다. 
  • http server: gateway의 공통 기능중 Login을 서비스한다.
  • reverse proxy: Login이 성공하면 dashboard micro service로 이동한다. (configuration, back-office)
  • auth server: JWT 기반으로 token을 발행하고, 요청에 대한 모든 Authorization(인가, 권한)을 체크한다.

 

Step-1) 설치 및 환경 설정

auth server 기능에서 사용자 정보 데이터처리를 위해 ORM으로 Prisma를 사용을 위해 패키지를 (v3.1.1) 설치한다.

$> yarn add -D prisma
$> yarn add @prisma/client
$> yarn add -D ts-node

전체 애플리케이션을 위한 초기 prisma 환경을 생성한다.

$> npx prisma init

✔ Your Prisma schema was created at prisma/schema.prisma
  You can now open it in your favorite editor.

Next steps:
1. Set the DATABASE_URL in the .env file to point to your existing database. If your database has no tables yet, read https://pris.ly/d/getting-started
2. Set the provider of the datasource block in schema.prisma to match your database: postgresql, mysql, sqlite, sqlserver (Preview) or mongodb (Preview).
3. Run prisma db pull to turn your database schema into a Prisma schema.
4. Run prisma generate to generate the Prisma Client. You can then start querying your database.

More information in our documentation:
https://pris.ly/d/getting-started

자동 생성된 .env 파일에 설정을 추가한다. 

# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#using-environment-variables
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server (Preview) and MongoDB (Preview).
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
#DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"

# POSTGRES
POSTGRES_USER=iot
POSTGRES_PASSWORD=1
POSTGRES_DB=rnm-stack

# Nest run locally
DB_HOST=localhost
# Nest run in docker, change host to database container name
# DB_HOST=postgres
DB_PORT=5432
DB_SCHEMA=public

# Prisma database connection
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${DB_HOST}:${DB_PORT}/${POSTGRES_DB}?schema=${DB_SCHEMA}&sslmode=prefer

VS Code의 extension을 설치한다. 

VSCode의 prisma extension

 

Step-2) schema.prisma 설정

VSCode extension이 설치되면 schema.prisma의 내용이 다음과 같이 highlighting된다. 

schema.prisma 파일 안에 Prisma 방식의 스키마를 정의한다.

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

generator client {
  provider = "prisma-client-js"
  // previewFeatures = []
}

// generator dbml {
//   provider = "prisma-dbml-generator"
// }

model User {
  id        Int   @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  email     String   @unique
  password  String
  firstname String?
  lastname  String?
  posts     Post[]
  role      Role
}

model Post {
  id        Int   @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  published Boolean
  title     String
  content   String?
  author    User?    @relation(fields: [authorId], references: [id])
  authorId  Int?
}

enum Role {
  ADMIN
  MANAGER
  USER
}

schema.prisma를 통해 Migrate SQL과 Prisma Client 파일을 자동 생성한다. Prisma Client 파일은 구현 코드에서 사용된다. (참조)

 

 

Step-3) schema 설정을 통해 sql 생성하기 

명령을 수행하면 prisma/migrations sql이 자동 실행된다. 생성된 sql을 통해 table schema를 업데이트한다. 변경점이 있으면 날짜별로 update 할 수 있는 table schema가 자동으로 생성된다. 

$> npx prisma migrate dev --create-only --name=iot

또한 DB Schema에 _prisma_migrations 테이블이 자동생성된다. 

rnm-stack 의 public에 migrations 테이블 자동 생성

테이블을 자동 생성하고 싶다. prisma db push 명령을 사용한다. (참조)

$> npx prisma db push

Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "rnm-stack", schema "public" at "localhost:5432"
🚀  Your database is now in sync with your schema. Done in 77ms
✔ Generated Prisma Client (3.1.1) to ./node_modules/@prisma/client in 61ms

Post, User 테이블 자동 생성

 

Step-4) Prisma Studio 사용하기

prisma는 내장 웹기반 studio를 제공한다. 테이블을 선택하여 조작할 수 있다.

$> npx prisma studio

 

 

NestJS 에서 PrismaClient  사용하기

Step-1) PrismaClient 생성

schema에 생성되었으면 다음으로 코드에서 Prisma 접근을 위해 PrismaClient를 생성해야 한다. (참조)

  • "npx prisma migrate dev" 명령으로 수행할 경우는 "npx prisma generate"이 필요없다. 
  • "npx prisma migrate dev --create-only" 일 경우만 수행한다.
  • schema.prisma 변경시 마다 다시 실행해 주어야 한다. (참조)

$> npx prisma generate
✔ Generated Prisma Client (3.1.1) to ./node_modules/@prisma/client in 181ms
You can now start using Prisma Client in your code. Reference: https://pris.ly/d/client
```
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
```

prisma client는 기본적으로 node_modules/.prisma/client 폴더 밑에 생성된다.

node_modules/.prisma/client 폴더

 

 

Step-2) schema.prisma가 변경이 발생할 경우

이제 "PrismaClient"를 import해서 사용할 수 있는 상태가 되었다. 만일 테이블 변경이 발생한다면 아래와 같이 수행한다. 

  • schema.prisma 파일 내역 수정
  • "npx prisma migrate" 명령 실행
  • migrations 폴더에 있는 migration sql을 database에 적용한다. 

참조: https://www.prisma.io/blog/prisma-migrate-ga-b5eno5g08d0b

 

 

Step-3) NestJS 서비스 생성

사용자 정보는 공통이므로 libs/domain 에 생성한다. (참조)

  • libs/shared/src/lib밑으로 prisma 폴더를 생성하고, prisma-client.service.ts파일을 생성한다. 
  • index.ts에 export를 추가한다. 

// prisma-client.service.ts 
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'
import { PrismaClient } from '@prisma/client'

@Injectable()
export class PrismaClientService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
  async onModuleInit() {
    await this.$connect()
  }

  async onModuleDestroy() {
    await this.$disconnect()
  }
}

// index.ts
export * from './lib/configuration/config.model';
export * from './lib/configuration/config.service';
export * from './lib/prisma/prisma-client.service';

apps/gateway/api/src/app/app.module.ts에 PrismaClientService를 추가한다.

// /apps/gateway/api/src/app/app.module.ts
import { Module } from '@nestjs/common';
import { ServeStaticModule } from '@nestjs/serve-static';
import { join } from 'path';

import { GatewayApiAppService } from '@rnm/domain';
import { PrismaClientService } from '@rnm/shared'; <== 요기

import { AppController } from './app.controller';
import { DashboardModule } from './dashboard/dashboard.module';

@Module({
  imports: [
    ServeStaticModule.forRoot({
      rootPath: join(__dirname, 'public'),
      exclude: [
        '/api/gateway*', '/api/dashboard*',
        '/dashboard*', '/configuration*', '/back-office*'
      ],
    }),
    DashboardModule
  ],
  controllers: [AppController],
  providers: [GatewayApiAppService, PrismaClientService] <== 요기
})
export class AppModule { }

gateway/api/src/app/app.controller.ts에 테스트 코드로 POST로 user를 생성하는 코드를 작성한다. 

// apps/gateway/api/src/app/app.controller.ts
import { Body, Controller, Get, Post } from '@nestjs/common';

import { GatewayApiAppService } from '@rnm/domain';
import { PrismaClientService } from '@rnm/shared';

import { Role, User as UserModel } from '@prisma/client';

@Controller('api/gateway')
export class AppController {
  constructor(
    private readonly appService: GatewayApiAppService,
    private readonly dbService: PrismaClientService
  ) { }

  @Get()
  getData() {
    return this.appService.getData();
  }

  @Post('user')
  async createUser(@Body() userData: {
    email: string,
    password: string,
    firstname: string,
    lastname; string,
    role: Role
  }): Promise<UserModel> {
    const { email, password, firstname, lastname, role } = userData;
    return this.dbService.user.create({
      data: {
        email,
        password,
        firstname,
        lastname,
        role: !role ? Role.USER : role
      }
    });
  }
}

VSCode에서 Debug창에서 Gateway를 실행하고, 디버깅을 한다.

28, 29줄에 breakpoint 

 

Postman을 실행하여 User 를 생성해 본다.

  • 새로운 Request를 만들고 POST를 선택
  • Body에 JSON 타입을 선택
  • request 값을 넣고, http://localhost:8000/api/gateway/user 호출

DB툴로 User insert가 되었는지 확인 또는 prisma studio에서 확인

test@test.com 사용자가 insert 성공!

 

소스: https://github.com/ysyun/rnm-stack/releases/tag/ms-4

 

<참조>

- Primsa on NestJS 기반 개발

https://www.prisma.io/nestjs

 

NestJS Database & Prisma | Type-safe ORM for SQL Databases

Prisma is a next-generation ORM for Node.js & TypeScript. It's the easiest way to build NestJS apps with MySQL, PostgreSQL & SQL Server databases.

www.prisma.io

- Prisma Migrate 순서

https://www.prisma.io/blog/prisma-migrate-ga-b5eno5g08d0b

 

Prisma Migrate is Production Ready - Hassle-Free Database Migrations

Prisma Migrate is ready for use in production - Database schema migration tool with declarative data modeling and auto-generated, customizable SQL migrations

www.prisma.io

- Primsa, JWT on NestJS StartKit

https://github.com/fivethree-team/nestjs-prisma-starter

 

GitHub - fivethree-team/nestjs-prisma-starter: Starter template for NestJS 😻 includes GraphQL with Prisma Client, Passport-JW

Starter template for NestJS 😻 includes GraphQL with Prisma Client, Passport-JWT authentication, Swagger Api and Docker - GitHub - fivethree-team/nestjs-prisma-starter: Starter template for NestJS 😻...

github.com

- Prisma의 다양한 DB 예제

https://github.com/prisma/prisma-examples

 

GitHub - prisma/prisma-examples: 🚀 Ready-to-run Prisma example projects

🚀 Ready-to-run Prisma example projects. Contribute to prisma/prisma-examples development by creating an account on GitHub.

github.com

posted by 윤영식
2021. 9. 23. 20:45 React/Architecture

micro service인 dashboard와 gateway간의 테스트 환경을 VS Code에 만들어 본다. 

 

VS Code 디버깅환경 설정

Step-1) package.json에 script 등록

각 애플리케이션의 build 스크립트를 등록한다. 

// package.json 
  "scripts": {
    "start": "nx serve",
    "build": "nx build",
    "test": "nx test",
    "build:gateway-api": "nx build gateway-api",
    "start:gateway-api": "nx serve gateway-api",
    "build:dashboard-api": "nx build dashboard-api",
    "start:dashboard-api": "nx serve dashboard-api",
    "build:configuration-api": "nx build configuration-api",
    "start:configuration-api": "nx serve configuration-api",
    "build:back-office-api": "nx build back-office-api",
    "start:back-office-api": "nx serve back-office-api"
  },

 

Step-2) .vscode 폴더안의 tasks.json 추가

tasks.json은 vscode의 디버깅 실행 환경설정파일인 launch.json에서 사용한다. 

{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "build:dashboard-api",
      "type": "shell",
      "command": "npm run build:dashboard-api"
    },
    {
      "label": "build:configuration-api",
      "type": "shell",
      "command": "npm run build:configuration-api"
    },
    {
      "label": "build:back-office-api",
      "type": "shell",
      "command": "npm run build:back-office-api"
    },
    {
      "label": "build:gateway-api",
      "type": "shell",
      "command": "npm run build:gateway-api"
    }
  ]
}

 

Step-3) .vscode 폴더안의 launch.json 추가

tasks.json의 설정내용은 "preLaunchTask"에 설정한다.

  • task.json의 build 를 먼저 실행한다. 빌드하면 루트폴더의 dist 폴더에 js, map이 생성된다. 
  • 이후 main.ts 소스에 breakpoint를 찍으면 디버깅을 할 수 있다. 
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Dashboard API",
      "program": "${workspaceFolder}/apps/dashboard/api/src/main.ts",
      "preLaunchTask": "build:dashboard-api",
      "outFiles": ["${workspaceFolder}/dist/apps/dashboard/api/**/*.js"]
    },
    {
      "type": "node",
      "request": "launch",
      "name": "Gateway API",
      "program": "${workspaceFolder}/apps/gateway/api/src/main.ts",
      "preLaunchTask": "build:gateway-api",
      "outFiles": ["${workspaceFolder}/dist/apps/gateway/api/**/*.js"]
    },
    {
      "type": "node",
      "request": "launch",
      "name": "Configuration API",
      "program": "${workspaceFolder}/apps/configuration/api/src/main.ts",
      "preLaunchTask": "build:configuration-api",
      "outFiles": ["${workspaceFolder}/dist/apps/configuration/api/**/*.js"]
    },
    {
      "type": "node",
      "request": "launch",
      "name": "Back-Office API",
      "program": "${workspaceFolder}/apps/back-office/api/src/main.ts",
      "preLaunchTask": "build:back-office-api",
      "outFiles": ["${workspaceFolder}/dist/apps/back-office/api/**/*.js"]
    }
  ]
}

.vscode안의 launch.json과 tasks.json 파일

 

Gateway와 Micro Service간의 TCP 통신 테스트

Step-1) gateway의 dashboard.controller.ts에서 호출

  • dashboard.controller.ts에서 서비스호출
  • dashboard.service.ts 에서 breakpoint
// apps/gateway/api/src/app/dashboard/dashboard.controller.ts 
import { Controller, Get } from '@nestjs/common';

import { Observable } from 'rxjs';
import { DashboardService } from './dashboard.service';

@Controller('api/dashboard')
export class DashboardController {
  constructor(
    private readonly dashboardService: DashboardService
  ) { }

  @Get('sum')
  accumulate(): Observable<{ message: number, duration: number }> {
    return this.dashboardService.sum();
  }
}

// apps/gateway/api/src/app/dashboard/dashboard.service.ts 
import { Injectable, Inject } from "@nestjs/common";
import { ClientProxy } from "@nestjs/microservices/client";
import { Observable } from "rxjs";
import { map } from "rxjs/operators";

@Injectable()
export class DashboardService {
  constructor(@Inject("DASHBOARD") private readonly client: ClientProxy) { }

  sum(): Observable<{ message: number, duration: number }> {
    const startTs = Date.now();
    const pattern = { cmd: 'dashboard-sum' };
    const payload = [1, 2, 3];
    return this.client
      .send<number>(pattern, payload)
      .pipe(
        map((message: number) => ({ message, duration: Date.now() - startTs }))
      );
  }
}

14줄 breakpoint

 

Step-2) Micro Service 인 dashboard에서 요청처리

  • 요청에 대한 sum 처리후 observable 로 반환
  • MessagePattern 데코레이터 적용
import { Controller, Get } from '@nestjs/common';
import { MessagePattern } from '@nestjs/microservices';

import { DashboardApiAppService } from '@rnm/domain';
import { from, Observable } from 'rxjs';

@Controller()
export class AppController {
  constructor(private readonly appService: DashboardApiAppService) { }

  @Get()
  getData() {
    return this.appService.getData();
  }

  @MessagePattern({ cmd: 'dashboard-sum' })
  accumulate(data: number[]): Observable<number> {
    console.log('calling sum from dashboard....');
    const sum = data[0] + data[1] + data[2];
    return from([sum]);
  }
}

20줄 breakpoint

 

Step-3) Dashboard, Gateway 서버 실행

디버깅으로 이동한다.

  • Dashboard API 실행
  • Gateway API 실행

실행결과

 

브라우져에서 호출

  • http://localhost:8000/api/dashboard/sum

디버깅 실행 영상

https://www.youtube.com/watch?v=UDWPnJdQUhI 

소스

https://github.com/ysyun/rnm-stack/releases/tag/ms-3

 

Release ms-3 · ysyun/rnm-stack

[ms-3] add debug config in vscode

github.com

 

posted by 윤영식
2021. 9. 20. 19:37 React/Architecture

React Nest를 기반으로 마이크로 서비스를 구축해 본다. 개발 환경은 Nx를 사용한다. Nx 환경구축은 [React HH-2] 글을 참조한다.

 

목차

  • Gateway, Dashboard등의 Application 생성
  • Application에서 사용하는 Library 생성
  • Gateway <-> MicroService Application간 통신 설정
    • api 호출: reverse proxy & tcp 설정
    • static assets 호출: static server 설정

 

Application 생성

4개의 카테고리에 각각 api, web 애플리케이션을 생성한다. 

  • gateway, dashboard, configuration, back-office
    • api, web

 

Step-1) 사전 준비환경 설치

글로벌 환경 설치를 한다. nvm 설치는 [React HH-2] 글을 참조한다.

$> npm i -g @angular/cli@latest 
$> npm i -g @nrwl/cli@latest 
$> npm i -g yarn@latest

node 버전은 nvm을 사용하여 14.18.0 를 사용한다. nvm 버전을 자동으로 설정하고 싶다면 

1) .zshrc파일에 다음 내용을 첨부한다. (Gist 코드 참조)

2) 작업 폴더에 .nvmrc 파일을 만든다.

// 코드: https://gist.github.com/ysyun/845ddd32467bb50af654e1e4605a4b50
// .zshrc
autoload -Uz add-zsh-hook
load-nvmrc() {
    local _CUR_NODE_VER="$(nvm version)"
    local _NVMRC_PATH="$(nvm_find_nvmrc)"

    if [[ -n "${_NVMRC_PATH}" ]]; then
        local _NVMRC_NODE_VER="$(nvm version "$(cat "${_NVMRC_PATH}")")"

        if [[ "${_NVMRC_NODE_VER}" == 'N/A' ]]; then
            local compcontext='yn:yes or no:(y n)'
            vared -cp "Install the unmet version ($(cat "${_NVMRC_PATH}")) in nvm (y/n) ?" _ANSWER
            if [[ "${_ANSWER}" =~ '^y(es)?$' ]] ; then
                nvm install
            fi
        elif [[ "${_NVMRC_NODE_VER}" != "${_CUR_NODE_VER}" ]]; then
            nvm use
        fi
    elif [[ "${_CUR_NODE_VER}" != "$(nvm version default)" ]]; then
        echo -e "Reverting to the default version in nvm"
        nvm use default
    fi
}
add-zsh-hook chpwd load-nvmrc
load-nvmrc

// 작업 폴더에 .nvmrc 파일을 생성하고, 버전을 기입힌다. 아래 내용 참조
// .nvmrc
14.18.0

 

Step-2) NX 개발환경 생성

create-nx-workspace@latest를 이용하고, .nvmrc 파일을 생성한다. .zshrc가 적용되고, rnm-stack 폴더에 진입하면 자동으로 14.18.0 버전이 적용된다. rnm은 React Nest Micro-service의 약어이다.

$> npx create-nx-workspace@latest
✔ Workspace name (e.g., org name)     · rnm-stack
✔ What to create in the new workspace · react
✔ Application name                    · gateway/web
✔ Default stylesheet format           · scss
✔ Use Nx Cloud? (It's free and doesn't require registration.) · No


$> cd rnm-stack
$> echo "14.18.0" > .nvmrc

다음은 api 애플리케이션을 Nest기반으로 생성한다.

// nest 제너레이터 설치
$> yarn add -D @nrwl/nest@latest

// api 생성
$> nx g @nrwl/nest:app gateway/api

다음으로 루트폴더에 있는 workspace.json 파일을 각 api와 web폴더에 project.json 파일을 생성하여 설정을 옮긴다. 이후 생성되는 애플리케이션들은 자동으로 project.json 파일로 생성된다. 

// workspace.json
{
  "version": 2,
  "projects": {
    "gateway-api": "apps/gateway/api",  <== project.json 파일의 위치 지정
    "gateway-web": "apps/gateway/web",
    "gateway/web-e2e": "apps/gateway/web-e2e"
  },
  ... 생략 ...
  "defaultProject": "gateway-web"
}

project.json 파일들

 

테스트 "nx serve gateway-web" 을 수행하면 정상적으로 dev server가 수행된다.

$> nx serve gateway-web

> nx run gateway-web:serve
>  NX  Web Development Server is listening at http://localhost:4200/

나머지 dashboard, configuration, back-office도 생성한다.

// react application
$> nx g @nrwl/react:app dashboard/web
$> nx g @nrwl/react:app configuration/web
$> nx g @nrwl/react:app back-office/web

// nest application
$> nx g @nrwl/nest:app dashboard/api
$> nx g @nrwl/nest:app configuration/api
$> nx g @nrwl/nest:app back-office/api

전체 생성 애플리케이션과 workspace.json 내역

 

Libraries 생성

다음으로 libs 폴더 하위로 라이브러리들을 생성한다. @rnm/page, @rnm/ui, @rnm/domain, @rnm/shared 로 import하여 사용한다.  

  • page
  • ui
  • domain
  • shared
$> nx g @nrwl/react:lib page --importPath=@rnm/page --publishable
$> nx g @nrwl/react:lib ui --importPath=@rnm/ui --publishable
$> nx g @nrwl/nest:lib domain --importPath=@rnm/domain --publishable
$> nx g @nrwl/nest:lib shared --importPath=@rnm/shared --publishable

libs 폴더밑으로 4개 라이브러리 생성

 

 

Micro Service Communication Channel 설정

다음은 애플리케이션간의 연결을 위한 통신부분을 설정한다. NestJS 기반으로 microservice 생성을 위해 패키지를 설치한다. NestJS버전을 7.6.18 버전으로 업데이트 한다. (현재 8.*은 microservice 버그가 있다. 작동불능)

$> yarn add @nestjs/microservices


// package.json 에서 nestjs의 모든 패키지의 버전을 업데이트 한다. 
  "dependencies": {
    "@nestjs/common": "^7.6.18", <== 요기
    "@nestjs/core": "^7.6.18", <== 요기
    "@nestjs/microservices": "^7.6.18", <== 요기
    "@nestjs/platform-express": "^7.6.18", <== 요기
    "@nestjs/serve-static": "^2.2.2",
    "core-js": "^3.6.5",
    "react": "17.0.2",
    "react-dom": "17.0.2",
    "react-router-dom": "5.2.0",
    "reflect-metadata": "^0.1.13",
    "regenerator-runtime": "0.13.7",
    "rxjs": "~6.6.3",
    "tslib": "^2.0.0"
  },
  "devDependencies": {
    "@nestjs/schematics": "^7.3.1", <== 요기, 7최신이 7.3.1
    "@nestjs/testing": "^7.6.18", <== 요기
    ...
  }
  
  
  // nestjs 모든 패키지의 업데이트 버전을 재설치한다.
  $> yarn

 

Step-1) Micro Service 설정

  • dashboard, configuration, back-office 3가지 micro service
    • http server & static server
      • dashboard: 8001, configuration: 8002, back-office: 8002 HTTP port를 사용한다.
    • api server
      • dashboard: 8100, configuration: 8200, back-office: 8300 TCP port를 사용한다.

apps/dashboard/api/src 하위로 public 폴더를 하위로 dashboard 폴더를 추가로 생성한 후 index.html 파일을 생성한다.

  • apps/dashboard/api/src/environments폴더에 config.json 파일을 생성한다.
  • apps/dashboard/api/project.json 에  "assets" 파트에 설정을 추가한다. 
    "assets": ["apps/dashboard/api/src/environments", "apps/dashboard/api/src/public"]

libs/shared/src/lib 밑에 configuration 폴더를 생성하고, config.service.ts 와 config.model.ts을 생성한다. config.json 파일을 읽기위한 용도이다. index.ts에  export도 설정한다.

// config.service.ts
import * as fs from "fs";
import { join } from 'path';
import { GatewayConfiguration, MicroServiceConfiguration } from "./config.model";

export const loadMicroServiceConfiguration = (message = '[LOAD] config.json file'): MicroServiceConfiguration => {
  console.log(`${message}:`, `${__dirname}/environments/config.json`);
  const jsonFile = fs.readFileSync(join(__dirname, 'environments', 'config.json'), 'utf8');
  return JSON.parse(jsonFile);
}

export const loadGatewayConfiguration = (message = '[LOAD] config.json file'): GatewayConfiguration => {
  console.log(`${message}:`, `${__dirname}/environments/config.json`);
  const jsonFile = fs.readFileSync(join(__dirname, 'environments', 'config.json'), 'utf8');
  return JSON.parse(jsonFile);
}


// config.model.ts 
export interface MicroServiceConfiguration {
  REVERSE_CONTEXT?: string;
  REVERSE_ADDRESS?: string;
  HTTP_PORT?: number,
  TCP_HOST?: string;
  TCP_PORT?: number,
  GLOBAL_API_PREFIX?: string;
}

export interface GatewayConfiguration {
  HTTP_PORT?: number,
  GLOBAL_API_PREFIX?: string;
  DASHBOARD?: MicroServiceConfiguration;
  CONFIGURATION?: MicroServiceConfiguration;
  BACK_OFFICE?: MicroServiceConfiguration;
}


// index.ts
export * from './lib/configuration/config.model';
export * from './lib/configuration/config.service';

apps/dashboard/api/src/main.ts 파일을 수정한다. http server를 위한 listen port를 설정하고, microservice client가 연결토록 TCP로 연결 준비를 한다. loadMicroServiceCofiguration 함수를 "@rnm/shared" 패키지로 부터 위치 투명하게 (마치 node_modules에 설치된 것처럼) import 할 수 있다.

import { Logger } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { Transport, MicroserviceOptions } from '@nestjs/microservices';

import { loadMicroServiceConfiguration } from '@rnm/shared';

import { AppModule } from './app/app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // Load config.json file
  const config = loadMicroServiceConfiguration();

  // // Setup tcp server for api
  const options: MicroserviceOptions = {
    transport: Transport.TCP,
    options: { host: config.TCP_HOST, port: config.TCP_PORT || 8100 }
  };
  app.connectMicroservice(options);
  app.startAllMicroservices();

  // // Setup http server for web
  const httpPort = config.HTTP_PORT || process.env.HTTP_PORT || 8001;
  const globalPrefix = config.GLOBAL_API_PREFIX || '/api';
  app.setGlobalPrefix(globalPrefix);
  await app.listen(httpPort, () => {
    Logger.log(`Listening at http://localhost:${httpPort}${globalPrefix}`);
  });
}

bootstrap();

확인 & 테스트 

// LISTEN 포트 확인
$> netstat -an |grep 8100
tcp4       0      0  *.8100                 *.*                    LISTEN
$> netstat -an |grep 8001
tcp46      0      0  *.8001                 *.*                    LISTEN

// gateway 실행
$> nx serve dashboard-api

// other console
$> curl -X GET "http://localhost:8001/api" 
{"message":"Welcome to dashboard/api in libs"}

configuration, back-office도 동일하게 적용한다.

 

 

Step-2) Micro Service의 HTTP port가 static file 서비스토록 설정

@nestjs/serve-static (v2.2.2) 패키지를 설치한다. (참조)

$> yarn add @nestjs/serve-static

 

apps/dashboard/api/src/app/app.module.ts 에 static 환경을 설정한다. rootPath는 public 폴더를 만든다. exclude는 global prefix인 api를 제외한다. public 폴더에 index.html 을 만들어 본다. 향후 react기반 SPA 번들파일을 위치할 것이다. 

import { Module } from '@nestjs/common';
import { ServeStaticModule } from '@nestjs/serve-static';
import { join } from 'path';

import { DashboardApiAppService } from '@rnm/domain';

import { AppController } from './app.controller';

@Module({
  imports: [
    ServeStaticModule.forRoot({
      rootPath: join(__dirname, 'public'),
      exclude: ['/api*', '/dashboard/api*'],
    }),
  ],
  controllers: [AppController],
  providers: [DashboardApiAppService],
})
export class AppModule { }

// index.html
dashboard home

재실행 후, dashboard context path로 브라우져에서 확인한다. public/dashboard/assets/images 밑으로 샘플 이미지 복사

// public/dashboard/index.html 내역

<html>
  <head>
    <base href="dashboard" />
  </head>
  <body>
    welcome to dashboard! <img src="/dashboard/assets/images/dashboard.png" />
  </body>
</html>

 

Step-3) Micro Service의 응답을 위한 TCP MessagePattern 데코레이터 생성

apps/dashboard/api/src/app/app.controller.ts 에서 TCP 요청에 대한 응답 코드를 작성한다. TCP는 MessagePattern 데코레이터를 통해 응답하고 반환은 Primitive type, Promise, Observable 다 가능하다. 만일 Observable로 하면 gateway 측에서 Observable로 대응할 수 있다. 

  • { cmd: 'dashboard-sum' } 은 gateway로부터 오는 Key이다.
import { Controller, Get } from '@nestjs/common';
import { MessagePattern } from '@nestjs/microservices';

import { DashboardApiAppService } from '@rnm/domain';
import { from, Observable } from 'rxjs';

@Controller()
export class AppController {
  constructor(private readonly appService: DashboardApiAppService) { }

  @Get()
  getData() {
    return this.appService.getData();
  }

  @MessagePattern({ cmd: 'dashboard-sum' })
  accumulate(data: number[]): Observable<number> {
    console.log('calling sum from dashboard....');
    const sum = data[0] + data[1] + data[2];
    return from([sum]);
  }
}

테스트는 Gateway까지 설정후 진행한다.

 

Step-4) Gateway 설정

  • gateway
    • http server & static server
    • reverse proxy
    • api client

config.json 파일을 apps/gateway/api/src/environment  폴더밑으로 생성하고 하기와 같이 설정한다.

{
  "HTTP_PORT": 8000,
  "DASHBOARD": {
    "REVERSE_CONTEXT": "dashboard",
    "REVERSE_ADDRESS": "http://127.0.0.1:8001",
    "HTTP_PORT": 8001,
    "TCP_HOST": "0.0.0.0",
    "TCP_PORT": 8100,
    "GLOBAL_API_PREFIX": "/api"
  },
  "CONFIGURATION": {
    "REVERSE_CONTEXT": "configuration",
    "REVERSE_ADDRESS": "http://127.0.0.1:8002",
    "HTTP_PORT": 8002,
    "TCP_HOST": "0.0.0.0",
    "TCP_PORT": 8200,
    "GLOBAL_API_PREFIX": "/api"
  },
  "BACK_OFFICE": {
    "REVERSE_CONTEXT": "back-office",
    "REVERSE_ADDRESS": "http://127.0.0.1:8003",
    "HTTP_PORT": 8003,
    "TCP_HOST": "0.0.0.0",
    "TCP_PORT": 8300,
    "GLOBAL_API_PREFIX": "/api"
  }
}

apps/gateway/api/src/main.ts는 http server를 설정한다.

  • global prefix를 설정하지 않고, 각 controller에서 설정한다.
  • gateway api 호출은 "api/gateway"
  • dashboard api 호출은 "api/dashboard". 다른 microservice도 동일하다.
// gateway의 main.ts
import { Logger } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';

import { loadGatewayConfiguration } from '@rnm/shared';

import { AppModule } from './app/app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // Load config.json file
  const config = loadGatewayConfiguration();

  const httpPort = config.HTTP_PORT || process.env.HTTP_PORT || 8000;
  await app.listen(httpPort, () => {
    Logger.log(`Listening at http://localhost:${httpPort}`);
  });
}

bootstrap();


// app.controller.ts
@Controller('api/gateway') <== 요기
export class AppController { ... }


// dashboard.controller.ts
@Controller('api/dashboard') <== 요기
export class DashboardController { ... }

apps/gateway/api/src/app/app.module.ts에서는 static file 서비스 폴더를 지정한다. reverse proxy를 위해 아래 3개의 exclude를 추가한다. web context처럼 사용될 것이다.

  • dashboard
  • configuration
  • back-office
import { Module } from '@nestjs/common';
import { ServeStaticModule } from '@nestjs/serve-static';
import { join } from 'path';

import { GatewayApiAppService } from '@rnm/domain';

import { AppController } from './app.controller';
import { DashboardModule } from './dashboard/dashboard.module';

@Module({
  imports: [
    ServeStaticModule.forRoot({
      rootPath: join(__dirname, 'public'),
      exclude: [
        '/api/gateway*', '/api/dashboard*',
        '/dashboard*', '/configuration*', '/back-office*'
      ],
    }),
    DashboardModule
  ],
  controllers: [AppController],
  providers: [GatewayApiAppService]
})
export class AppModule { }

gateway, back-office, configuration, dashboard등에서 사용하는 api service 를 domain library로 옮기고, 명칭을 수정하여 import해서 사용한다. 예로 GatewayApiAppService, DashboardApiAppService와 같다. api service중에 라이브러리성으로 옮겨도 될 만한 것들을 domain 라이브러리 밑으로 작성한다. 예로 여러 곳에서 호출되는 공통 api의 경우 또는 업무적인 묶음단위로 api를 관리하고 싶을 경우이다. 해당 api를 라이브러리로 빼는 이유는 향후 다른 UI에서 사용하거나 또는 UI가 바뀌어도 업무적인 api에 변경이 없을 경우를 가정한 것이다.

 

Step-5) Gateway에 Micro Service쪽으로 reverse proxy 설정하기

micro service 인 dashboard, configuration, back-office는 자신의 http web server로써 static file을 서비스하고, tcp를 통해 api를 전송한다. 개발시에는 gateway를 구동하지 않고, micro service만 실행한다면 http를 통해 api 서비스 추가하면 될 것이다.

Gateway에서 reverse proxy되는 3개 context

 

http-proxy-middleware를 설치한다. 

$>  yarn add http-proxy-middleware

 

다음으로 apps/gateway/api/src/app 폴더 밑으로 dashboard 폴더를 생성하고,  dashboard용 controller, service, module과 dashboard.proxy.ts 파일들을 생성한다.

  • NestMiddleware를 상속받아 use를 구현한다.
// dashboard.proxy.ts ==> dashboard-proxy.middleware.ts 로 추후 명칭 변경됨
/* eslint-disable @typescript-eslint/no-explicit-any */
import { NestMiddleware, Logger } from '@nestjs/common';
import { loadGatewayConfiguration } from '@rnm/shared';
import { createProxyMiddleware } from 'http-proxy-middleware';

export class DashboardReverseProxyMiddleware implements NestMiddleware {
  private config = loadGatewayConfiguration();
  private proxyOptions = {
    target: this.config.DASHBOARD.REVERSE_ADDRESS,
    secure: false,
    onProxyReq: (proxyReq: any, req: any, res: any) => {
      Logger.debug(`[DashboardReverseProxyMiddleware]: Proxying ${req.method} request originally made to '${req.url}'`);
    },
  };
  private proxy: any = createProxyMiddleware(this.proxyOptions);

  use(req: any, res: any, next: () => void) {
    this.proxy(req, res, next);
  }
}

dashboard.module.ts에 다음과 같이 dashboard에 TCP로 접근하는 Client설정을 한다. 그리고 ProxyMiddleware를 설정한다.

import { MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';

import { loadGatewayConfiguration } from '@rnm/shared';

import { DashboardController } from './dashboard.controller';
import { DashboardReverseProxyMiddleware } from './dashboard.proxy';
import { DashboardService } from './dashboard.service';

const config = loadGatewayConfiguration();

@Module({
  imports: [
    // dashboard쪽 api 호출을 위한 TCP Client Proxy 등록
    ClientsModule.register([
      {
        name: "DASHBOARD",
        transport: Transport.TCP,
        options: {
          host: config.DASHBOARD.TCP_HOST,
          port: config.DASHBOARD.TCP_PORT
        }
      }
    ])
  ],
  controllers: [DashboardController],
  providers: [DashboardService],
})
export class DashboardModule implements NestModule {
  // middleware 적용
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(DashboardReverseProxyMiddleware)
      .forRoutes({ path: config.DASHBOARD.REVERSE_CONTEXT, method: RequestMethod.ALL });
  }
}

전체 코드 준비는 되었고, 다음 글에서는 MS Code에서 Debugging을 통해 위의 코드를 테스트해 본다.

 

소스: https://github.com/ysyun/rnm-stack/releases/tag/ms-2

 

 

<참조>

- 마이크로 서비스 설정 예제
https://github.com/danmt/microservices-basics

 

GitHub - danmt/microservices-basics: This project intention is to show microservices basic concepts in NestJs

This project intention is to show microservices basic concepts in NestJs - GitHub - danmt/microservices-basics: This project intention is to show microservices basic concepts in NestJs

github.com

- reverse proxy 설정 예제

https://github.com/baumgarb/reverse-proxy-demo

 

GitHub - baumgarb/reverse-proxy-demo: Reverse proxy demo with NestJS and http-proxy-middleware

Reverse proxy demo with NestJS and http-proxy-middleware - GitHub - baumgarb/reverse-proxy-demo: Reverse proxy demo with NestJS and http-proxy-middleware

github.com

- http-proxy-middleware 패키지

https://github.com/chimurai/http-proxy-middleware

 

GitHub - chimurai/http-proxy-middleware: The one-liner node.js http-proxy middleware for connect, express and browser-sync

:zap: The one-liner node.js http-proxy middleware for connect, express and browser-sync - GitHub - chimurai/http-proxy-middleware: The one-liner node.js http-proxy middleware for connect, express a...

github.com

 

posted by 윤영식
2021. 9. 20. 19:36 React/Architecture

애플리케이션이 계속 증가하고, 작은 변경에 기민하게 대응할 수 있고, 팀별 기술셋의 다양성을 수용하는 환경에서 Micro Service 개발/운영 환경을 구축해 본다. 

 

Micro Service

모노리틱환경의 단점을 극복한다. (참조)

  • 작은 사이즈의 번들링 및 배포
  • 지속적인 애플리케이션 추가
  • 점점 늘어나는 접속자의 대응

Gateway Pattern 적용

  • gateway 역할
    • api gateway: micro service의 api server와 TCP로 연결한다. 또는 중간에 Queue(Redis, RabbitMQ, Kafka)등을 두어서 구성해도 된다.
    • http server: login UI부분을 gateway에서 담당하고 JWT를 사용한다.
    • reverse proxy: micro service에 대한 web bundling file에 대한 reverse proxy 역할을 한다.
    • auth server: login시 auth 역할을 한다. 
  • micro service 역할
    • api server: api는 TCP로 응답한다. 
    • http server: reverse proxy의 static 파일 대응을 한다. 

 

가용성을 위하여 gateway를 두개를 두어 용도별로 운영할 수 있다. 

docker 기반으로 운영

 

 

BFF 패턴

Backend For Frontend 패턴 방식으로 개발한다. 

  • Server는 NodeJS 기반으로 구성한다. 
  • Client/Server 개발은 TypeScript로 통일한다.
  • CSR(Client Side Rendering), SSR(Server Side Rendering)을 사용한다. 

참조: https://giljae.medium.com/back-end-for-front-end-pattern-bff-4b73f29858d6 

 

 

 

개발 폴더 구성

멀티 애플리케이션을 mono repo에서 개발할 수 있는 폴더 구조를 정의한다. 

  • apps 폴더
    • 4개의 애플리케이션이 존재한다.
  • libs 폴더
    • page: 애플리케이션에 단위 화면 page를 개발하는 것이 아니라. page단위 별도 구성하고, application에서 조합한다.
    • ui: 모든 애플리케이션에서 사용하는 공통 ui
    • domain: core 업무에 대한 모든 내역들, 변경에 대해 유연하게 대처하기 위해 hexagonal architect방식으로 구성한다.
    • shared: common 라이브러리를 적용한다.

 

application의 libs 라이브러리 참조 방식

  • api: api server역할이므로 주로 none visible한 라이브러리 참조
  • web: ui 애플리케이션이므로 page, ui 라이브러도 참조

 

애플리케이션과 라이브러리 참조 관계

 

 

운영 폴더 구성

gateway와 micro services를 배포하는 폴더 구조를 정의한다. 

  • bin: start/stop shell
  • config: 환경파일
  • lib: server 구동 번들링 파일
  • public: static, web 번들링 파일들
  • logs: 서버의 로그 파일

 

 

<참조>

- 마이크로 서비스 아키텍쳐 패턴 그리고 장/단점 (강추)

https://medium.com/design-microservices-architecture-with-patterns/monolithic-to-microservices-architecture-with-patterns-best-practices-a768272797b2

 

Monolithic to Microservices Architecture with Patterns & Best Practices

In this article, we’re going to learn how to Design Microservices Architecture with using Design Patterns, Principles and the Best…

medium.com

 

- 헥사고날 아키텍쳐

https://engineering-skcc.github.io/microservice%20inner%20achitecture/inner-architecture-2/

 

마이크로서비스 내부아키텍처 - 2회 : 클린 아키텍처와 헥사고널 아키텍처

마이크서비스 내부 아키텍처에 대해 살펴보자. 3회에 걸쳐서 기존 아키텍처의 문제점 및 이를 개선하고자 했던 아키텍처 변화양상과 이를 반영한 어플리케이션 구조에 대해 알아 보겠다.

engineering-skcc.github.io

 

- BFF 패턴

https://giljae.medium.com/back-end-for-front-end-pattern-bff-4b73f29858d6

 

Back-end for Front-end Pattern (BFF)

소프트웨어 구성의 진화

giljae.medium.com

 

posted by 윤영식
2021. 9. 8. 19:10 React/Start React

서버 사이드 렌더링을 위하여 Next.JS를 적용해 본다. Micro Frontend 전략을 활용하여 구성해 본다. 

  • apps 밑에는 multi application이 존재한다.
  • libs 밑에는 base 패키지들이 있다. base 패키지는 계속 추가될 수 있다.
  • feature는 view와 domain이 존재한다.
    • view: 업무 UI 표현
    • domain: 업무 api 호출(react-query or graphql query) 및 결과 data model, custom Hook 기반
  • page에서 view와 domain을 조합한다.
    • SSR에서 Next.JS 기반으로 진행시 feature의 domain을 통해 데이터를 다룬다. 

 

멀티 애플리케이션에서의 단위 업무화면 구성방식

 

 

NextJS 개념

최신것이 항상 좋은 것이 아니다. SPA 프레임워크의 CSR(Client Side Rendering)만으로 개발하다가 예전의 JSP, ASP같은 SSR(Server Side Rendering)의 이점이 있었다. NextJS는 CSR, SSR 뿐만아니라 SSG(Static Site Generation)도 지원을 한다. 

 

  • CSR
  • SSR
  • SSG

 

SSG에 대한 개념

 

SSG를 위한 getStaticProps, getStaticPaths

  • getStaticProps와 getStaticPaths는 async이다.
  • getStaticProps, getStaticPaths에서 데이터를 가져오는 domain에 위치한 api를 호출한다. 
  • getStaticPaths는 URL path에 따라 데이터가 변경될 때를 대비한다. 

 

SSR를 위한 getServerSideProps

  • 서버에 요청때 마다 데이터를 맵핑하여 응답한다. 

 

Use Case

  • Data가 즉각적으로 변화하는 Dashboard에는 CSR을 적용
  • eCommerce 같은 경우 제품의 변경이 수시로 일어나지 않을 때 SSG 적용
  • NodeJS위에 Isomorphic Application으로 간다. 

 

Frontend 개발 영역의 변경

  • NodeJS 기반위에 페이지 개발의 주도권을 Frontend에서 갖는다.
  • Backend API를 조합하는 역할을 수행한다. 

Frontend 개발영역의 확장. (출처: D2 참조글)

 

Rendering on the Web 위치

Rendering on the Web 출처

 

NextJS기반 애플리케이션은 이전 블로그에서 tube-ssr 애플리케이션을 생성하였다. 이를 기반으로 다음 글에서 로그인 화면을 만들어 보자 

 

 

<참조>

> Vercel의 NextJS 소개 영상

https://www.youtube.com/watch?v=eMwTEo6AjDc 

> CSR, SSR, SSG 의 흐름

https://wonit.tistory.com/361

 

[정직하게 배워보는 Next.js] (번외) 웹의 발전 과정으로 보는 CSR 그리고 SSG와 SSR

해당 블로그 시리즈는 정직하게 배워보는 Next js 시리즈로써 총 8부작으로 이루어져 있습니다. Next.js공식 홈페이지에서 이야기하는 내용을 최대한 이해하기 쉽고 직관적이게 구성하였습니다. 0.

wonit.tistory.com

 

> SPA의 SSR 지원

https://d2.naver.com/helloworld/7804182

 

> NextJS 기반 실습 시리즈

https://velog.io/@mskwon/next-js-page-static-generation

 

Next JS Page Static Generation

Next JS에서는 각 페이지에 대해서 pre-rendering을 위해 SSR(서버 사이드 렌더링) 또는 SG(정적 생성) 옵션을 제공한다. 각각 페이지 컴포넌트에서 getServerSideProps, getStaticProps/getStaticPaths 함수 expo

velog.io

 

posted by 윤영식