본문 바로가기

프로젝트/XRP 환전 서비스

[기능 구현] AsyncLocalStorage를 사용해서 Transaction 기능을 구현해보자

Transactional Decorator

Decorator

decorator는 함수를 반환하는 표현식으로

class, method, property에 @로 시작하는 decorator를 붙여서 코드를 수정하거나 추가해 주는 문법이다.

이를 통해 횡단 관심사를 분리해서 관점 지향 프로그래밍을 할 수 있다.

 

트랜잭션 로직이 필요한 메서드에 아래와 같은 decorator를 만들어서 적용할 수 있다.

descriptor.value는 원본 함수이고 트랜잭션 로직으로 감싸서 바꿔치기 한걸 볼 수 있다. 

export function Transactional() {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor,
  ) {
    const original = descriptor.value;
    descriptor.value = async function (...args: any[]) {
        return this.prisma.$transaction(async (tx) => {
            return await original.apply(this, args);
      });
    };
  };
}

 

AsyncLocalStroage

async_hooks은 비동기 리소스의 실행 흐름을 추적할 수 있는 API이다.

비동기 리소스마다 고유한 asyncId를 가지고 있고 아래와 같은 hook을 제공한다.

init은 비동기 리소스 생성 시 호출되고

before와 after는 callback 실행 전과 후에 호출되고

destory는 비동기 리소스 소멸 시 호출된다.

const hook = async_hooks.createHook({
  init(asyncId, type, triggerAsyncId) {
  },
  before(asyncId) {
  },
  after(asyncId) {
  },
  destroy(asyncId) {
  },
});

hook.enable();

 

AsyncLocalStorage는 async_hooks을 기반으로 비동기 context별로 데이터를 저장하고 관리할 수 있는 API이다.

이를 사용하면 함수마다 context를 매개변수로 전달하지 않고도 context를 참조할 수 있다.

AsyncLocalStorage의 run 메서드에서 데이터를 바인딩하면
callback 함수에서 getStore 메서드를 통해서 해당 데이터를 참조할 수 있다.

asyncLocalStorage.run(store, async () => {  
})

asyncLocalStorage.getStore();

 

트랜잭션 전파기능을 구현하면서

asyncLocalStorage를 조회해서 기존 트랜잭션이 있으면 해당 트랜잭션을 사용하고

없으면 새로운 트랜잭션을 생성하도록 했다. 

const asyncLocalStorage = new AsyncLocalStorage<Prisma.TransactionClient>();

export function Transactional() {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const original = descriptor.value;
    descriptor.value = async function (...args: any[]) {
      const existingTx = asyncLocalStorage.getStore();
      if (existingTx) {
        return await original.apply(this, args);
      }
      return this.prisma.$transaction(async (tx) => {
        return asyncLocalStorage.run(tx, async () => {
          return await original.apply(this, args);
        });
      });
    };
  };
}