본문으로 건너뛰기
개발 머꼬
개발 노트Node.js
hohyeon.dev23

요청 ID를 로그에 남기려고 모든 함수에 인자를 하나씩 더하던 것을 멈춘 방법

  • #Engineering Note
  • #Node.js

문제 발생

장애를 추적하려면 로그마다 요청 ID가 필요했습니다. 그래서 함수 시그니처가 이렇게 번져갔습니다.

async function createPost(data, requestId) {
  await validate(data, requestId);
  await save(data, requestId);       // 그 아래로 계속…
}

도메인 로직과 상관없는 인자가 모든 계층을 오염시켰고, 한 곳만 빠뜨려도 그 아래 로그에서 ID가 사라졌습니다.

원인 분석

컨텍스트 전파는 인자 전달과 다른 문제입니다. 요청 ID는 비즈니스 로직의 입력이 아니라 실행 환경에 대한 정보인데, 그걸 표현할 자리가 없어서 인자로 흘려보내고 있었습니다.

Node.js는 이 목적의 API를 제공합니다. 문서 설명대로 AsyncLocalStorage비동기 연산을 거쳐 상태를 자동으로 전파하며, 명시적인 파라미터 전달을 요구하지 않습니다. 콜백 체인과 프로미스 연속 실행 전반에 걸쳐 격리된 저장소를 유지합니다.

API는 두 개면 충분합니다 — run(store, callback)은 그 콜백 안의 모든 비동기 연산이 접근할 수 있는 저장소 값을 두고 실행하고, getStore()는 현재 저장소 값을 반환합니다(run() 밖에서는 undefined).

해결 방안

  1. 요청 경계에서 한 번 감쌉니다.
import { AsyncLocalStorage } from "node:async_hooks";
export const requestContext = new AsyncLocalStorage();

http.createServer((req, res) => {
  requestContext.run({ requestId: crypto.randomUUID() }, () => handle(req, res));
});
  1. 로거가 알아서 꺼내 쓰게 합니다. 호출부는 아무것도 넘기지 않습니다.
function log(message) {
  const { requestId } = requestContext.getStore() ?? {};
  console.log(JSON.stringify({ requestId, message }));
}
  1. 저장소에 담을 것을 좁힙니다. 요청 ID, 사용자 ID, 트레이스 ID 정도입니다. 여기에 도메인 데이터를 담기 시작하면 어디서 값이 오는지 알 수 없는 전역 변수가 됩니다.

  2. getStore()undefined인 경우를 처리합니다. 배치 스크립트나 부팅 코드처럼 run() 밖에서 호출되는 경로가 반드시 있습니다.

  3. 프레임워크가 이미 제공하는지 확인합니다. Next.js처럼 요청 컨텍스트를 다루는 계층이 있으면 그 API를 먼저 씁니다 — 같은 일을 두 겹으로 하면 디버깅이 어려워집니다.

  4. 테스트에서 격리를 확인합니다. 두 요청이 동시에 진행될 때 서로의 컨텍스트가 섞이지 않는지 보는 것이 이 도구의 유일한 핵심 성질입니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.