AsyncLocalStorage를 사용하여 Node.js 비동기 체인에서 요청 ID 안전하게 전파하기
James Reed
Infrastructure Engineer · Leapcell

소개
현대의 마이크로서비스 아키텍처와 복잡한 Node.js 애플리케이션에서 단일 사용자 요청은 종종 여러 함수, 모듈, 심지어 외부 서비스에 걸쳐 비동기 작업의 연쇄 작용을 유발합니다. 이러한 작업이 진행됨에 따라, 특히 초기 요청에 대한 고유 식별자와 같은 컨텍스트를 유지하는 것이 효과적인 로깅, 디버깅 및 추적을 위해 중요해집니다. 요청의 여정을 추적할 일관된 방법 없이는 서로 다른 로그에서 이벤트 시퀀스를 재구성하는 것이 엄청난 어려움이 될 수 있으며, 디버깅 시간 연장과 시스템 관찰 가능성 저하로 이어집니다. 명시적인 매개변수 전달을 포함하는 기존 접근 방식은 함수 시그니처를 어지럽히고 관심사 분리를 위반하면서 빠르게 번거롭고 오류가 발생하기 쉽습니다.
이것이 바로 Node.js의 AsyncLocalStorage가 등장하여 비동기 호출 체인 전체에 걸쳐 요청별 데이터를 안전하게 전파하는 강력하고 우아한 솔루션을 제공하는 곳입니다.
비동기 컨텍스트 및 AsyncLocalStorage 이해
실제 구현으로 들어가기 전에 주요 개념에 대한 공통된 이해를 확립해 봅시다.
비동기 컨텍스트
Node.js에서 실행 흐름은 본질적으로 비동기입니다. 네트워크 요청, 파일 I/O 또는 데이터베이스 쿼리와 같은 작업은 메인 스레드를 차단하지 않습니다. 대신 나중에 실행될 콜백을 예약합니다. 이러한 논블로킹 특성이 Node.js를 효율적으로 만드는 이유이지만, 컨텍스트 관리에도 어려움을 야기합니다. 함수가 비동기 작업을 시작하면 await 후 또는 콜백에서 실행되는 후속 코드는 이벤트 루프의 다른 "틱"에서 실행될 수 있으며 원래 호출의 컨텍스트를 잃을 수 있습니다.
요청 ID
요청 ID는 각 수신 요청(예: HTTP 요청)에 할당된 고유 식별자입니다. 이 ID는 특정 요청의 처리를 위해 수행된 모든 로그 및 작업을 연결하는 상관 관계 키 역할을 합니다. 분산 추적 및 근본 원인 분석에 없어서는 안 될 도구입니다.
AsyncLocalStorage
AsyncLocalStorage는 비동기 작업 전반에 걸쳐 컨텍스트를 관리하기 위해 도입된 핵심 Node.js API입니다. 비동기 호출 체인을 위한 스레드 로컬 저장소라고 생각하십시오. 비동기 컨텍스트에 로컬인 데이터를 저장할 수 있으며, 이 데이터는 await 호출, setTimeout, Promises 및 기타 비동기 경계를 통해 자동으로 전파됩니다. 즉, AsyncLocalStorage 인스턴스를 시작하고 값을 저장한 다음 해당 컨텍스트 내에서 시작된 후속 비동기 작업은 명시적으로 전달하지 않고도 동일한 값에 액세스할 수 있습니다.
AsyncLocalStorage 작동 방식
AsyncLocalStorage는 Node.js의 내부 비동기 후크를 활용하여 마법을 발휘합니다. asyncLocalStorage.run(store, callback, ...args)를 호출하면 새로운 비동기 컨텍스트가 생성됩니다. callback 내에서 시작된 모든 비동기 작업은 이 컨텍스트를 상속합니다. 즉, asyncLocalStorage.getStore()는 제공한 store 객체를 반환합니다. 해당 비동기 작업이 완료되면 컨텍스트는 자동으로 해제됩니다. 이 컨텍스트 전파는 모든 함수 시그니처를 수정하거나 컨텍스트 객체를 명시적으로 전달할 필요 없이 작동합니다.
AsyncLocalStorage를 사용하여 요청 ID 전파 구현
HTTP 요청 수명 주기 전체에 요청 ID를 전파하기 위해 AsyncLocalStorage를 사용하는 방법을 설명해 보겠습니다.
기본 설정
먼저 AsyncLocalStorage를 가져와야 합니다.
const { AsyncLocalStorage } = require('async_hooks'); // AsyncLocalStorage 인스턴스 생성 const asyncLocalStorage = new AsyncLocalStorage();
미들웨어 접근 방식
AsyncLocalStorage를 HTTP 요청과 통합하는 가장 일반적이고 효과적인 방법은 미들웨어를 통하는 것입니다(예: Express.js 애플리케이션).
미들웨어는 다음을 담당합니다.
- 요청 ID를 생성하거나 추출합니다.
asyncLocalStorage컨텍스트 내에서 나머지 요청 처리 로직을 실행하고 요청 ID를 저장합니다.
const express = require('express'); const { AsyncLocalStorage } = require('async_hooks'); const crypto = require('crypto'); // 고유 ID 생성을 위해 const app = express(); const asyncLocalStorage = new AsyncLocalStorage(); // 요청 ID를 할당하고 전파하는 미들웨어 app.use((req, res, next) => { // 각 수신 요청에 대한 고유 요청 ID 생성 const requestId = crypto.randomBytes(16).toString('hex'); // AsyncLocalStorage 컨텍스트에 requestId를 저장 asyncLocalStorage.run({ requestId: requestId }, () => { // 쉽게 액세스할 수 있도록 요청 객체에 연결 (선택 사항이지만 편리함) req.requestId = requestId; console.log(`[${requestId}] Incoming Request: ${req.method} ${req.url}`); next(); // 다음 미들웨어 또는 라우트 핸들러로 계속 진행 }); }); // 비동기 작업을 시뮬레이션하는 샘플 서비스 const someService = { doSomethingAsync: async () => { await new Promise(resolve => setTimeout(resolve, 100)); // 비동기 작업 시뮬레이션 const store = asyncLocalStorage.getStore(); console.log(`[${store?.requestId || 'N/A'}] Service doing something asynchronously.`); return 'Operation completed!'; }, // doSomethingAsync를 호출할 수 있는 다른 비동기 작업 doAnotherThing: async (data) => { const store = asyncLocalStorage.getStore(); console.log(`[${store?.requestId || 'N/A'}] Service received data: ${data}`); const result = await someService.doSomethingAsync(); return `Another thing done: ${result}`; } }; // 컨텍스트 액세스를 보여주는 라우트 핸들러 app.get('/data', async (req, res) => { // AsyncLocalStorage에서 스토어 데이터 (requestId 포함) 가져오기 const store = asyncLocalStorage.getStore(); const currentRequestId = store?.requestId || 'UNKNOWN'; console.log(`[${currentRequestId}] Handler received request.`); try { const serviceResult = await someService.doAnotherThing('some input data'); res.json({ message: `Data fetched successfully, request ID: ${currentRequestId}`, serviceResult }); } catch (error) { console.error(`[${currentRequestId}] Error processing request:`, error); res.status(500).json({ error: 'Internal server error' }); } }); const PORT = 3000; app.listen(PORT, () => { console.log(`Server listening on port ${PORT}`); });
코드 설명
asyncLocalStorage.run({ requestId: requestId }, () => { ... });: 이것이 솔루션의 핵심입니다. 각 수신 요청에 대해 새로운AsyncLocalStorage컨텍스트를 만듭니다.{ requestId: requestId }객체는 이 컨텍스트 내에서 사용할 수 있는 "스토어"입니다.()=> { ... }` 함수 내에서 시작된 모든 후속 비동기 작업은 이 스토어에 액세스할 수 있습니다.req.requestId = requestId;:AsyncLocalStorage는 ID를 안전하게 전파하지만,req객체에 넣으면AsyncLocalStorage.getStore()가 동기적으로 선호되기 전에 현재 미들웨어 스택 내에서 즉각적이고 동기적인 액세스를 제공하여 편리할 수 있습니다.asyncLocalStorage.getStore():someService.doSomethingAsync및app.get('/data')내부에서asyncLocalStorage.getStore()를 호출합니다. 이 메서드는 현재 비동기 컨텍스트에 대해asyncLocalStorage.run에 의해 설정된 스토어 객체({ requestId: ... })를 검색합니다.doSomethingAsync가await후에 호출되고 컨텍스트 전환이 포함될 수 있더라도AsyncLocalStorage는 올바른requestId가 검색되도록 보장합니다.- 로깅: 애플리케이션 전반에 걸쳐
console.log문에서requestId가 어떻게 사용되어 로그에 명확한 상관 관계를 제공하는지 확인하십시오.
애플리케이션 시나리오
- 요청 추적: 서비스 간의 로그를 연결하는 고유 ID를 제공하는 분산 추적 시스템의 핵심입니다.
- 컨텍스트별 로깅: 요청 처리 내의 모든 로그 메시지에 요청 ID를 자동으로 포함하여 로그를 디버깅에 훨씬 더 유용하게 만듭니다.
- 사용자 정보: 요청 ID 외에도 사용자 ID, 인증 토큰 또는 테넌트 ID를 저장하여 컨텍스트 인식 액세스 제어를 제공하거나 데이터를 개인화할 수 있습니다.
- 성능 모니터링:
AsyncLocalStorage에서 요청 시작 시간을 추적하여 애플리케이션의 다른 부분에 걸친 지연 시간을 계산할 수 있습니다.
결론
AsyncLocalStorage는 Node.js에서 비동기 컨텍스트를 관리하는 데 있어 획기적인 솔루션이며, 특히 요청 ID 전파와 같은 중요한 관심사에 그렇습니다. 비동기 경계를 가로질러 컨텍스트 데이터를 전달하는 안전하고 성능이 뛰어나며 침해적이지 않은 메커니즘을 제공함으로써 복잡한 Node.js 애플리케이션의 관찰 가능성, 디버깅 용이성 및 유지 관리성을 크게 향상시킵니다. AsyncLocalStorage를 채택하면 개발자가 명시적인 컨텍스트 전달의 번거로움에서 벗어나 비즈니스 로직에 집중하는 동시에 애플리케이션 동작에 대한 더 풍부하고 더 상관 관계 있는 통찰력을 얻을 수 있습니다. 복잡한 비동기 호출 체인을 통해 요청별 컨텍스트를 안전하게 전파하는 데 있어 확실한 솔루션입니다.

