avatar
Published on

Nodejs 프로세스를 종료시키는 방법

Author
  • avatar
    Name
    yceffort

nodejs 프로세스가 종료되는 상황으로는 여러가지가 있다. 에러가 발생하는 케이스와 같이 사전에 예방할 수 있는 경우가 있고, 혹은 메모리 부족과 시스템 오류와 같은 예방할 수 없는 것이 있다. 이 Process Global은 Event Emitter 인스턴스이며, graceful exit가 실행되면, 종료 이벤트를 발생 (emit) 한다. 그러면 애플리케이션 코드가 이 이 벤트를 수신하여 마지막 순간에 동기로 일어나는 정리 작업을 할 수 있다.

다음은 프로세스 종료를 의도적으로 발생시킬 수 있는 몇가지 방법이다.

Operation예시
수동 프로세스 종료process.exit(1)
Uncaught exceptionthrow new Error()
Unhandled promise rejectionPromise.reject()
error event 무시EventEmitter#emit('error')
Unhandled Signals$ kill <PROCESS_ID>

이러한 오류 중 대부분은 uncaught errors unhandled rejects와 같이 실수로 발생되는 경우도 있지만, 이 들 중 일부는 프로세스를 직접 종료하기 위해 만들어 진 것이다.

Process Exit

process.exit(code)는 프로세스를 종료하기 위한 가장 간단한 도구다. 프로세스의 수명이 다하여 종료시켜도 되는 경우에 스크립트를 작성할 때 매우 유용하다. 이 코드는 선택사항이며, 기본값은 0 이고 0에서 255까지 선택 가능하다. 0은 성공적인 프로세스 실행을 나타내는 반면, 0이 아닌 숫자는 사고가 발생했다는 것을 나타낸다. 이러한 값은 다양한 외부 툴에서 사용된다. 예를 들어, 테스트를 실행 할 때, 0이 아니면 테스트가 실패한 것이다.

process.exit가 직접 실행되면, 콘솔에는 암묵적으로 텍스트가 출력되지 않는다. 오류를 알리기 위해 이 메서드를 호출하는 경우, 사용자가 직접 오류를 찍어야 한다.

$ node -e "process.exit(42)"
$ echo $?

이 경우, shell이 종료를 나타내긴 했지만, nodejs 애플리케이션에서는 이 메시지가 출력되지 않았다. 이렇게 되면 사용자는 무슨 일이 일어났는지를 알지 못한다. 따라서 아래와 같이 종료시키는 것이 좋다.

function checkConfig(config) {
  if (!config.host) {
    console.error("Configuration is missing 'host' parameter!")
    process.exit(1)
  }
}

사용자는 이 경우 명확하게 이해할 수 있다. 콘솔에 에러가 찍히고, 사용자는 이 상황에 대해 이해하고 해걸할 수 있다.

process.exit()는 매우 강력한 도구다. 하지만 재사용 가능한 라이브러리에 이 코드를 사용해서는 안된다.라이브러리에서 오류가 발생하면 애플리케이션이 오류를 어떻게 할지 결정할 수 있도록 오류를 생성해야 한다.

Exceptions, Rejections, 그리고 Emitted Error

process.exit()는 시작/설정단계에서 사용할 수 있는 강력한 도구인반면, 실행 단계에서는 다른 툴을 사용해야한다. 예를 들어, 애플리케이션이 http 요청을 처리할 때 발생하는 오류는 프로세스를 종료하지 않고 오류 응답만 반환해야 한다. 오류가 발생한 위치에 대한 정보를 노출하는 것도 필요하다. 따라서 여기서 던져진 오류 객체가 유용하다.

Error 클래스의 인스턴스에는 스택 추적 및 메시지 문자열과 같이 오류의 원인을 파악하는데 유용한 메타데이터가 포함되어 있다. Error 클래스를 기반으로 사용자가 고유의 애플리케이션 Error 클래스를 만들어서 확장해서 사용하는 것이 일반적이다. Error를 인스턴스화하는 것 자체로는 부수효과가 없다. (=별일이 일어나지 않는다.) 오류가 발생하기 위해서는, 이 Error 클래스를 던져야 한다.

에러는 throw 키워드를 사용해서 던지거나, 특정 논리적인 오류가 발행할때 나타난다. 이러한 상황이 나타나면 현재 스택은 unwinds가 된다. 이 뜻은 각 함수가 try...catch 가 감싸는 문구를 만날 때까지 종료됨을 의미한다. 만약 try...catch를 만나지 못한다면, 이 는 uncaught된 에러로 간주한다.

throw 키워드를 사용하여 throw new Error('hi')와 같이 에러를 던지는 것은, 기술적으로 무엇이든 던질 수 있다. 무엇이든 던져지게 되면 이는 예외로 간주된다. 이렇게 던져지는 에러 인스턴스는 이 인스턴스를 기반으로 에러의 속성을 예상할 수 있으므로, 에러 인스턴스를 생성하는 것이 중요하다.

Node.js 라이브러리 내부에서 널리 사용되는 또다른 패턴은, 릴리즈 간에 일관성을 유지하기 위한 .code 값을 제공하는 것이다. 일례로 ERR_INVALID_URI가 있는데, 사람이 읽을 수 있는 message는 바뀔 수 있지만, .code 는 바뀌지 않는다.

안타깝게도, 에러를 구분하는 방법 중 또다른 하나는 .message 프로퍼티를 사용하는 것인데, 이는 위험하고 오류가 발생하기 쉽다. Node.js에서는 모든 라이브러리에서 오류를 완벽하게 구분할 수 있는 방법은 없다.

uncaught 에러가 스택에 던져지면, 콘솔에 찍히고 프로세스가 종료되며, 종료 상태값은 1이다. 이러한 예외의 예제를 살펴보자.

/tmp/foo.js:1
throw new TypeError('invalid foo');
^
Error: invalid foo
    at Object.<anonymous> (/tmp/foo.js:2:11)
    ... TRUNCATED ...
    at internal/main/run_main_module.js:17:47

process 글로벌은 Event Emitter로 uncapturedException 이벤트를 수신하여 uncaught 에러를 처리하는데 사용한다.

const logger = require('./lib/logger.js')
process.on('uncaughtException', (error) => {
  logger.send('An uncaught exception has occured', error, () => {
    console.error(error)
    process.exit(1)
  })
})

Promise Rejection은 에러를 던지는 것과 유사하다. Promise에서 reject() 메서드가 호출되거나, 비동기 함수내에서 에러가 던져지는 경우 사용된다.

Promise.reject(new Error('oh no'))
;(async () => {
  throw new Error('oh no')
})()
(node:52298) UnhandledPromiseRejectionWarning: Error: oh no
    at Object.<anonymous> (/tmp/reject.js:1:16)
    ... TRUNCATED ...
    at internal/main/run_main_module.js:17:47
(node:52298) UnhandledPromiseRejectionWarning: Unhandled promise
  rejection. This error originated either by throwing inside of an
  async function without a catch block, or by rejecting a promise
  which was not handled with .catch().

uncaught exception와는 다르게, 이러한 거부로 인해 node.js v14 에서는 크래쉬하지 않는다. 그러나, 그 이후 버전부터는 프로세스가 크래쉬된다. 또한 , 이 이벤트는 다음과 같이 캐치할 수 있다.

process.on('unhandledRejection', (reason, promise) => {})

Event Emitter는 nodejs에서 흔한 패턴으로, 라이브러리와 애플리케이션 등에서 기본 클래스에서 확장한 많은 객체들이 존재한다.

Event Emitter가 error 이벤트를 발생시켰는데 여기에 아무런 리스너가 없다면, Emitter가 내보낸 인수를 던진다. 그렇게 되면 에러가 나서 프로세스가 종료된다.

events.js:306
    throw err; // Unhandled 'error' event
    ^
Error [ERR_UNHANDLED_ERROR]: Unhandled error. (undefined)
    at EventEmitter.emit (events.js:304:17)
    at Object.<anonymous> (/tmp/foo.js:1:40)
    ... TRUNCATED ...
    at internal/main/run_main_module.js:17:47 {
  code: 'ERR_UNHANDLED_ERROR',
  context: undefined
}

작업하는 Event Emitter 인스턴스에서 에러 이벤트를 수신하여, 애플리케이션이 멈추지 않고 이벤트를 정상적으로 처리할 수 있도록 해야 한다.

Signal

시그널은 운영체제에서 하나의 프로그램에서 다른 프로그램으로 작은 숫자 메시지를 보내기 위해 제공하는 메커니즘이다. 이러한 숫자는 상수 문자열로 참조되는 경우가 많다. 예를 들어, 시그널 SIGKILL은 숫자 9의 시그널을 나타낸다.

운영 체제에 따라 서로다른 시그널이 정의될 수 있지만, 아래 목록은 일반적으로 범용이다.

이름숫자handleableNode.js 동작목적
SIGUP1YES종료부모 터미널이 종료된 경우
SIGINT2YES종료Ctrl + C로 터미널에 간섭하는 경우
SIGQUIT3YES종료Ctrl + D로 터미널을 끝내려는 경우
SIGKILL9NO종료프로세스가 강제로 죽는 경우
SIGUSR110YES디버거 시작사용자 정의 시그널 1
SIGUSR212YES종료사용자 정의 시그널 2
SIGUSR110YES종료정상종료
SIGUSR119NO종료프로세스가 강제로 멈추는 경우

프로그램에서 이러한 시그널 처리를 구현할 수 있도록 한 경우, Handleable이 YES 다. NO로 표시되어 있는 경우 처리할 수 없다. Node.js 동작은 신호가 수신되었을 때 Node.js 프로그램의 기본작업을 나타낸다. 마지막 열은, 일반적으로 어떻게 사용되는지 알려준다.

Node.js에서 이러한 시그널을 수신하기 위해서는, 아래처럼 process객체에 이벤트 리스너를 달면 된다.

#!/usr/bin/env node
console.log(`Process ID: ${process.pid}`)
process.on('SIGHUP', () => console.log('Received: SIGHUP'))
process.on('SIGINT', () => console.log('Received: SIGINT'))
setTimeout(() => {}, 5 * 60 * 1000) // keep process alive

이 프로그램을 터미널에서 실행하고, Ctrl+C를 해보면, 프로세스가 죽지 않는다. 그 대신, SIGINT시그널을 받는다. 다른 터미널 창으로 가서, 프로세스 ID 값을 기준으로

$ kill -s SIGHUP <PROCESS_ID>

를 실행하면, 이는 한 프로그램이 다른 프로그램으로 신호를 보낼 수 있다는 것을 알 수 있다. 이전 터미널에서 실행중인 node.js 프로그램이 SIGHUP 신호를 수신하여 인쇄한다.

눈치챘을 수도 있지만, Node.js 는 다른 프로그램에도 명령을 전송할 수 있다.

$ node -e "process.kill(<PROCESS_ID>, 'SIGHUP')"

이는 첫번째 프로그램에 SIGHUP를 표시하게 한다. 만약, 해당 프로세스를 종료 시키고 싶다면 아래 명령어를 통해서 SIGKILL 시그널을 보내면 된다.

$ kill -9 <PROCESS_ID>

이 시점에서, 애플리케이션은 종료된다.

이러한 시그널은 정상 종료 처리 이벤트를 처리하기 위해 Node.js 애플리케이션에서 많이 사용된다. 예를 들어 쿠버네틱스의 pod가 종료되면 애플리케이션에 SIGTERM 신호를 보낸다음, 30초 타이머를 시작한다. 그러면 애플리케이션이 30초 내에 정상적으로 종료되면서 연결을 닫고, 데이터를 저장할 수 있다. 타이머 이후에도 프로세스가 활성화 되어있으면 쿠버네틱스가 SIGKILL을 보낸다.

https://thomashunter.name/posts/2021-03-08-the-death-of-a-nodejs-process