본문 바로가기
Programming/Node.js

[Node.js] AsyncLocalStorage

by SpiralMoon 2025. 3. 24.
반응형

AsyncLocalStorage

비동기 작업의 context를 유지할 수 있도록 해주는 AsyncLocalStorage API에 대해 알아보자.

작성 환경

Node.js 16.4 (stable)


AsyncLocalStorage란?

AsyncLocalStorage(이하 als)는 Node.js 16.4에서 정식 추가된 API로 비동기 호출 간에 상태를 저장하고 공유할 수 있도록 도와주는 기능이다.

(프론트엔드 개발에 쓰이는 localStorage와 관련 없음)

 

Java에서는 각 Thread마다 독립적인 context를 유지할 수 있도록 ThreadLocal이라는 기능을 제공하지만, Node.js는 Single Thread Event Loop 기반의 비동기 실행 모델을 사용하기 때문에 여러 실행 흐름 간의 context를 관리하는 방식이 달라져서 ThreadLocal과 같은 기능을 직접 구현하기 어려운 점이 있다. 이와 같은 문제를 해결하기 위해, Node.js에서는 als를 사용하여 비동기적인 실행 흐름 내에서 context를 안전하게 관리할 수 있다.

 

als는 context와 데이터를 함께 유지하여 비동기 함수들이 공통된 데이터를 공유하도록 한다.

 

import { AsyncLocalStorage } from 'async_hooks';

const als = new AsyncLocalStorage<Map<string, any>>();

function runWithContext(store: Map<string, any>, callback: () => unknown) {
  als.run(store, callback);
}

function setValue(key: string, value: any) {
  const store = als.getStore();
  
  if (store) {
    store.set(key, value);
  }
}

function getValue(key: string) {
  const store = als.getStore();
  return store?.get(key);
}

 

run() 함수는 새로운 비동기 context를 생성하고, 이 context는 현재 실행 중인 call stack에서 실행된 모든 비동기 작업에 전파된다.


AsyncLocalStorage를 사용하지 않고 데이터를 유지하는 방법?

als의 필요성을 알기 위해서는 우선 als를 사용하지 않고 데이터를 유지할 때 어떤 문제가 발생하는지 알아야 한다.

계층마다 매개변수를 통해 전파하는 방법

부모 함수에서 자식 함수까지 데이터를 공유할 수 있는 제일 쉬운 방법은 매개변수로 전달하는 것이다.

 

function f1() {
  const value = 'your data';
  
  f2(value);
}

function f2(value: any) {
  f3(value);
}

function f3(value: any) {
  console.log(value);
}

 

이 방법은 두 가지 단점이 존재한다.

  • 매개변수를 매번 정의하고 전달해야하므로 Depth가 깊어지면 수정하기 어려워진다. (유지보수 난이도 증가, 생산성 감소)
  • 하위 계층에서 value를 수정하는 작업이 발생 했을 때, 수정된 value를 상위 계층의 call stack에서 알아야하는 경우 value가 참조 타입이 되거나 리턴값이 되어야 한다.

모든 계층에서 접근할 수 있는 scope에 변수를 선언하는 방법

부모 함수와 자식 함수에서 모두 접근 가능한 scope에 변수를 두고 사용하는 것이다.

 

 

let value: any;

function f1() {
  value = 'your data';
  f2();
}

function f2() {
  console.log(value);
}

 

이 방법 역시 단점이 존재한다.

  • 전역변수로 선언했기 때문에 다른 곳에서 사용하게 될 가능성이 생긴다. (scope 오염)
  • 비동기 상황에서 전역변수 핸들링에 대한 실행 순서를 보장할 수 없다.

AsyncLocalStorage로 context 관리하기

als를 활용하면 특정 실행 흐름에서 context를 유지할 수 있기 때문에 앞에서 말했던 문제점들이 해결된다.

 

import { AsyncLocalStorage } from 'async_hooks'

const als = new AsyncLocalStorage<number>();

function f1() {
  als.run(random(), f2);
}

function f2() {
  const value = als.getStore();
  setTimeout(() => console.log(value), Math.random() * 10);
}

function random() {
  return 랜덤숫자;
}

f1(); // 43242
f1(); // 32131
f1(); // 12335

 

als 인스턴스를 생성하고 run() 함수를 통해 데이터와 callback 함수를 전달한다. run() 함수로 전달한 callback 내부에서는 동일한 context라는 것이 보장되며 getStore() 함수를 통해 데이터에 접근할 수 있다.


중첩 AsyncLocalStorage

als를 중첩해서 사용할 수도 있다. 하나의 context에서 실행 중인 코드가 새로운 context를 생성하여 또 다른 데이터를 저장할 수 있다.

 

const outerStore = new AsyncLocalStorage<Map<string, any>>();
const innerStore = new AsyncLocalStorage<Map<string, any>>();

outerStore.run(new Map(), () => {
  outerStore.getStore()?.set('outerKey', 'Outer Context');
  
  console.log('Outer Context:', outerStore.getStore()?.get('outerKey'));
  console.log('Inner Context Before:', innerStore.getStore()?.get('innerKey'));

  innerStore.run(new Map(), () => {
    innerStore.getStore()?.set('innerKey', 'Inner Context');
    console.log('Inner Context:', innerStore.getStore()?.get('innerKey'));
    console.log('Outer Context Inside Inner:', outerStore.getStore()?.get('outerKey'));
  });

  console.log('Inner Context After:', innerStore.getStore()?.get('innerKey'));
});


-----
출력 결과

Outer Context: Outer Context
Inner Context Before: undefined
Inner Context: Inner Context
Outer Context Inside Inner: Outer Context
Inner Context After: undefined

참조

 

Asynchronous context tracking | Node.js v23.10.0 Documentation

Asynchronous context tracking# Source Code: lib/async_hooks.js Introduction# These classes are used to associate state and propagate it throughout callbacks and promise chains. They allow storing data throughout the lifetime of a web request or any other a

nodejs.org

 

반응형

댓글