Node.js에서 실시간 코드 업데이트 구현하기

Node.js 환경에서 동적 코드 업데이트 구현하기

오늘 Jeff의 글을 읽다가 Jack Moffitt의 게시물이 눈길을 끌었습니다. 그는 실행 중인 Python 프로세스가 동적으로 코드를 다시 로드할 수 있게 하는 해킹 기법을 논의했습니다. 그 해킹 자체는 말 그대로 정교하지 않지만, Jack의 글은 나에게 생각할 거리를 주었습니다. 사실 Erlang의 핫 코드 로딩은 99.9999999% 가동률이라는 Erlang의 주장을 가능하게 하는 훌륭한 기능입니다. node.js의 CommonJS 기반 모듈 로더에 이를 구현하는 것이 그리 어렵지 않을 것 같다는念头가 떠올랐습니다.

몇 시간 뒤(그리고 맛있는 홈메이드 파에야를 먹고나서), 제가 낸 답은 Hotload 노드 브랜치입니다.

대체 이것이 뭘 하는가?

var responseProcessor = require('./myResponseProcessor');

process.watchFile('./myResponseProcessor', function () {
  module.unCacheModule('./myResponseProcessor');
  responseProcessor = require('./myResponseProcessor');
}

var handleRequest = function (request, response) {
  responseProcessor.process(request, response);
}

http.createServer(handleRequest).listen(8080);

이제 myResponseProcessor.js를 수정할 때마다 위 코드는 변경을 감지하고 새로운 코드로 로컬의 responseProcessor를 교체합니다. 기존에 처리 중인 요청은 이전 코드를 계속 사용하고, 새로 들어오는 요청은 새로운 코드를 사용합니다. 서버를 내리지 않고도, 요청을 중단하거나 미리 종료시키거나, inteligent 로드 밸런서에 의존하지 않고도 이 모든 것이 가능합니다.

대단하다! 동작 원리가 뭔가?

기본적으로 모든 노드 모듈은 샌드박스로 생성되므로, 전역 변수를 사용하지 않는 한 작성한 모듈이 다른 사람의 코드를 오버라이드하지 않으며, 반대로 다른 사람의 모듈이 내 코드를 오버라이드하지 않음을 확신할 수 있습니다.

모듈은 require()를 호출하고 반환 값을 로컬 변수에 할당하여 로드됩니다:

var http = require('http');

중요한 통찰력은 require()의 반환 값이 독립형 클로저라는 것입니다. 매번 동일할 필요가 없습니다. 본질적으로 require(file)는 "파일을 읽고, 보호 케이스에 넣고, 그 보호 케이스를 반환한다"고 말합니다. require()는 똑똑하게도 여러 번 require() 호출 시 디스크에서 동기적으로 시간을 낭비하지 않도록 모듈을 캐시합니다. 그러나 이러한 캐시는 무효화되지 않으며, 파일 변경을 감지할 수 있다고 하더라도 캐시된 버전이 우선시되므로 단순히 require()를 다시 호출할 수 없습니다.

이 문제를 해결하는 몇 가지 방법이 있지만, 세세한 부분들은 빠르게 상황을 복잡하게 만듭니다. 이미 실행 중인 모듈(예: HTTP 요청 핸들러)이 계속 실행되면서 새 코드가 로드되도록 하는 것이 최종 목표라면 자동 코드 리로딩은 불가능합니다. 한 모듈을 변경하면 모든 모듈이 변경되기 때문입니다. 제가 취한 접근 방식에서는 두 가지 목표를 달성하려 했습니다:

  1. 기존 node.js require() 로직에 최소한의 변경을 가합니다.
  2. 이미 로드된 모듈 내의 모든 require() 호출이 핫 로드 이전 버전의 코드에 해당하는 함수를 반환하도록 합니다.

後자 목표는 모듈이 의존하는 모듈에서 특정 동작 집합을 기대하기 때문에 중요합니다. 핫 로딩은 모듈들이 일관된 세계관을 가지고 있는 한만 작동합니다.

이러한 목표를 달성하기 위해 제가 한 것은 모듈 캐시를 전역에서 모듈 자체로 이동하는 것입니다. 리로딩은 부모의 캐시를 자식 모듈로 복사하여 최소화했습니다(V8의 변수 처리 방식 덕분에 빠르고 효율적입니다). 모든 모듈은 먼저 로컬 캐시에서 해당 모듈을 제거함으로써 로드된 모듈의 새 버전을 로드할 수 있습니다. 이는 다른 모든 모듈(종속 모듈 포함)에 영향을 주지 않지만, 부모의 캐시에 없는 한 하위 모듈이 다시 로드됩니다.

상대적으로 보수적인 모듈 리로딩 접근 방식을 통해 이것은 유연하고 강력한 핫 코드 리로딩 접근 방식이라고 믿습니다. 대부분의 서버 애플리케이션은 strongly 계층적인 코드 구조를 가지고 있습니다. 코드 리로딩이 많은 모듈이 require되기 전에 상위 수준에서 수행되는 한, 간단하고 효율적으로 수행될 수 있습니다.

이 패치나 수정된 버전이 node.js에 포함되기를 바라지만, 이 접근 방식은 node의 핵심 외부에서 require() 구현 두 개를 유지하는 비용으로 적용될 수 있습니다.

태그: nodejs hot-reloading CommonJS module-system v8

5월 31일 19:20에 게시됨