안녕하세요 깍돌이 입니다. Timer Phase 에서 Worker ( 테스트 자동화 코드 ) 를 실행하면서

 

느꼈던 문제들 그리고 Node.js의 Event Loop 에 있는 Timer Phase 에 대한 생각 그리고 위의 Worker들을

 

Worker Thread Pool 로 해결이 될지 된다면 어떤방법인지에 대해 생각한 내용들입니다.

 

Node.js Timer Phase 

네 타이머 페이즈에서 해결할수 있었습니다. Event Loop 가 계속 돌면서 타이머 페이즈에 는 계속 등록된 함수들이 있게 되고 ( Worker ) Event Loop 가 돌면서 계속 실행됩니다.  하지만 테스트 플랫폼의 초기 단계이기도 하고 API 테스트의 구조는 비동기 형태로 단순 Request , Response 에 대한 결과를 검증 하기 때문에 큰 문제는 되지않았지만 ( 이것도 케이스가 수천건이 넘어가면 이슈가 생길듯 ) 

 

시나리오의 경우 UI, API 가 있고 현재는 API 를 우선하고있지만서도 스텝과 스텝 사이의 Polling 하는 구조가 많이 있다보니 Event loop 에서 Timer Phase가 점점 무거워집니다. 그렇기 때문에 결국 Timer Phase 도 뭔가 좋은 방법은 아니지 않을까? 라는 생각을 하게 되었습니다.  ( 이번 작업은 UI 를 하지 않았지만 UI 의 경우 Child_Process를 통하여 직접적으로 하나의 프로세스를 더 띄우는 방법을 생각 하고 있었습니다. ) 

 

그리고 테스트 해보고 싶었던 마음에 노드 관련 홈페이지를 기웃 기웃하다 보니 Node.js 공식 홈페이지에 아래와같은 말이 있었습니다. 

 

그리고 앞선 Worker Pool 에 대한 이야기 중 간과해야 될게 있습니다. 

 

현재 작성하고있는 테스트 플랫폼의 Test Case들은 CPU I/O 가 많지 않습니다.(그렇기 때문에 무조건 워커 스레드 풀을 써야하는게 아닌 Timer Phase 에서 처리되는 워커들이 워커 스레드 풀에선 좀 더 나을까?라는 내용에 대한 확인 을 하고 싶기 때문입니다. )   케이스 스텝 사이에 대기(Polling) 하는 텀이 길기 때문에 하나의 Task(작업) 이 일어날때 무거운 작업은 아니라는 점을 중간에 한번 적어보았습니다.  TC의 예시

 

A시나리오

 

               -> A작업이 완료가 됐는지 최대 10분간 확인 ( Polling) -> 10분이 지나도 완료가 안됐을 경우 -> 실패

A 작업 

            ->  10초마다 확인하여 10분(Test Timeout) 전에 작업이 완료되면 다시 다음 B작업 실행 

 

=> 위와같은 형태로 A -> B -> C -> D -> E -> F -> G -> H -> I -> J - > K -> L (실제 케이스는 이거보다 더많습니다. ) 

로 진행됩니다. 하나의 시나리오에 테스트 시간은 약 30분 정도가 됩니다.  Polling하는 시간이 많기 때문에 CPU I/O를 많이 쓰지 않는 케이스에 대한 자동화라는 점을 기억해 주시기 바랍니다.

Don't Block the Event Loop (or the Worker Pool)

https://nodejs.org/en/docs/guides/dont-block-the-event-loop/

이부분에서 핵심내용으로 생각되는 부분들에 대해서 간략하게 몇가지 적어 보자면  

    Node.js는 EventLoop ( Initialization and callbacks ) 에서  JavaScript 코드를 실행하고 파일 I/O 같은 값비싼 작업을 처리하기 위해서 작업자 풀 ( Worker Thread Pool ) 을 제공한다고 합니다.  

 

두가지 타입의 쓰레드가 존재 

첫번째는 Event Loop ( Main Loop, Main Thread , event Thread ) - 전 포스팅에 있던 노드 이벤트 루프

두번째는 Worker Pool ( Thread Pool ) 

Node.js의 경우 Event-Driven 구조이며 관리를 위한 Event Loop 와 비싼 작업을 하는 Worker Pool 이 있다.

 

그 밑에는 O(n) 의 복잡도를 가지는 콜백에 대해서 작은 n이라면 무엇보다 빠르지만 점점 높아질수록 문제가 생김에 대한 내용이 있고 Node.js는 V8 엔진을 사용하기 때문에 일반적인 조작은 매우고속이며 JSON 연산과 regexps에서는 예외가 될수 있음을 설명 합니다.  regexp는 입력 스트링에 따라서 o(2n) 이 가능하기 때문에 이 경우는 Event Loop를 차단합니다. 

그래서 regexp를 이용한 redos에 대한 샘플이 있지만 ( 테스트 자동화에서는 정규식을 많이 사용하지도 않으며.. 내부 백오피스 에서 사용하기 때문에 일단 제외 해도 될거 같습니다. ) 

결론은 regexp 를 조심해서 써야 한다 이런내용

 

Node.js의 Event Loop를 차단하는 코어 모듈 

Encryption

Compression

File System

Child Process ( 이건 추후에 쓸 예정인데 유심히 봐야될듯 ) 

 

위의 API 들은 상당한 계산을 잠재적으로 사용하기 때문에 많은 비용이 발생 하기 때문에 서버를 구성한다면 아래의 API 들을 사용하면 안된다. ( express를 예로 들면 express Router에서 쓰지 말라는 의미입니다. ) 

  • Encryption:
    • crypto.randomBytes (synchronous version)
    • crypto.randomFillSync
    • crypto.pbkdf2Sync
    • You should also be careful about providing large input to the encryption and decryption routines.
  • Compression:
    • zlib.inflateSync
    • zlib.deflateSync
  • File system:
    • Do not use the synchronous file system APIs. For example, if the file you access is in a distributed file system like NFS, access times can vary widely.
  • Child process:
    • child_process.spawnSync
    • child_process.execSync
    • child_process.execFileSync

API 자동화에서는 CryptoJS를 사용하는데 확인해봐야 될거 같습니다.  파일시스템의 NFS는 NAS같은 류를 이야기 하는거 같습니다.  대부분 동기적인 작업 및 CPU I/O가 순간적으로 많이 발생되는 API 들이라고 생각 됩니다.

 

해당 밑에는JSON DOS ( JSON 에 대한 이야기니 넘어 가겠습니다. ) 

Complex calculations without blocking the Event Loop

이제 뭔가 나왔습니다. 이벤트 루프를 막지 않고 복잡한 계산 에 대해서 파티셔닝과 오프로드 2가지 에 대해서 설명합니다.  이부분은 간략하게 적고 직접 테스트하면서 결과를 적어 보겠습니다.

 

첫번째

C++ 애드온을 개발하여 내장된 Node.js Worker Pool 을 직접 쓴다가 있습니다. 

node-webworekr-thread 를 이용하여 Node.js Worker Pool 에 액세스 하는 js를 사용합니다.

 

두번째

직접 Node.js I/O 테마 워커 풀 대신 계산 전용 워커 풀을 직접 만드는 방법 이다. 방법으로는 Child_Process or Cluster 를 이용한다. 

 

사실 Worker Thread 에 대한 내용을 읽어 보면 CPU I/O 에 대한 이야기가 많다.  왜냐면 해당 작업이 무거워지면 Event Loop 를 막는 경우가 생길거고 이로 인해서 Node.js 전체가 느려지는 현상이 발생하기 때문이다.  서버를 이용한다면 대용량 트래픽을 받을 경우 문제가 생길수 있다 그렇기 때문에 이와같은 형태의 설명들이 많았고 아마 지금 글을 쓰고 있는 저처럼 모든 시나리오의 스텝의 사이가 CPU I/O 가 아닌 대기(Polling) 로 이루어진 케이스가 있을 거라곤 생각을 못했을 것이다.

 

지금 포스팅을 하면서도 트리거 시스템을 이렇게 구성하는게 과연 맞는 것인가? 에 대한 의문이 들지만 나와 유사한 작업을 한 사람도 볼수 없었고 블로그 포스팅도 없었기 때문에 그냥 부딪혀 보면서 포스팅 하고 운영해보다가 더 좋은 개선될 내용이 있다면 다시 포스팅해서 Node.js로 완벽한 트리거 시스템을 구현해 보고자 한다.

 

서론이 쓸데 없이 길었던거 같긴한데 포스팅을 하게되면 이해없이는 쓸수 없기 때문에 서론을 쓰게되면 많은 이해가 된 상태로 작업을 할수 있게 됩니다.

 

해당 테스트는 많으면 2포스팅 적으면 1포스팅으로 하려고 하며 트리거 시스템의 기본 베이스 서버는 Express.js를 쓰기 때문에 Express.js를 가지고 작성하며 Express.js의 기본적인 내용은 다 알고 있다고 생각하고 자잘한 설명은 제외하겠습니다. ( Express.js는 블로그 검색하면 내용이 워낙 많으니까요 ) 

 

https://www.nearform.com/blog/learning-to-swim-with-piscina-the-node-js-worker-pool/

 

 

Express.js

const express = require('express')
const app = express()
const port = 8880

const jsWorker = require('./express_worker');

app.get('/', (req, res) => {
  res.send('Hello World!')
})

app.get('/latency',(req,res)=>{

  let st_time = performance.now();
  let ed_time = performance.now();

  let total_time = ed_time-st_time;
  res.send(`latency Time - ${total_time}`);
})

app.get('/express-polling-worker',(req,res)=>{

  let uuid = createStepUUID();
  setTimeout(async()=>{
    try {
      jsWorker(uuid);
    } catch (error) {
      console.error(error);
    }
  },0);
  res.send('Start Polling Worker');
});

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
})


function createStepUUID() {
  let d = new Date().getTime();
  if (
    typeof performance !== 'undefined' &&
    typeof performance.now === 'function'
  ) {
    d += performance.now(); // use high -precision timer if available
  }
  return '깍돌이-exps-xxxx-4xxx-yxxx'.replace(/[xy]/g, function(c) {
    var r = (d + Math.random() * 16) % 16 | 0;
    d = Math.floor(d / 16);
    return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);
  })
 }

 

Express_Worker.js

const { setTimeout } = require('timers/promises')

module.exports = (uuid)=>{
  return new Promise(async(resolve,reject)=>{
    try {
      console.warn(`Start UUID ${uuid}`);
      await setTimeout(1000000);
      console.warn(`Close UUID ${uuid}`);
      resolve(true);

    } catch (error) {
      reject('ERROR');
    }
  })
}

 

기본 베이스 코드입니다.

latency 는 해당 컨트롤러에서 코드를 읽고 res.send까지의 시간을 던져 줍니다.

express-polling-worker 는 Timer Phase 에 Timer 를 계속 추가합니다. 위의 코드 베이스로 테스트하겠습니다. 

시간초는 해당 컨트롤러에서 작업이 없기 때문에 보기 편하게 

 

let total_time = ((ed_time-st_time)*1000).toFixed(4);

으로 살짝 보정을 하겠습니다.  ( 사실 express 내에서 하는건 의미가없을 수도 있습니다. node.js의 블록킹이 걸리면 아래 GIF와 같이 코드가 실행되는시간이 오래 걸리는게 아니라 그냥 대기 ( 로딩 ) 시간이 생기기 때문입니다. )

express 내에서 찍어본건 요청이 왔을때 서버 내에서 시간을 한번 체크해보려고 했습니다.

 

Timer Phase 1만개 케이스 Polling

눈치 채셨을 수도 있지만 순식간에 1만개의 함수를 Timer Phase 에 넣었지만 넣고 있던 순간에 잠깐 느려질뿐 넣고 나서는 크게 느리지 않는걸 볼수 있습니다.  해당 이유는 워커에서 실제로 폴링을 한다면  상태 체크 -> 다음함수 또는 다시 상태 체크 의 경우를 해야되는데 하고있지 않기 때문입니다. 해당 코드를 추가하겠습니다.

 

module.exports = (uuid)=>{
  return new Promise(async(resolve,reject)=>{
    try {
      console.warn(`Start UUID ${uuid}`);

      let retry = 0 ;
      let loopCondition = 240;

      // 2400초 = 40분
      
      let closeCondition = true;
      while(loopCondition>=retry && closeCondition){

        let status = await getStatus(loopCondition);

        console.warn(`테스트 상태 체크 ${uuid}  = ${status}`);
        // true 면 반복문 종료 
        if(status==='Y'){
          closeCondition = false;
        }
        await setTimeout(5000);
        retry++;
      }

      console.warn(`Close UUID ${uuid}`);

      resolve(true);
    } catch (error) {
      reject('ERROR');
    }
  })
}

// 30분에 테스트 종료 
const getStatus = (loopCondition)=>{
  return new Promise(async(resolve,reject)=>{
    try {
      // 상태를 가져옵니다.
      let retrunValue='N';

      if(loopCondition===180){
        retrunValue='Y';
      }
      resolve(retrunValue);
    } catch (error) {
      reject(error);      
    }
  })
}

 

Timer Phase 1만개 케이스 Polling

서버에서 계속 동작을 하는 케이스로 추가하였습니다. 워커가 돌기 전 

 

폴링하면서 상태를 체크하는 코드에서 CPU가 과도하게 올라갑니다.

 

1만개의 워커를 사용시 최대 CPU 47%까지 사용하며 레이턴시 ( 서버내에서 실행할 경우 ) 는 큰 변화가 없었습니다.

아마 측정을 FE단에서 요청하고 돌려받는 시간을 재야 할거 같네요 마지막으로 위에 코드에 수정을 하나 하겠습니다.

 

서버쪽과 클라 쪽 입니다.

 

일반적으로는 FE에서는 9의 차이가 나타나지만

 

 

스레드 실행 후 1734 값이 나오는걸 알수 있습니다. 

=> 순간적으로 Timer Phase 에 꽉차기 때문에 병목 현상이 발생한다는 뜻입니다.   ( Event Loop 가 많은 양의 Fn을 Timer Phase 에 등록 하면서 생기는 현상 ) 

1. 케이스가 추가 될경우 병목 현상이 발생할수 있음

2. 추가된 케이스가 계속 수행되면서 CPU 사용량이 매우 높아짐 

 

이제 여기서 Node.js에서 말하는 Child_Process와 Cluster를 이용하여 처리해보려고합니다. 가장 유명한건 Cluster모듈이고 ( pm2 에서 많이 쓰고잇죠 )  해당 Cluster 모듈로 구현된 Node.js Piscina 라는 모듈을 사용해보려고 합니다.

 

Node.js with Piscina (feat. By James Snell

우선 관련 링크입니다.

https://www.nearform.com/blog/learning-to-swim-with-piscina-the-node-js-worker-pool/

해당 링크의 작성자를 유심히 봐야 하는데요 By James Snell 

 

https://github.com/piscinajs/piscina

해당 레포지토리의 메인 컨테이너가 쓴글인걸 알수 있습니다. 그리고 프로젝트들 보시면 알겠지만 Node.js의 개발자이기도 합니다.    해당 포스팅을 이용하여서 위의 테스트한 케이스들에 대해서 Piscina를 적용해보고 결과를 적어 보겠습니다.

해당 글이 2020년도 글이라 현재 많이 바뀐 점이 있습니다. 

runTask X  => run

 

단순하게 바로 테스트 해보겠습니다.

 

const piscina = new Piscina({
  filename: path.resolve(__dirname, 'express_worker')
});


app.get('/express-piscina-worker',async (req,res)=>{


  let loop=10000;
  for(let i=0;i<loop;i++){
      let uuid = createStepUUID();
      console.warn('ppap test - ' + i);
      const result = piscina.run(uuid+'-PPAP');
  }


  res.send('Piscina Worker Start ');
})

Timer Phase 1만개 케이스 Polling ( Node.js with Piscina )

위의 GIF ( without piscina ) 의 비교를 확실히 해보세요 !  같은 코드의 1만건의 테스트 런이 발생했을 때 실제 나의 PC의 CPU 와 웹 FE에서 latency 가 어떤지 비교하면 좋을거 같습니다.

 

1. 1만 TestCase  JS Timer Phase  Worker 실행 

1. 웹화면 지연

setTimer를 이용한 Timer Phase 에 TestCase Worker 를 1만개를 밀어 넣었을 경우 웹화면서 1862 ms 의 지연이 생깁니다. 

2. CPU 사용 률

케이스들의 폴링하면서 상태를 체크할 경우 최대 31% 까지 발생합니다.

* 참고로 제가 사용하는 cpu 입니다.

AMD Ryzen 7 5800X 8-Core Processor 3.80 GHz

 

 

 

1. 1만 TestCase  JS Worker Thread Pool 실행

 

1. 웹화면 지연

Worker Thread Pool 에 1만개의 TestCase를 밀어 넣었을 경우 800ms 가 나옵니다. 

Event Loop을 Blocking 하던 Timer Phase에서 2배의 효율이 발생합니다.

 

2. CPU 사용 률 

보시면 12%를 절대 넘지 않습니다.  

 

뭔가 결과만 보면 Piscina 에서 Worker Thread Pool 을 사용하면서 지연도 2배의 효율이고 CPU 도 오버해서 쓰지 않는거 같습니다.

 

하지만 자세히보면 다른점이 있습니다.  CPU 사용률 체크시 보면 Piscina의 경우 

 

24개의 워커만 실행되는걸 알수 있습니다.  1만대를 실행하였지만 실제로는 워커의 개수만큼인 24개의 워커만 실행되면서 진행한다는 점입니다. Piscina의 구조를 보면 알수 있습니다.

아마 Worker Pool 을 PC의 코어 수 * 1.5 로 만들어 놓고 실행합니다.

 

 

JS timerPhase로 밀어 넣는 경우는 서버의 상태등 아무것도 고려하지않고 밀어 넣기 때문에

 

10000개의 케이스를 전부 Timer Phase에 밀어 넣고 Event Loop 가 돌다가 다시 Timer Phase에 왔을때 실행이 되기 때문에 모든 케이스가 다 실행되며 CPU의 과부하 까지 걸리게 됩니다.

 

그럼 단순하게 여기서 피시나는 24개 뿐 ( Worker Thread )  -> 이것도 제 PC의 CPU 가 16코어이기 때문에 *1.5하여 24개의 스레드가 돌게 되는 겁니다.

 

만약에 스레드가 1초마다 실행하고 종료되는 케이스라면 적절하게 모든 케이스가 추가될건지 확인해보면

 

1만개의 TestCase가 추가된 후 적절하게 테스트 종료 및 대기하던 TestCase 가 다시 시작이 되는 걸 볼수 있습니다. 그런데 이렇게 24개의 워커만 사용해도 10%이상을 쓰지 않는데 좀더 많이 사용할수 없을까? 라는 고민으로 다음 포스팅은 위에서 piscina로 24개 Worker만 사용하는 PC 상태에서를 Core * 1.5배 만큼의 워커를 쓰지 않고 더 쓰는 방법이 있는지와 Piscina의 옵션 하나하나를 설명 하는 포스팅을 하도록 하겠습니다.

 

 

 

 

안녕하세요 깍돌이 입니다. 

 

해당 포스팅은 사실 QA가 릴리즈 또는 배포 모니터링시에 사용하게되는 툴 중에 자동화 스크립트가 있을수 있습니다.

하지만 해당 포스팅 시리즈는 테스트 플랫폼 ( Test Platform ) 에 관련된 이야기기 때문에

 

이번에 작성되는 내용은 worker 에 대한 내용을 적으려고합니다. 저는 QA하면서 필요한 기술적인 부분들은

전부 Node.js 하나로 처리하고 있습니다. 이부분에 대해서 간략하게 적고 넘어가자면 

Node.js 를 계속 써오고있어서 이기도 하지만 노드가 무조건 짱이다 이런 느낌도 아닙니다.

저연차 QA시절 (지금도 고연차는 아니지만) 에는 메인이 C++이였기 때문입니다.  ( UI 자동화를 IE , Chrome C++ Object 로 하던 시절.. 크흠 )

 

Node.js 하나로 필요한 부분들을 다 커버할수 있기도 하고 에 Node 위주로 사용하고 있으며 효율적으로 사용해야 하기 때문입니다.  ( 하지만 자바로 시작했어도 파이썬이나 노드로 넘어 왔을거 같긴합니다. ) 

 

그리고 궁극적인 목적은 높은 품질의 제품을 제공하기 위함입니다.  높은 품질의 S/W를 제공하기 위해서 자동화 테스트 를 넘어서 테스트 플랫폼이 필요하다면 만들면 됩니다. ( 자동화의 제일 중요한점은 이게 메인이 되선 안되고 QA활동에서 사용되는 Tool 로 써 작용되어야 하며 명확한 이유를 가지고 있어야 합니다. - 이건 대외비라 포스팅하지않습니다. )

 

JS Worker

JS Function 으로 작성된 함수에 대한 Worker 입니다. 

해당 Worker.js의 경우 5초 25초 50초를 대기 후 정상 종료 합니다. 

* 여기서부터 말하는 Worker는 실무에서는 작성될 독립된 자동화 테스트 코드 입니다. 

=> 제가 사용할 자동화 테스트 코드는 폴링하는 경우가 많기 때문에 시나리오 사이사이에 폴링을 유사하게 기대하는 loop 함수를 사용합니다.

 

실행시

순차적으로 실행이 되었습니다.  좀 더 자세히 보기 위해서 로그를 추가 하였습니다.

일반적으로 JS 함수를 통해서 Worker 를 진행시 

다같이 잘 돌아가는거 같지만 모든 함수들이 순차적으로 실행되고 있습니다.

모두 실행전에 결과를 주지않는 함수를 하나 만들어 놓겠습니다.

시나리오는 아래와 같습니다.

하나의 시나리오는 5초 15초 25초의 각각 의 폴링을 가지고 정상 종료 합니다.

2개의 시나리오를 실행시키며 1번째 시나리오에서는 5초 후 return 을 주지 않게됩니다.

 

이렇게만 보면 정상적으로 잘 동작하는 것 처럼 보입니다.

하지만 1에 suspend에서 어떠한 작업도 하지 않았기 때문입니다.  만약에 1에서 suspend 가 true일경우 에러가 발생했다면?

앞에 Worker 에서 발생한 서스펜드 에러로 인해서 뒤에서 실행되어야 모든 워커들이 실행되지 않는 현상이 발생합니다.

=> 해당 이유는 Node.js의 Event Loop 가 Single Thread 이기 때문인것 과 동시에 현재 실행되는 코드는 같은 Phase 에서 실행이 되고 있기 때문입니다. ( 이부분은 점차 수정하면서 설명 하도록 하겠습니다. ) 

 

이 뜻은 결국 각각의 Worker 들이 독립적으로 실행되지 않았다는 반증이 되기도 합니다. 

Node.js의 기본적인 Event Loop 를 같이 사용하다보니 위와같은 이슈가 발생합니다.

 

테스트 플랫폼을 이용하는 QA가 자기자신의 코드가 아닌 다른곳에서의 에러도 신경 써야 한다? 

=> 말이 안되는 이야기 입니다.

 

위의 경우로 인해서 일반적인 JS Function을 사용하는건 말이 안되는 상태입니다. 

Event Loop (Timer Phase)

Node.js의 Event Loop 에는 Timer Phase가 있습니다.

* 출처(Node.js 공식 홈페이지) : https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/ 

 

The Node.js Event Loop, Timers, and process.nextTick() | Node.js

Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine.

nodejs.org

 

필자의 자동화 시스템에서 Trigger 가 되는 주체는 Express.js로 구현된 서버가 되며 해당 Http 기반의 서버들은 기본적으로 Callback 이 등록되어있기 때문에 지속적으로 Event Loop 가 생성 하고 사라지지 않는 형태가 됩니다 .

그렇기 때문에 서버의 테스트 요청이 왔을 경우 ( 자동화 테스트 코드의 실행 요청 ) 해당 코드를 타이머로 묶어서 타이머 페이즈에 넣어 놓는 방법을 생각해 보았습니다.  

그 결과 코드는 아래와같이 변경 됩니다.   ( Express는 제외하고 샘플 코드를 작성하였습니다. - 실행할 자동화 테스트 코드를 타이머 페이즈로 밀어 넣습니다.)

 

 

위의 JS Worker 에서 오류가 났던 코드 그대로 사용시

뭔가 독립적으로 실행되는것처럼 보이게됩니다.

 

이상태에서 위의 JS Worker 를다시 보면 이거 try catch 를 잘써서 그런거 아닌가? 라는 의문이 다시 들수 있습니다.

setTimeout을 제거하고 Timer phase 로 들어가지않게 코드를 수정하게 되면

2개의 워커가 일단 실행은 되고 비동기 처럼? 돌아가는 것처럼 보이지만 같은 Stack 에서 돌아가고 있기 때문에

결국은 위와같은 현상이 발생하게 됩니다. 

 

이렇게만 보이면 독립적으로 작성된 자동화 테스트 코드를 Timer Phase 에 넣어서 돌아 가고 있는 것 같고 실제로  10개의 worker 를 작성하였을 경우 아래와 같이 (시뮬레이션이지만) 뭔가 잘 돌아가는거 같습니다.

 

하지만 여기에서는 문제가 하나있습니다.  이건 다음 포스팅에 작성 하도록 하겠습니다.

 

다음 포스팅 - 현재의 Timer Phase 에 Worker를 등록해서 사용할 경우 문제점

마지막 포스팅 - 해당 문제를 해결하기 위한 Node.js Piscina

 

안녕하세요 원래 기본적인 포스팅은 따로 안하려고 하는 편인데 자동화 플랫폼내에서는 필수적인 요소이기 때문에

 

기본부터 차근차근 넣어 놓으려고합니다.

 

실제 서버는 리눅스 환경이지만 현재는 윈도우즈에서 테스트를 하고있기 때문에 윈도우즈에서 우선 작성하도록 하겠습니다.

 

Node.js 가 설치된 상태로 PM2 를 글로벌 로 설치 후 테스트 시 아래와 같은 오류가 발생합니다.

 

npm i -g pm2
pm2 list

pm2 : 이 시스템에서 스크립트를 실행할 수 없으므로 C:\Users\GDL\AppData\Roaming\npm\pm2.ps1 파일을 로드할 수 없습니다. 자세한 내용은 about_Execution_Policies(https://go.microsoft.com/fwlink/?LinkID=135170)를 참조하십시오.

위치 줄: 1 문자 :1

pm2 list

     CategoryInfo : 보안 오류 : ( :) [], PSSecurityExeception

     FullyQualifiedErrorId : UnauthorizedAccess

 

인터넷에서 간단하게 구글링을 해보면 파워 쉘을 킨 후에 ExecutionPolicy Unrestricted 을 통해서 해제를 하라는 이야기들이 많이 있습니다.

 

 

Get-ExecutionPolicy : 'Scope' 매개 변수를 바인딩할 수 없습니다. 값 "Unrestricted"을(를) "Microsoft.PowerShell.Execution
PolicyScope" 유형으로 변환할 수 없습니다. 오류: "식별자 이름 Unrestricted을(를) 유효한 열거자 이름과 일치시킬 수 없습니
다. 다음 열거자 이름 중 하나를 지정한 후 다시 시도하십시오.
Process, CurrentUser, LocalMachine, UserPolicy, MachinePolicy"
위치 줄:1 문자:17
+ ExecutionPolicy Unrestricted
+                 ~~~~~~~~~~~~
    + CategoryInfo          : InvalidArgument: (:) [Get-ExecutionPolicy], ParameterBindingException
    + FullyQualifiedErrorId : CannotConvertArgumentNoMessage,Microsoft.PowerShell.Commands.GetExecutionPolicyCommand

 

실제 실행시에는 위처럼 발생하며 아래와 같은 방식을 사용해야 합니다.

 

이제 pm2가 준비 되었으니 기본적으로 pm2 cluster 를 기본적으로 해보도록 하겠습니다.

 

const express = require('express')
const app = express()

app.get('/', function (req, res) {
  res.send('Hello World  ')
})

app.listen(3000,()=>{
  console.log('Example Clustering App on Port 3000')
})

테스트에 사용할 express 기본 코드입니다.

 

npmjs.com 에서 pm2 의 cluster mode를 보면 아래를 참고하라고 되어있습니다.

https://pm2.keymetrics.io/docs/usage/cluster-mode/

npmjs 간단 설명입니다.

클러스터 모드는 Node.js 애플리케이션을 시작할 때의 특수 모드로, 여러 프로세스를 시작하고 이들 사이에서 HTTP/TCP/UDP 쿼리를 로드 밸런싱합니다. 이렇게 하면 전체 성능(16코어 시스템에서 10배)과 안정성(처리되지 않은 오류의 경우 더 빠른 소켓 재조정)이 향상됩니다.

 

const express = require('express')
const app = express()

let i=0;
function wait (time){
  return new Promise(async(resolve)=>{
    setTimeout(()=>{
      resolve(true)
    },time)
  })
}
app.get('/',async function (req, res) {
  try {
    const randomNumber =Math.floor( Math.random()*1000 );
    await wait(2000);

    console.log(`Count ${i}`);
    i++;
    res.send('test        '+ randomNumber)
  } catch (error) {
    console.error(error);
  }
})
app.listen(3000,()=>{
  console.log('Example Clustering App on Port 3000')
})

 

express  코드를 살짝 수정하였습니다.  / 의 Router에서 임의로 2000ms 의 시간을 두었습니다. 이렇게 되면 서버 1대로 돌릴 경우 웹에서 접속시 아래와같이 대기 시간이 생길수 있습니다.

해당 딜레이를 pm2 모니터로 확인시

 

 

PM2 Cluster 를 통하여서 해당 로딩이 없이 바로 바로 응답을 줄수 있도록 변경해보겠습니다.

해당 Cluster를 최대한으로 쓰기전에 현재 CPU 의 개수 먼저 확인해보니

const os = require('os')
const cpuCount = os.cpus().length

 

 

 

16개로 나오네요 자 이제 클러스터 모드를 사용하기 위해 공식홈페이지 Usage를 따라 세팅하도록 하겠습니다.

pm2 start app.js -i max

기본적인 로컬 PC의 CPU 개수만큼을 전부 사용 하는 모습입니다. 실제로 서버에서 요청도 다 받아 지는지 보겠습니다.

 

4번의 요청을 해보았을시 pm2 모니터를 통해 확인해보니 process 가 여러개 동작하는건 맞지만 실제로 제가 생각했던건 알아서 로드밸런싱을 해주는 형태였는데 해당 형태로는 절대 동작하지 않음을 알수 있습니다. 15번 app이 Idle타임을 가질경우 다른 서버가 대신 처리를 해줬어야 하는건데 말이죠 

 

해당 클러스터링 모드는 단순히 서버 한대가 돌아가다가 죽었을 경우 다음 서버가 일을 해주는 형태입니다.

 

여러 블로그들을 검색해서 찾아봤을 경우 대충 공식 홈페이지나 번역 하거나 의역하고 끝내는 경우가 많아서 좀더 찾아 보니 결국은 Node.js의 Cluster mode를 사용한 경우면 parent process에서 child_proces를 fork 로 여러대의 APP 을 띄워서 무중단 배포나 앞단에 서버가 죽었을 경우에 대한 처리를 이용하는 형태로 사용되며 결국 제가 원하는 로드밸런싱을 사용하기 위해서는 nginx의 로드밸런싱 기능을 같이 사용해야 합니다.

=> 오해가 있을수 있어 추가합니다.  해당 서버들의 CPU I/O 나 메모리의 사용률이 높아질 경우 알아서 클러스터링이 되지만 테스트 자동화의 경우 Idle타임을 사용하기 때문에 다른 클러스터를 사용해야한다고 인지 하지 못하여 클러스터링이 되지않는 것입니다.  ( 실제 클러스터링 되는 경우 하단의 테스트결과를 넣었습니다.)

 

module.exports = {
  apps : [
    {
      name      :"clusterTest",
      script    : "app.js",
      instances : "4",
      exec_mode : "cluster",
      wait_ready: true,
      listen_timeout: 50000,
      kill_timeout: 5000,
      watch: true, // 파일이 변경되었을 때 재시작 할지 선택
    }
  ]
}

 

마지막으로 테스트하였던 ecosystem.config.js 입니다. watch의 경우 파일 변경시 재시작인데 일반적으로는 쓰지 않는게 좋겠네요 

 

nginx 연동으로 LB처리는 실제로 쓰고있는 자동화 플랫폼 앞단에 처리하도록 하면서 다시 포스팅하겠습니다.

 

감사합니다.

 

 

--> 위에 로드밸런싱의경우 오해할수 있는 내용이 있어서 수정 하려고 합니다.

 

PM2 Cluster 를 사용할 경우 CPU , 메모리 등의 점유율에 따라 클러스터링을 해주고 있기 때문에 

테스트 자동화에 사용되는 Test Runner 들은  CPU , 메모리를 많이 쓴다기보단 Idle time 을 많이 사용하게 됩니다. 

그렇기 때문에 pm2 로 클러스터링을 늘려놔도 Test Runner 를 사용하기 위한 Idle time에서는 해당 클러스터링을 위한 조건에 부합하지 않기 때문에 동작되지 않는 것 처럼 보입니다. Round Robin 같은 방식이 아니기 때문에 그렇게 보이는 것이며

동작 예시를 아래에 추가 하였습니다.

 

pm2 monit

 

그리고 해당 cluster_app은 

새로 생성한 앱이라면 새로운 UUID를 response 로 주는 기본 서버입니다.

해당 코드로 비동기로 사용시 

안되는 것처럼 보입니다.

 

하지만 100이 아니라 5000번을 사용한다면 아마 200ms (0.2초안에 5000번이 접속시도)

 

최대 3개의 서버에서 돌아가면서 클러스터링 됨을 알수 있습니다. ㅎㅎ 좀더 직관적으로 보기 위해서 gif로 보여드리겠습니다.

 

 

정상적으로 클러스터링 되어 동작 함을 확인할수 있습니다 ㅎㅎ 

 

테스트 러너의 사용성에 살짝 부합하지 않을뿐 pm2 cluster 는 엄청 좋은 모듈입니다 

안녕하세요 깍돌이입니다. 

기존에 중단되었던 자동화 시스템 중 아직 정식시작이 된건 아니지만 앞에 부분들을 어느정도 다 갈아 엎고

다시 하고 있는 도중 상태관리에 대해서 중간 점검? 같은 형태를 해야될거같아서 정식 포스팅은 아니지만 중간 정리용으로 남기려고 합니다.

 

 

 

 

Recoil

이제까지 Redux 만 가지고 상태관리를 해오던 터라 요즘엔 Recoil 을 쓴다는 이야기가 있어 redux에서 recoil로 한번 넘어가보려고 써보게 되었는데 따로 사용법을 포스팅할거는 없을거같다는 생각이 들었습니다.

사용법이 생각보다 너무 간단해서 .. 하지만 hydraton 을 지원하는 next.js에서는 recoil 사용시 아래와 같은 warning이 뜨게 됩니다.  Duplicate atomkey '[key]' This is a FATAL ERROR in production. But it is safe to ignore this warning if it occurred because of hot module replacement. 

현재 2022-05-01 기준 Recoil 버전은 0.x 으로 베타 버전인데 해당 버전에 있는 알려진 이슈라고 하다보니 Redux에서 관리하던 상태를 전부 Recoil 로 가져오진 않았습니다. 

현재 Recoil 에서 사용되는 상태는 이제 FE 에서 사용되는 Nav의 상태 ( Open , Close ) 나 화면단의 설정 값 등으로만 사용하고 있습니다. BE에서 받는 비동기 처리에 대해서는 Recoil 을 사용하지 않는 상태입니다.

 

 

React-Query 

비동기 통신을 위한 데이터는 Recoil 을 사용하지 않기 때문에 비동기 통신 데이터는 React-Query 를 사용하고 있습니다.

 

처음에 사용할 때 고려 되었던 부분만 작성하도록 하겠습니다.

 

우선 React-Query 를 사용하게되면  QueryClientProvider 를 사용해야되는데 기존에 Recoil Root를 덮고 있어서

어떻게 써야되나 고민을 잠깐하게 되었는데 별건 없었습니다.  그냥 Recoil Root위에 덮어서 사용하면 됩니다.

 

 

GET

데이터를 가져와서 FE에 뿌려주는 형태로 사용시 아래와 같은 형태로 데이터를 요청해서 가져올수 있는데 

 

React-Query 의 경우 캐싱이 기본이기 때문에 위와같이 가져오게되면 

target_os가 A든 B든 돌아오는 값이 같다고 하여도 재 요청이 발생하지 않습니다. 

( retry 와 refetchOnWindowFocus를 Fasle로 놓았기 때문에 그런것도 있지만 ) 

의도하는 바람은 target_os가 가 달라질때만 요청을 하는게 의도되는 바이기 때문입니다. ( 불필요한 retry X ) 

 

첫번째 해결방법은 refetch  + useEffect 입니다.

 

아래와 같이 useEffect 와 refetch 를 하게되면 첫렌더링 후에 refetch 를 통하여 query 를 강제 재 발생합니다.

 

위와같에 하게되면 특이점이 

 

컴포넌트 렌더링시 

 

컴포넌트 렌더링 -> useQuery -> use Efffect  -> useQuery 로 재 렌더링 -> fetch Data ( 비동기 완료 ) -> 재 렌더링

발생합니다.

 

다른 방법은 useQuery의 인스턴스를 다시 초기화 하는 방법입니다.

위와같이 useQuery 의 인스턴스를 props를 통해서 지정하게 되면 다른 키로 인식을 하기 때문에 다시 fetch 가 발생합니다.

 

렌더링이 발생하는 건 똑같이 나타나네요 ㅎㅎ;;

 

새로 fetch을 할때만 렌더링을 하고싶은데 이건 오늘 처음 공부해본거라 좀더 찾아봐야될거 같습니다.

현재 사용 방식은 인스턴스를 재 초기화 하는 방식으로 사용 하려고합니다.

 

refetch 충분히 괜찮아 보이지만 렌더링시에 props를 통해서 새로 받는 부분이 더나을거 같고 그리고 추후에

Query Invalidation 의 경우 특정 한 쿼리 인스턴스를 통해서 해야 하기에 현재 받아온 쿼리 인스턴스를 위와같이 써야 특정 할수 있기에 위와같이 쓰도록 하겠습니다.

 

 

 

 

 

 

 

- 임시 포스팅 입니다. ㅜ

안녕하세요 깍돌이입니다.  테스트 자동화 플랫폼 개발기에서 Detail VIEW와 Image VIEW에 대한 기본 구현이 되었는데요  (아직 포스팅안됨) 해당의 Image VIEW의 경우 이미지를 통해서 하다보니까 프레임이 낮고 그러다보니 아쉬운점이 이만 저만이 아닙니다. 그렇기 때문에 해당 자동화 테스트 화면을 실시간으로 스트리밍을 하는게 궁극적인 목표인데요

 

그렇기 위해서는 아래의 작업들이 필요합니다. 

OBS ( Open Broadcaster Software ) 

방송과 녹화를 할수있는 오픈소스 프로그램으로 해당을 통해서 화면 캡처를 하게 됩니다.

 

RTMP Server

OBS으로 캡처된 화면을 RTMP Server로 보내게 되며 해당 화면들을 받아서 처리합니다.

 

ffmpeg && HLS 인코딩

RTMP Server로 받아온 캡처 내용들을 ffmpeg을 통해서 hls로 인코딩합니다.

저장소 저장

저장된 HLS 파일을 특정 저장소에 저장합니다.( S3, Object Storage ) 

hls url 배포

저장소에 저장된 경우 해당 URL이 있기 때문에 배포가 완료 될 경우 hls url을 배포합니다. 그리고 Client FE에서 배포가 된점을 알수 있게 합니다.

FE Client HLS Player 재생 

FE Client에서는 해당 배포 내용을 받고 스트리밍을 시작합니다.

 

( 작업 내용 추후 포스팅 ) 

임시 포스팅

 

Mysql 에서 시간 저장을 위해서 datetime 으로 저장하게 되는데

해당의 경우 DB로부터 데이터를 받아 그대로 렌더링시

 

이와같이 나타나게 해주기 때문에 이를 위해 여러가지 구글링시

첫번째 dbconfig의 dateStrings를  'date' 로 설정합니다.  

 

 

 

 

 

안녕하세요 깍돌이입니다.

 

이제는 FE 쪽 간단한 개발을 위해서 Table 태그를 이용한 TASK 상태를 그려줄 화면을 만들어야하는데요

 

React 에서 해당 table을 만들게 되면 위와같은 warning이 나타납니다.(노랑색도 아니고 빨간색이라 오류로 보고 처리하는게 나을거 같습니다.)

 

 <div>
    <table>
      <colgroup>
        <col width="100px"/>
        <col width="*"/>
        <col width="90px"/>
        <col width="160px"/>
        <col width="120px"/>
        <col width="50px"/>
        <col width="50px"/>
      </colgroup>
      <thead>
          <th>UUID</th>
          <th>TESTNO</th>
          <th>TASK</th>
          <th>TASKSTATUS</th>
          <th>TESTRESULT</th>
          <th>START</th>
          <th>FINISH</th>
      </thead>
      <tbody>
        <tr>
          <td>test-1234-sddf-4321</td>
          <td>1</td>
          <td>할당 및 접속 테스트</td>
          <td>WAIT</td>
          <td>FAIL</td>
          <td>5분전</td>
          <td>2분전</td>
        </tr>
      </tbody>
    </table>
  </div>

위와같이 하였을때 일반 페이지에서는 오류가 발생하지 않지만 React 에서는 해당 태그의 구조는 오류가 발생합니다.

 

validateDOMNesting(...) <th> cannot appear as a child of <thead>

직역을 하자면 th는 thead의 자식으로 나타날수 없다 라는 뜻이 되는데요 간단하게 처리하자면

 

 <div>
    <table>
      <colgroup>
        <col width="100px"/>
        <col width="*"/>
        <col width="90px"/>
        <col width="160px"/>
        <col width="120px"/>
        <col width="50px"/>
        <col width="50px"/>
      </colgroup>
      <thead>

         <tr>
          <th>UUID</th>
          <th>TESTNO</th>
          <th>TASK</th>
          <th>TASKSTATUS</th>
          <th>TESTRESULT</th>
          <th>START</th>
          <th>FINISH</th>

         </tr>
      </thead>
      <tbody>
        <tr>
          <td>test-1234-sddf-4321</td>
          <td>1</td>
          <td>할당 및 접속 테스트</td>
          <td>WAIT</td>
          <td>FAIL</td>
          <td>5분전</td>
          <td>2분전</td>
        </tr>
      </tbody>
    </table>
  </div>

 

th 부분을 tr로 묵어 주면 됩니다.

안녕하세요 깍돌이 입니다.

저번에는 mysql 을 기본적으로 세팅해서 SELECT 문 정도로만 테스트로 해보았었는데요

이번에는 CRUD 작업들을 작성하기 전에 SQL 을 작성할때 흔히 말하는 "날 쿼리" 를 작성하지 않도록 하는 작업부터

해보도록 하려고 합니다.

 

DOCS (node-mysql2) & Prepared Statements 

우선  SQL 인젝션에 대해서 조사를 하다보면 가장 많이 나오는 이야기 들이 "날 쿼리" 를 사용하지 않아야 한다는 점입니다.  예를 들면 

  const sql = `SELECT * FROM ${table} where uuid = ${_uuid}`;

위와같이 작성 하였을 때 날 쿼리라고 하게 됩니다.  _uuid 값은 사용자가 웹에서 입력해서 넘어오는 값이 될수 있습니다. 그렇기 에 입력하는 입력 폼에서 sql injection 이 가능해지는 경우가 됩니다. 

 

또한 해당 입력되는 값에 SQL 의 예약어가 들어오게되면 서버 에러도 유발 시킬수 있습니다. 

 

우선 Prepared Statements 의 경우 왜 인젝션에 방어가 되는가 에 대한 설명은 블로그로 대체 하도록 하겠습니다.

m.blog.naver.com/PostView.nhn?blogId=skinfosec2000&logNo=220482240245&proxyReferer=https:%2F%2Fwww.google.com%2F

 

[SQL인젝션] Prepared Statement를 쓰면 SQL 인젝션 공격이 되지 않는 이유는?

Prepared Statement의 내부 원리 개요 SQL인젝션 취약점에 대한 대응방안으로 Prepared Statement와...

blog.naver.com

SK 인포섹 블로그 라고 되어있는데 보안은 전문가들에게 맡기는게 좋을 것 같습니다. ^^ 

 

그럼이제 Node.js & mysql2 로 해당 과정을 작성하여야 하는데요 위에 잘못된 예시인 "날 쿼리" 를 변경하는 작업을 보겠습니다. 

먼저 제목과 같이 DOCS 도큐먼트라고 하죠 저희가 쓰고 있는 라이브러리는 mysql2 을 쓰고있기 때문에 mysql2 의 도큐먼트를 먼저 찾아 보는게 좋습니다. (docs가 이해가안된다면 한글로 구글링하는 방안으로 ) 도큐먼트를 자주 보다보면 어느샌가 DOCS의 틀? 프레임? 덕분에  검색되는 내용보다 더 이해가 쉽게 되는 경우가 있습니다.

 

Node.js 의 사용모듈의 DOCS를 찾을 때 저는 npm 으로 먼저 확인합니다.

 

1. npm 검색 (mysql2)

 

2. Repository 이동

여기서 HomePage가 DOCS인경우도있고 Repository 가 DOCS인 경우도 있습니다. 둘다 같은 경우도있구요 저는 Repository 로 들어가서 prepared statements에 대해 검색해보았습니다.

 

떡하니 목차가 있네요 이제 해당 DOCS를 보고 위에 있는 "날 쿼리" 를 변경해보도록 하겠습니다.

DOCS 주소입니다.(github.com/sidorares/node-mysql2#using-prepared-statements)

기존 날 쿼리

  const sql = `SELECT * FROM ${table} where uuid = ${_uuid}`;

변경된 쿼리

const sql = `SELECT * FROM ${this.table} WHERE uuid = ?`;

단순 비교하자면 uuid 를 받는 부분이 ? 로 변경되어있습니다. 해당 이유는 ( SK 인포섹 블로그 글 참조 )

 

그리고 해당을 이용하여서 전체 수정시 아래와 같이 변경할수 있습니다.

 const uuid = req.params.id;

    if(uuid!==undefined){
      const sql = `SELECT * FROM ${this.table} WHERE uuid = ?`;

      const connection = mysql.createConnection(this.db_config);
      connection.connect();
  
      connection.execute(sql,[uuid],
        function(err,results,fields){
          if(err) {
            console.error(err);
            res.status(500).json({'message':'DB Result error'});
          }
          else{
            console.log(results);
            res.status(200).json({response:results})
            ;
          }
        })
      connection.end();
    }
    else{
      res.json({message:"Not Exist UUID"});
    }

저희는 express로 DB 에 대한 API 를 작성해야 하기 때문에 위와같이 작성이 되었습니다.

http://{serverIP}/action/uuid 와 같은 형태입니다.   action?uuid= 같은 형태가 아니기때문에 

(? 뒤에 붙는 부분들을 fragment라고 하는데 express에서는 req.query로 작성하면 되지만 위와같은 경우는)

req.params으로 받습니다.  

 

유의사항

기존에는 connection.query -> 변경된 코드를 유심히 보면 connection.exeucte 입니다. 

또한 values를 매핑하기 위한 (? 에 대칭 ) 두번째 파라미터는 [ ] -> 배열의 형태를 가지고있습니다. 이부분 유의하여서 작성하시기 바랍니다. 

 

기본적인 sql Injection 에 대한 SQL prepared statements 였습니다.

 

감사합니다.

 

안녕하세요 깍돌이입니당 ~

저번 포스팅까지는 Mysql 8.0 을 서버에 설치하고  간단하게 SQL 문 등 몇가지 테스트만 해보았는데요

이제는 해당서버에 있는 DB와 연동하기 위한 Manager 서버를 구성하려고 합니다.

 

Node.js 관련 포스팅은 다른글에도 있고 하니 기본적인건 제외 하고 시작 하도록 하겠습니다.

 

Package.json

해당 파일 이미지를 통해 어떠한 부분을 생성하였고 안하였는지 체크 하면 될 것 같습니다.

devDependencies -> npm i -D [모듈명] 으로 설치 한 모듈입니다.

dependencies ->npm i 로 [모듈명] 으로 설치 한 모듈입니다.

** mysql2 의 @types 는  npm i -D types/mysql2 으로 설치합니다.

  "dependencies": {
    "axios": "^0.20.0",
    "cookie-parser": "^1.4.5",
    "cors": "^2.8.5",
    "dotenv": "^8.2.0",
    "express": "^4.17.1",
    "helmet": "^4.1.0",
    "moment": "^2.27.0",
    "mysql2": "^2.1.0"
  },
  "devDependencies": {
    "@types/axios": "^0.14.0",
    "@types/cookie-parser": "^1.4.2",
    "@types/cors": "^2.8.7",
    "@types/express": "^4.17.7",
    "@types/moment": "^2.13.0",
    "@types/multer": "^1.4.4",
    "@types/mysql2": "github:types/mysql2",
    "@types/node": "^14.6.2",
    "@types/node-cron": "^2.0.3",
    "@types/serve-favicon": "^2.5.0",
    "pm2": "^4.4.1",
    "typescript": "^4.0.2"
  }

위와같이 설치 되었습니다. ( 작업하다 더 늘어나면 해당 글에 최신화를 할 예정입니다. )

현재 mysql8에는 

위와같이 테스트 데이터를 넣어 놓은 상태입니다.

 

Node.js에서 해당 파일을 mysql2 모듈을 이용해서 CRUD 하는 작업을 간단하게 작성 하도록 하겠습니다.

 

Express & TypeScript

Express.js를 TypeScript로 일단 기본 미들웨어 세팅을 구성하려고 합니다.

 

github.com/lgance/network.git

 

lgance/network

Contribute to lgance/network development by creating an account on GitHub.

github.com

위의 network 로 되어있는 프로젝트가 typescript & Express.js의 Boilerplate code 입니다. 현재는 더이상 사용하거나 추가하지 않기 때문에 위의 프로젝트를 기준으로 작성 하겠습니다.

 

Mysql 연결

기본적인 틀은 위의 github을 통해서 작성을 한 상태로 Mysql 을 연결하는 Router 만 작성하도록 하겠습니다.

 

import * as express from 'express';
import Controller from '../interface/controller.interface'


class ActionStatusController implements Controller {
  public path='/action';
  public pathId='/action/:id';
  public router = express.Router();

  constructor(){
    this.initializeRoutes();
  }
  private initializeRoutes(){
    /** 모든 액션 가져오기  */
    this.router.get(this.path,this.readAllActionStatus);
    /** 특정 액션 가져오기 */
    this.router.get(this.pathId,this.readActionStatus);
    /** 특정 액션 지우기*/
    this.router.delete(this.pathId,this.deleteActionStatus);
    /** 액션 생성 */
    this.router.post(this.pathId,this.createActionStatus);
    /** 액션 변경 */
    this.router.put(this.pathId,this.updateActionStatus);
  }

  private readAllActionStatus = async (req:express.Request,res:express.Response)=>{
    const _str = this._LOG(req).concat(' Read All Action Status');
    res.send(_str);
  }
  private createActionStatus = async(req:express.Request,res:express.Response)=>{
    const _str = this._LOG(req).concat(' Create Action Status');
    res.send(_str);
  }
  private readActionStatus = async (req:express.Request,res:express.Response)=>{
    const _str = this._LOG(req).concat(' Read Action Status');
    res.send(_str);
  }
  private updateActionStatus = async(req:express.Request,res:express.Response)=>{
    const _str = this._LOG(req).concat(' Update Action Status');
    res.send(_str);
  }
  private deleteActionStatus = async(req:express.Request,res:express.Response)=>{
    const _str = this._LOG(req).concat(' Delete Action Status');
    res.send(_str);
  } 

  private _LOG = (req:express.Request):string => {
    const id:string = req.params.id;
    console.log(`Check ID : [${id}]`);
    if(id===undefined){
      return '';
    }
    else{
      return id;
    }
  }

}
export default ActionStatusController;

기본적으로 action Controller를 하나 생성합니다.

 

각각의 기본적인 CRUD를 작성하겠습니다. ( 테스트용 ) 

초반이니까 얼렁 얼렁 작성해서 ㅎㅎ;;

 

일단  http://localhost/action 로 접속 해서 확인해봅시다.

 

정상적으로 나왔으니까 이제 mysql2을 이용하여서 간단한 쿼리로 SELECT 문을 이용하여 가져와 봅니다.

 

mysql 모듈 Import

import * as mysql from 'mysql2';

DB Config 파일 설정 

 private db_config = {
    "host":process.env.DB_HOST,
    "user":process.env.DB_USERNAME,
    "password":process.env.DB_PASSWORD,
    "database":process.env.DB_NAME
  }

ReadAllActionStatus Controller 수정 

 private readAllActionStatus = async (req:express.Request,res:express.Response)=>{
    const _str = this._LOG(req).concat(' Read All Action Status');
    const table = 'status_table';
    const query = `SELECT * FROM ${table}`;

    const connection = mysql.createConnection(this.db_config);
    connection.connect();

    connection.query(query,function(error,results,fields){
      if (error) throw error;
      console.log(results);
  
      res.send(results);
      console.log('connected as id ' + connection.threadId);
    });
    connection.end();
   
  }

 

 

결과는 ? 

 

네 정상적으로 연결되어 값을 가져옴을 확인하였습니다.  하지만 여러가지 문제가 있습니다.

 

1. query Callback 

connection.query 의 Callback 으로 결과를 받은 후에 res.send 를 해주기 때문에 Client한테 지연을 줄수 있습니다. (좋은방법 X)

 

2. SQL Injection 

sql injection 에 당하기 딱 좋은 방법이죠 위의 코드에선 안당하겠지만 (너무단순한 SQL 문이라)

이부분을 유의하여서 앞으로 작성하도록 하겠습니다.

 

감사합니다.

 

Thanks for always

 

 

 

 

 

 

 

 

안녕하세요 기본 세팅 및 정리  [2] 를 작성하다가 컴퓨터를 꺼버리게 되어서 작업 내용이 날아갔네요 ㅜㅜ

 

다시 기억나는대로 작성 해야겠습니다.  관련 내용은 ( DB Tool 인 Dbeaver 와  외부접속을 위한 Mysql 8.0 서버 세팅 등..)

 

이번에는 서버 세팅후에 이것저것 하나씩 찾아 보았던 부분들을 하나의 페이지로 작성하려고 합니다. 현재 DB 를 학부생 시절 이후로 아예 지식이 전무한 상태에서 DB관련 작업 및 세팅을 해야되서 구글의 힘을 빌리고 있습니다. ㅜ 

 

간단 시연 후에는 전반적으로 DB자체에 대한 공부를 진행 하여 포스팅 하도록 하겠습니다.

 

Date ( Mysql Date 현재 시간 )

Test Automation Platform 을 제작하는 부분에 있어서 모든 액션의 단계에서는 언제 실행되었는지에 대한 값이 필요하기 때문에 Date라는 컬럼이 꼭 필요하여야 했습니다. 또한 절대 없으면 안되는 값이 였구요  그래서 이 값을 DB에서 제공하는 현재 시간 값을 자동으로 넣을지 혹은 node.js에서 new Date를 찍어서 DB에 넣어 줄지에 대한 선택이 필요 하였고 근처 지인 개발자들에게 질문 했을 시 답은 DB에 있는 기본 날짜를 사용하라 였습니다.  단순하게 보자면 DB에서 new Date를 기본값으로 찍히는게 Node.js 에서 매번 insert 할 때 마다 new Date를 찍는거보다 훨씬 빠르고 부하가 적기 때문입니다.

 

그래서 생성된 Date컬럼에 기본값으로 넣어 주기 위해서 아래의 SQL문을 작성 하였습니다.

ALTER TABLE [테이블명] MODIFY COLUMN [컬럼명] datetime DEFAULT current_timestamp NOT NULL;
ALTER TABLE status_table MODIFY COLUMN `date` datetime DEFAULT current_timestamp NOT NULL;
ALTER TABLE [테이블명] MODIFY COLUMN [컬럼명] datetime DEFAULT now() NOT NULL;
ALTER TABLE status_table MODIFY COLUMN `date` datetime DEFAULT now() NOT NULL;

 

저는 현재 Mysql 8.0 버전을 사용하고 있으며 위의 2개 SQL문 이 되는 점을 확인 하였습니다.

alter를 이용하여 date 컬럼에 기본 값으로 now() 혹은 current_timestamp 을 사용하며 해당 컬럼은 NOT NULL 으로 설정 합니다.

 

Insert 문을 이용하여서 테스트시 

정상적으로 기본 날짜로 들어가는 점이 확인되었습니다.

 

AutoIncrement  초기화

NO 컬럼을 이용하여서 DB의 autoincrement 을 사용하지만 개발 중 테스트 단계에서는 이부분은 초기화 할 일이 있었습니다. 당연히 이후로는 없어야 할 것 이구요

NO 컬럼에 autoincrement가 들어가있습니다. 테스트 중이니 추후에 는 꼭 초기화가 필요합니다.

1- 초기화 를 위해 아래의 SQL문을 사용 합니다. 

ALTER TABLE [테이블 명] AUTO_INCREMENT = [시작 값]
ALTER TABLE status_table AUTO_INCREMENT=0

 ALTER TABLE [테이블명] AUTO_INCREMENT=[시작값] 인데요

 

해당 SQL문 전에

위와같은 데이터가 있은 후에 다시 INSERT를 하게되면

보시는 것처럼 AUTO_INCREMENT 값이 초기화가 되어있지 않습니다.  이를 동작하게 하기위해선 시작하는 AUTO_INCREMENT 값보다 더 큰 값이 있으면 안되기 때문인데요 해당 데이터를 전부 날려보고 다시 작업합니다.

 

DELETE FROM [Table명]
DELETE FROM status_table

아직은 테스트 단계니 날려보고 다시 INSERT를 하게되면

0부터 시작하게 되어 1이 표시됨을 확인 할수 있습니다.

 

 

 

 

 

 

 

 

 

+ Recent posts