Spring

Spring @Async 컨텍스트 정보 전파

기록만이살길 2022. 11. 27. 23:44
반응형

Spring @Async 컨텍스트 정보 전파

1. 질문(문제점):

Spring Boot 2.2 애플리케이션이 있습니다. 다음과 같은 서비스를 만들었습니다.

@Async
@PreAuthorize("hasAnyRole('ROLE_PBX')")
@PlanAuthorization(allowedPlans = {PlanType.BUSINESS, PlanType.ENTERPRISE})
public Future<AuditCdr> saveCDR(Cdr3CXDto cdrRecord) {
    log.debug("Current tenant {}", TenantContext.getCurrentTenantId());  

    return new AsyncResult<AuditCdr>(auditCdrRepository.save(cdr3CXMapper.cdr3CXDtoToAuditCdr(cdrRecord)));
}

이것은 내 @Async 구성입니다.

@Configuration
@EnableAsync
public class AsyncConfiguration implements AsyncConfigurer {

    @Override
    public Executor getAsyncExecutor() {
        SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(500);
        executor.setThreadNamePrefix("threadAsync");
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.initialize();
        return executor;
    }
}

사용 SecurityContextHolder.MODE_INHERITABLETHREADLOCAL하면 Security 컨텍스트가 @Async 메서드로 전달되는 것을 볼 수 있습니다. 다중 테넌트 애플리케이션에서 ThreadLocal을 사용하여 테넌트의 ID를 설정합니다.

public class TenantContext {
    public final static String TENANT_DEFAULT = "empty";
    private static final ThreadLocal<String> code = new ThreadLocal<>();

    public static void setCurrentTenantId(String code) {
        if (code != null)
            TenantContext.code.set(code);
    }

    public static String getCurrentTenantId() {
        String tenantId = code.get();
        if (StringUtils.isNotBlank(tenantId)) {
            return tenantId;
        }
        return TENANT_DEFAULT;
    }

    public static void clear() {
        code.remove();
    }

}

ThreadLocal은 스레드와 관련이 있기 때문에 @Async 메서드에서는 사용할 수 없습니다. 또한 내 사용자 지정 @PlanAuthorizationaop는 테넌트 계획의 확인을 수행하기 위해 필요합니다. 내 애플리케이션의 @Async 메서드에서 TenantContext를 설정하는 깔끔한 방법이 있습니까?

2. 해결방안:

이러한 경우에 대한 해결책은 다음과 같습니다.

  1. 사용자 지정 스레드 풀을 구성하여 실행 메서드를 재정의하여 스레드 로컬을 설정(또는 기본 컨텍스트에서 작업 실행)하고, 작업을 장식하고, 원래 작업 대신 실행을 위해 장식된 작업을 제출합니다.

  2. 구체적인 스레드 풀을 사용하도록 @Async 어노테이션에 지시

      @Bean("tenantExecutor")
      public Executor threadLocalAwareThreadPool() {
    
       final CustomizableThreadFactory threadNameAwareFactory =
          new CustomizableThreadFactory("threadAsync");
    
        final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 10,
          0L, TimeUnit.MILLISECONDS,
          new ArrayBlockingQueue<>(500), threadNameAwareFactory) {
    
          // override original method of thread pool
          @Override
          public void execute(Runnable originalTask) {
            final String tenantId = tenantThreadLocal.get(); // read data from current before passing the task to async thread 
    
    // decorate the actual task by creating new task (Runnable) where you first set up the thread local and then execute your actual task 
            super.execute(() -> {
              tenantThreadLocal.set(tenantId); // set data in actual async thread
              originalTask.run();
            });
          }
        };
    
    
        return threadPoolExecutor;
    
      }
    

이제 우리는 Spring에 사용자 지정 실행자를 사용하도록 지시합니다.

    @Async("tenantExecutor") 
    public Future<AuditCdr> saveCDR(Cdr3CXDto cdrRecord) {
        // your code....
    }
반응형