Spring-Scheduler 사용 // Pass NullpointException : Authentication 해결(Security 연관)// Chatgpt 참조

2023. 7. 27. 11:32JAVA & Kotiln/Spring-Boot

Spring에서 제공하는 스케쥴러를 사용해

일정시간마다 DB를 조회해 데이터를 전송하는 기능을 구현하던 중

SpringSecurity에서 NullpointException 예외가 발생했다.

 

설정 코드는 아래와 같다.

스케쥴러를 사용하기 위해 '@EnableScheduling' 어노테이션을 추가 해 주었다.

 main 메소드가 있는곳에 어노테이션을 선언 해야 기본적으로 사용이 가능하다.
또는 @Configuration 어노테이션과 함께 @EnableScheduling을 선언해서 설정 파일을 만들어 
사용이 가능하다. 예시는 아래와 같다.

 

 

1. 기본적으로 SchedulingConfigurer interface를 상속받아 작성한다. 

2. override한 'configureTasks' 를 통해 taskRegistrar을 전달받고 

3. 여기에 설정한 스케쥴러 설정을 적용하고 (== .setTaskScheduler() )
4. 최종적으로 Bean등록되어 스케쥴러를 필요한 위치에서 사용할 수 있다.(=> @Scheduled 어노테이션 사용)


'taskScheduler()' 메소드의 경우는 개별 설정 영역이다. 
나는 간단하게 threadName과 poolsize 정도를 설정하려고 했다. (singleThread로 동작해야 해서 주석처리)

return에 있는 DelegatingSecurityContextTaskScheduler의 경우

org.springframework.security.scheduling 라이브러리에 포함 된 클래스다.

SpringSecurity와 같이 사용되는 프로젝트일 경우 해당 설정을 통해
스케쥴러를 Bean으로 등록하는걸 권고 하는 것을 아래 링크에서 확인하여 사용하였다. 

https://www.petrikainulainen.net/programming/spring-framework/spring-from-the-trenches-invoking-a-secured-method-from-a-scheduled-job/

 

@Scheduled Annotaion으로 메소드에 적용

cron 방식을 이용해 반복 단위시간을 설정하였고 

내부에는 필요한 기능을 작성하면 된다.

 

------------------------------------------

아래는 위와 같이 설정하기까지 chatgpt에 처음 질문을 이용한 내역이다. 

질문을 수정하면서 했는데 아래처럼 세세히 하니
꽤나 괜찮은 답변이 나와 참고에 도움이되었기에 

기록을 남긴다. ( 심심하면 읽어보는걸루 >< )

 

 

질문 : 자바 Springboot aplication 에서 scheduler 를 이용해서 method 를 실행할 때 SecurityContextHolder 에 Authentication 의 NullpointException이 발생하는데 이를 DelegatingSecurityContextScheduledExecutorService 를 통해서 해결할 수 있는 방법과 예시 코드를 작성 해 줘

 

GPT said : 

 

Spring Security의 `SecurityContextHolder`를 사용하여 스케줄러에서 실행되는 메서드에서 `Authentication` 객체에 접근할 때, `NullPointerException`이 발생하는 이유는 스케줄러는 보통 별도의 스레드에서 실행되기 때문입니다. Spring Security는 기본적으로 스레드 로컬(ThreadLocal)을 사용하여 `SecurityContext`를 관리하는데, 스레드가 변경되면 `SecurityContext`에 대한 정보가 유지되지 않기 때문에 발생합니다.

이를 해결하기 위해 `DelegatingSecurityContextScheduledExecutorService`를 사용할 수 있습니다. 이 클래스는 Spring Security `SecurityContext`를 스케줄러가 사용하는 스레드에 전달해주는 역할을 합니다.

아래는 `DelegatingSecurityContextScheduledExecutorService`를 사용하여 스케줄러에서 메서드를 실행하는 예시 코드입니다:

1. 먼저, 스프링 부트 애플리케이션에 Spring Security가 구성되어 있어야 합니다. 이를 위해 `SecurityConfig`와 같은 설정 파일이 필요합니다.

```java
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    // Spring Security 설정 내용 구성 (예: 사용자 인증, 권한 등)
}
```

2. 스케줄러에서 실행할 작업을 수행하는 서비스 클래스를 생성합니다. 이 서비스 클래스에서는 `@Async` 어노테이션을 사용하여 비동기적으로 메서드가 실행되도록 합니다. `@Async` 어노테이션을 사용하면 Spring이 내부적으로 비동기적인 방식으로 실행되며, `SecurityContext`가 유지됩니다.

```java
@Service
public class MyScheduledService {

    @Async
    public void myScheduledMethod() {
        // 여기에 스케줄러에서 실행할 작업을 구현합니다.
        // SecurityContextHolder.getContext().getAuthentication()을 사용해도 NullPointerExcetion이 발생하지 않습니다.
    }
}
```

3. 스케줄러를 설정하는 클래스를 생성합니다. `DelegatingSecurityContextScheduledExecutorService`를 사용하여 스케줄러를 구성합니다.

```java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.DefaultScheduledExecutorFactory;
import org.springframework.scheduling.concurrent.ScheduledExecutorFactoryBean;
import org.springframework.scheduling.concurrent.ScheduledExecutorTask;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.security.task.DelegatingSecurityContextScheduledExecutorService;

import java.util.concurrent.ScheduledExecutorService;

@Configuration
@EnableAsync
public class SchedulerConfig implements SchedulingConfigurer {

    private final MyScheduledService myScheduledService;

    @Autowired
    public SchedulerConfig(MyScheduledService myScheduledService) {
        this.myScheduledService = myScheduledService;
    }

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.setScheduler(scheduledExecutorService());
    }

    @Bean(destroyMethod = "destroy")
    public ScheduledExecutorService scheduledExecutorService() {
        ScheduledExecutorFactoryBean executorFactory = new ScheduledExecutorFactoryBean();
        executorFactory.setScheduledExecutorTasks(new ScheduledExecutorTask[] {});
        executorFactory.setThreadNamePrefix("my-scheduler-");
        executorFactory.setWaitForTasksToCompleteOnShutdown(true);
        executorFactory.setAwaitTerminationSeconds(60);
        executorFactory.setThreadFactory(new DefaultScheduledExecutorFactory("my-scheduler-thread-factory"));
        executorFactory.afterPropertiesSet();
        return new DelegatingSecurityContextScheduledExecutorService(executorFactory.getObject());
    }
}
```

4. 이제 `MyScheduledService` 클래스의 `myScheduledMethod()`가 스케줄러에서 실행될 때, `SecurityContextHolder.getContext().getAuthentication()`를 사용해도 `NullPointerException`이 발생하지 않을 것입니다. `DelegatingSecurityContextScheduledExecutorService`가 `SecurityContext`를 스케줄러가 사용하는 스레드로 전달하고 유지하기 때문입니다.

위의 예시 코드를 참고하여 스케줄러에서 `SecurityContextHolder`와 `Authentication` 객체에 접근하는 문제를 해결할 수 있습니다. 비슷한 방법으로 다른 스레드에서도 `SecurityContextHolder`를 사용할 수 있습니다.