도메인
[히스토리]
버전 | 변경 사항 |
---|---|
v8.8.0 | VM 컨텍스트에서 생성된 모든 Promise 는 더 이상 .domain 속성을 갖지 않습니다. 하지만 해당 핸들러는 여전히 적절한 도메인에서 실행되며, 메인 컨텍스트에서 생성된 Promise 는 여전히 .domain 속성을 가지고 있습니다. |
v8.0.0 | Promise 의 핸들러는 이제 체인의 첫 번째 Promise가 생성된 도메인에서 호출됩니다. |
v1.4.2 | 사용 중지됨: v1.4.2 |
소스 코드: lib/domain.js
이 모듈은 사용 중지 예정입니다. 대체 API가 최종 확정되면 이 모듈은 완전히 사용 중지됩니다. 대부분의 개발자는 이 모듈을 사용할 필요가 없습니다. 도메인이 제공하는 기능이 절대적으로 필요한 사용자는 당분간 이를 사용할 수 있지만, 향후 다른 솔루션으로 마이그레이션해야 할 것으로 예상해야 합니다.
도메인은 여러 개의 다른 IO 작업을 단일 그룹으로 처리하는 방법을 제공합니다. 도메인에 등록된 이벤트 이미터 또는 콜백 중 하나가 'error'
이벤트를 방출하거나 오류를 throw하면 process.on('uncaughtException')
핸들러에서 오류의 컨텍스트를 잃거나 오류 코드와 함께 프로그램이 즉시 종료되는 대신 도메인 객체에 알림이 전달됩니다.
경고: 오류를 무시하지 마세요!
도메인 오류 핸들러는 오류가 발생할 때 프로세스를 종료하는 대체 방법이 아닙니다.
throw
가 JavaScript에서 작동하는 방식의 본질상, 참조 누수나 다른 종류의 정의되지 않은 취약한 상태를 생성하지 않고 "중단된 지점부터 안전하게 다시 시작할" 방법이 거의 없습니다.
throw된 오류에 응답하는 가장 안전한 방법은 프로세스를 종료하는 것입니다. 물론 일반적인 웹 서버에서는 많은 열린 연결이 있을 수 있으며, 다른 사용자에 의해 오류가 트리거되었다는 이유로 이러한 연결을 갑자기 닫는 것은 합리적이지 않습니다.
더 나은 방법은 오류를 트리거한 요청에 오류 응답을 보내는 동시에 다른 요청은 정상적인 시간에 완료하도록 하고 해당 작업자에서 새 요청 수신을 중지하는 것입니다.
이러한 방식으로 domain
사용은 클러스터 모듈과 함께 사용되며, 기본 프로세스는 작업자가 오류를 발생시키면 새 작업자를 포크할 수 있습니다. 여러 머신으로 확장되는 Node.js 프로그램의 경우 종료 프록시 또는 서비스 레지스트리가 오류를 인식하고 적절하게 반응할 수 있습니다.
예를 들어, 이것은 좋은 생각이 아닙니다.
// XXX 경고! 나쁜 생각!
const d = require('node:domain').create()
d.on('error', er => {
// 오류로 인해 프로세스가 충돌하지는 않지만, 그 결과는 더 심각합니다!
// 갑작스러운 프로세스 재시작을 방지했지만, 이런 일이 발생하면 많은 리소스가 누출됩니다.
// 이것은 process.on('uncaughtException')보다 나을 게 없습니다!
console.log(`오류 발생, 하지만 괜찮아요 ${er.message}`)
})
d.run(() => {
require('node:http')
.createServer((req, res) => {
handleRequest(req, res)
})
.listen(PORT)
})
도메인의 컨텍스트와 프로그램을 여러 작업 프로세스로 분리하는 복원력을 사용하여 더 적절하게 반응하고 훨씬 더 안전하게 오류를 처리할 수 있습니다.
// 훨씬 더 좋습니다!
const cluster = require('node:cluster')
const PORT = +process.env.PORT || 1337
if (cluster.isPrimary) {
// 더 현실적인 시나리오에서는 2개 이상의 작업자가 있고, 기본 및 작업자를 동일한 파일에 두지 않을 수도 있습니다.
// 로깅을 좀 더 세련되게 하고 DoS 공격 및 기타 악의적인 동작을 방지하기 위해 필요한 사용자 정의 로직을 구현할 수도 있습니다.
// 클러스터 문서의 옵션을 참조하십시오.
// 중요한 점은 기본이 거의 아무것도 하지 않아 예상치 못한 오류에 대한 복원력이 향상된다는 것입니다.
cluster.fork()
cluster.fork()
cluster.on('disconnect', worker => {
console.error('연결 끊김!')
cluster.fork()
})
} else {
// 작업자
//
// 여기에 버그를 넣습니다!
const domain = require('node:domain')
// 요청을 처리하기 위한 작업 프로세스 사용에 대한 자세한 내용은 클러스터 문서를 참조하십시오. 작동 방식, 주의 사항 등
const server = require('node:http').createServer((req, res) => {
const d = domain.create()
d.on('error', er => {
console.error(`오류 ${er.stack}`)
// 위험한 영역에 있습니다!
// 정의상 예상치 못한 일이 발생했으며, 아마 원하지 않았을 것입니다.
// 이제 무슨 일이든 일어날 수 있습니다! 매우 조심하십시오!
try {
// 30초 이내에 종료되도록 합니다.
const killtimer = setTimeout(() => {
process.exit(1)
}, 30000)
// 하지만 그 때문에 프로세스를 열어두지 마십시오!
killtimer.unref()
// 새 요청을 받지 않습니다.
server.close()
// 기본에 우리가 죽었다는 것을 알립니다. 이렇게 하면 클러스터 기본에 'disconnect'가 트리거되고 새 작업자가 포크됩니다.
cluster.worker.disconnect()
// 문제를 일으킨 요청에 오류를 보내려고 합니다.
res.statusCode = 500
res.setHeader('content-type', 'text/plain')
res.end('죄송합니다. 문제가 발생했습니다!\n')
} catch (er2) {
// 뭐, 이 시점에서는 할 수 있는 일이 많지 않습니다.
console.error(`500 전송 오류! ${er2.stack}`)
}
})
// req 및 res는 이 도메인이 존재하기 전에 생성되었으므로 명시적으로 추가해야 합니다.
// 아래의 암시적 바인딩과 명시적 바인딩에 대한 설명을 참조하십시오.
d.add(req)
d.add(res)
// 이제 도메인에서 핸들러 함수를 실행합니다.
d.run(() => {
handleRequest(req, res)
})
})
server.listen(PORT)
}
// 이 부분은 중요하지 않습니다. 단지 라우팅 예시입니다.
// 여기에 고급 애플리케이션 로직을 넣으십시오.
function handleRequest(req, res) {
switch (req.url) {
case '/error':
// 비동기 작업을 수행한 다음...
setTimeout(() => {
// 엇!
flerb.bark()
}, timeout)
break
default:
res.end('ok')
}
}
Error
객체에 추가되는 내용
Error
객체가 도메인을 통해 라우팅될 때마다 몇 가지 추가 필드가 추가됩니다.
error.domain
오류를 처음 처리한 도메인.error.domainEmitter
'error'
이벤트를 오류 객체와 함께 방출한 이벤트 방출기.error.domainBound
도메인에 바인딩되었고, 첫 번째 인수로 오류를 전달받은 콜백 함수.error.domainThrown
오류가 throw되었는지, 방출되었는지, 아니면 바인딩된 콜백 함수에 전달되었는지 여부를 나타내는 부울 값.
암시적 바인딩
도메인이 사용 중인 경우, 모든 새로운 EventEmitter
객체(스트림 객체, 요청, 응답 등 포함)는 생성 시점의 활성 도메인에 암시적으로 바인딩됩니다.
또한, 저수준 이벤트 루프 요청(예: fs.open()
또는 기타 콜백을 받는 메서드)에 전달되는 콜백은 활성 도메인에 자동으로 바인딩됩니다. 콜백에서 예외가 발생하면 도메인이 해당 오류를 catch합니다.
과도한 메모리 사용을 방지하기 위해 Domain
객체 자체는 활성 도메인의 자식으로 암시적으로 추가되지 않습니다. 그렇게 되면 요청 및 응답 객체가 제대로 가비지 수집되는 것을 방지하기가 너무 쉬워집니다.
Domain
객체를 상위 Domain
의 자식으로 중첩하려면 명시적으로 추가해야 합니다.
암시적 바인딩은 throw된 오류와 'error'
이벤트를 Domain
의 'error'
이벤트로 라우팅하지만, EventEmitter
를 Domain
에 등록하지는 않습니다. 암시적 바인딩은 throw된 오류와 'error'
이벤트만 처리합니다.
명시적 바인딩
때때로 사용 중인 도메인이 특정 이벤트 방출기에 사용되어야 하는 도메인이 아닐 수 있습니다. 또는 이벤트 방출기가 한 도메인의 컨텍스트에서 생성되었지만 다른 도메인에 바인딩되어야 할 수도 있습니다.
예를 들어, HTTP 서버에 대해 하나의 도메인을 사용할 수 있지만, 각 요청에 대해 별도의 도메인을 사용하려고 할 수 있습니다.
명시적 바인딩을 통해 가능합니다.
// 서버에 대한 최상위 도메인 생성
const domain = require('node:domain')
const http = require('node:http')
const serverDomain = domain.create()
serverDomain.run(() => {
// 서버는 serverDomain의 범위에서 생성됨
http
.createServer((req, res) => {
// Req와 res도 serverDomain의 범위에서 생성됨
// 하지만 각 요청에 대해 별도의 도메인을 사용하는 것이 좋습니다.
// 먼저 생성하고, req와 res를 추가합니다.
const reqd = domain.create()
reqd.add(req)
reqd.add(res)
reqd.on('error', er => {
console.error('Error', er, req.url)
try {
res.writeHead(500)
res.end('Error occurred, sorry.')
} catch (er2) {
console.error('Error sending 500', er2, req.url)
}
})
})
.listen(1337)
})
domain.create()
- 반환값: <Domain>
클래스: Domain
- 상속: <EventEmitter>
Domain
클래스는 활성화된 Domain
객체로 라우팅되는 오류 및 처리되지 않은 예외의 기능을 캡슐화합니다.
이 클래스가 잡은 오류를 처리하려면 'error'
이벤트를 수신합니다.
domain.members
도메인에 명시적으로 추가된 타이머와 이벤트 방출기의 배열입니다.
domain.add(emitter)
emitter
<EventEmitter> | <Timer> 도메인에 추가할 이벤트 방출기 또는 타이머
이벤트 방출기를 도메인에 명시적으로 추가합니다. 이벤트 방출기에서 호출된 이벤트 핸들러가 오류를 발생시키거나 이벤트 방출기가 'error'
이벤트를 방출하면 암시적 바인딩과 마찬가지로 도메인의 'error'
이벤트로 라우팅됩니다.
setInterval()
및 setTimeout()
에서 반환된 타이머에도 적용됩니다. 해당 콜백 함수가 오류를 발생시키면 도메인 'error'
핸들러에서 잡습니다.
타이머 또는 EventEmitter
가 이미 도메인에 바인딩된 경우 해당 도메인에서 제거되고 이 도메인에 바인딩됩니다.
domain.bind(callback)
callback
<Function> 콜백 함수- 반환값: <Function> 바인딩된 함수
반환된 함수는 제공된 콜백 함수를 래핑합니다. 반환된 함수가 호출될 때 발생하는 오류는 도메인의 'error'
이벤트로 라우팅됩니다.
const d = domain.create()
function readSomeFile(filename, cb) {
fs.readFile(
filename,
'utf8',
d.bind((er, data) => {
// 이 부분에서 오류가 발생해도 도메인으로 전달됩니다.
return cb(er, data ? JSON.parse(data) : null)
})
)
}
d.on('error', er => {
// 어딘가에서 오류가 발생했습니다. 지금 이 오류를 발생시키면 일반적인 줄 번호와 스택 메시지와 함께 프로그램이 크래시됩니다.
})
domain.enter()
enter()
메서드는 run()
, bind()
및 intercept()
메서드에서 활성 도메인을 설정하는 데 사용되는 파이프라인입니다. domain.active
및 process.domain
을 도메인으로 설정하고, 도메인 모듈에서 관리하는 도메인 스택에 도메인을 암시적으로 푸시합니다(도메인 스택에 대한 자세한 내용은 domain.exit()
참조). enter()
호출은 도메인에 바인딩된 비동기 호출 및 I/O 작업 체인의 시작을 구분합니다.
enter()
호출은 활성 도메인만 변경하고 도메인 자체는 변경하지 않습니다. enter()
및 exit()
는 단일 도메인에서 임의의 횟수만큼 호출할 수 있습니다.
domain.exit()
exit()
메서드는 현재 도메인을 종료하고 도메인 스택에서 팝합니다. 실행이 다른 비동기 호출 체인의 컨텍스트로 전환될 때마다 현재 도메인이 종료되었는지 확인하는 것이 중요합니다. exit()
호출은 도메인에 바인딩된 비동기 호출 및 I/O 작업 체인의 끝 또는 중단을 구분합니다.
현재 실행 컨텍스트에 바인딩된 여러 개의 중첩된 도메인이 있는 경우 exit()
는 이 도메인 내에 중첩된 모든 도메인을 종료합니다.
exit()
호출은 활성 도메인만 변경하고 도메인 자체는 변경하지 않습니다. enter()
및 exit()
는 단일 도메인에서 임의의 횟수만큼 호출할 수 있습니다.
domain.intercept(callback)
callback
<Function> 콜백 함수- 반환값: <Function> 인터셉트된 함수
이 메서드는 domain.bind(callback)
과 거의 동일합니다. 그러나 throw된 오류를 catch하는 것 외에도 함수의 첫 번째 인수로 전달된 Error
객체도 인터셉트합니다.
이러한 방식으로 일반적인 if (err) return callback(err);
패턴을 단일 위치의 단일 오류 처리기로 바꿀 수 있습니다.
const d = domain.create()
function readSomeFile(filename, cb) {
fs.readFile(
filename,
'utf8',
d.intercept(data => {
// 참고: 첫 번째 인수는 'Error' 인수로 간주되고 도메인에 의해 인터셉트되므로 콜백에 전달되지 않습니다.
// 이 부분에서 throw가 발생하면 도메인에도 전달되므로 오류 처리 로직을 프로그램 전체에 반복적으로 작성하는 대신 도메인의 'error' 이벤트로 이동할 수 있습니다.
return cb(null, JSON.parse(data))
})
)
}
d.on('error', er => {
// 어딘가에서 오류가 발생했습니다. 지금 throw하면 일반적인 줄 번호와 스택 메시지로 프로그램이 충돌합니다.
})
domain.remove(emitter)
emitter
<EventEmitter> | <Timer> 도메인에서 제거할 이벤트 이미터 또는 타이머
domain.add(emitter)
의 반대입니다. 지정된 이벤트 이미터에서 도메인 처리를 제거합니다.
domain.run(fn[, ...args])
fn
<Function>...args
<any>
지정된 함수를 도메인의 컨텍스트에서 실행하여 해당 컨텍스트에서 생성된 모든 이벤트 이미터, 타이머 및 저수준 요청을 암시적으로 바인딩합니다. 선택적으로 함수에 인수를 전달할 수 있습니다.
이는 도메인을 사용하는 가장 기본적인 방법입니다.
const domain = require('node:domain')
const fs = require('node:fs')
const d = domain.create()
d.on('error', er => {
console.error('Caught error!', er)
})
d.run(() => {
process.nextTick(() => {
setTimeout(() => {
// 다양한 비동기 작업 시뮬레이션
fs.open('non-existent file', 'r', (er, fd) => {
if (er) throw er
// 진행...
})
}, 100)
})
})
이 예제에서는 프로그램이 충돌하는 대신 d.on('error')
핸들러가 트리거됩니다.
도메인과 프로미스
Node.js 8.0.0부터는 프로미스의 핸들러가 .then()
또는 .catch()
자체가 호출된 도메인 내에서 실행됩니다.
const d1 = domain.create()
const d2 = domain.create()
let p
d1.run(() => {
p = Promise.resolve(42)
})
d2.run(() => {
p.then(v => {
// d2에서 실행
})
})
콜백은 domain.bind(callback)
을 사용하여 특정 도메인에 바인딩할 수 있습니다.
const d1 = domain.create()
const d2 = domain.create()
let p
d1.run(() => {
p = Promise.resolve(42)
})
d2.run(() => {
p.then(
p.domain.bind(v => {
// d1에서 실행
})
)
})
도메인은 프로미스의 오류 처리 메커니즘을 방해하지 않습니다. 즉, 처리되지 않은 Promise
거부에 대해 'error'
이벤트가 발생하지 않습니다.