[Spring Batch] Tasklet에서 왜 @BeforeStep과 @AfterStep이 동작하지 않을까?

2024. 8. 1. 23:00·Spring/Spring

 

1. Tasklet만으로는 beforeStep이나 afterStep을 트리거하지 못한다.

  • Tasklet을 구현하고 StepBuilder에서 tasklet() 메소드를 등록해주는 것만으로는 @BeforeStep이나 @AfterStep을 사용할 수 없다.
  • Tasklet에서 Step의 생명주기에 관여하고 싶다면 추가적인 작업이 필요하다는 말이다.
@Configuration
public class MemberJobConfig {

  @Bean("memberJob")
  public Job memberJob(
      JobRepository jobRepository, PlatformTransactionManager transactionManager) {
    return new JobBuilder("memberJob", jobRepository)
        .start(memberStep(jobRepository, transactionManager))
        .build();
  }

  @Bean("memberStep")
  public Step memberStep(
      JobRepository jobRepository, PlatformTransactionManager transactionManager) {
    return new StepBuilder("memberStep", jobRepository)
        .tasklet(sendingItemScheduleAlarmTasklet(), transactionManager)
        .build();
  }

  @Bean("memberTasklet")
  public Tasklet memberTasklet() {
    return new Tasklet() {
      @Override
      public RepeatStatus execute(
          StepContribution contribution, ChunkContext chunkContext) throws Exception {/** 로직 */}

      @BeforeStep
      public void beforeStep(StepExecution stepExecution) {/** 실행되지 않는다. */}

      @AfterStep
      public ExitStatus afterStep() {/** 실행되지 않는다. */}
    };
  }
}

 

 

2. Tasklet이 Step의 생명주기에 관여하는 방법

2-1. StepExecutionListener를 구현한다.

  • Tasklet과 함께 StepExecutionListener를 구현해주면 된다.
  • beforeStep() 메소드와 afterStep() 메소드를 오버라이드를 해서 작성하면 된다.
@Component
public class MemberTasklet implements Tasklet, StepExecutionListener {

  @Override
  public RepeatStatus execute(final StepContribution contribution, final ChunkContext chunkContext)
      throws Exception {
    return RepeatStatus.FINISHED;
  }

  @Override
  public void beforeStep(final StepExecution stepExecution) {
    // 실행
  }

  @Override
  public ExitStatus afterStep() {
    // 실행
  }
}
  • 아래와 같이 tasklet() 메소드를 사용해 등록만 해줘도 Spring Batch 내부적으로 Tasklet으로 등록한 StepExecutionListener 구현체의 beforeStep(), afterStep() 메소드를 실행해준다.
@Bean("memberStep")
public Step memberStep(
    final JobRepository jobRepository, final PlatformTransactionManager transactionManager) {
  return new StepBuilder("myStep", jobRepository)
      .tasklet(memberTasklet, transactionManager)
      .build();
}
  • 단, StepExecutionListener을 구현해도 @BeforeStep, @AfterStep이 트리거되지 않는다는 사실은 주의하자.
@Component
public class MemberTasklet implements Tasklet, StepExecutionListener {

  @Override
  public RepeatStatus execute(final StepContribution contribution, final ChunkContext chunkContext)
      throws Exception {/** 로직 */}

  @BeforeStep
  public void findMember(final StepExecution stepExecution) {/** 실행되지 않는다! */}

  @AfterStep
  public ExitStatus writeMember() {/** 실행되지 않는다! */}
}

 

2-2. Tasklet을 StepListener로 등록한다.

  • 아래와 같이 StepBuilder의 listener() 메소드를 사용해 Tasklet를 리스너로 등록해주면 @BeforeStep과 @AfterStep이 트리거된다.
@Bean("memberStep")
public Step memberStep(
    JobRepository jobRepository, PlatformTransactionManager transactionManager) {
  return new StepBuilder("memberStep", jobRepository)
      .tasklet(memberTasklet(), transactionManager)
      .listener(memberTasklet()) // 리스너로 등록해줘도 트리거된다!!
      .build();
}

 

 

2-3. 어떻게 동작하길래 리스너로 등록해도 사용이 가능한 걸까?

  • 위와 같이 StepBuilder에서 listener() 메소드를 호출하면 AbstractTaskletStepBuilder에서 super.listener(listener)를 호출해 리스너를 추가해준다.
public abstract class AbstractTaskletStepBuilder<B extends AbstractTaskletStepBuilder<B>> extends StepBuilderHelper<B> {
  ...
  
  public B listener(Object listener) {
	super.listener(listener);

	Set<Method> chunkListenerMethods = new HashSet<>();
	chunkListenerMethods.addAll(ReflectionUtils.findMethod(listener.getClass(), BeforeChunk.class));
	chunkListenerMethods.addAll(ReflectionUtils.findMethod(listener.getClass(), AfterChunk.class));
	chunkListenerMethods.addAll(ReflectionUtils.findMethod(listener.getClass(), AfterChunkError.class));

	if (chunkListenerMethods.size() > 0) {
		StepListenerFactoryBean factory = new StepListenerFactoryBean();
		factory.setDelegate(listener);
		this.listener((ChunkListener) factory.getObject());
	}

	return self();
  }
}
  • StepBuilderHelper에서는 @BeforeStep과 @AfterStep 어노테이션이 달려있는 메소드를 탐색해 StepListenerFactoryBean이라는 객체를 사용해 프록시로 감싸준다.
public abstract class StepBuilderHelper<B extends StepBuilderHelper<B>> {

  ...

  public B listener(Object listener) {
    Set<Method> stepExecutionListenerMethods = new HashSet<>();
    stepExecutionListenerMethods.addAll(ReflectionUtils.findMethod(listener.getClass(), BeforeStep.class));
    stepExecutionListenerMethods.addAll(ReflectionUtils.findMethod(listener.getClass(), AfterStep.class));

    if (stepExecutionListenerMethods.size() > 0) {
        StepListenerFactoryBean factory = new StepListenerFactoryBean();
        factory.setDelegate(listener);
        properties.addStepExecutionListener((StepExecutionListener) factory.getObject());
    }

    return self();
  }
  
  ...
}
  • getObject()를 실행할 때 nvokers에는 현재 리스너를 실행시키고 있는 컨텍스트에 대한 정보와 내가 @BeforeStep나 @AfterStep을 달았던 메소드 이름이 담긴다.
  • 나중에 StepListener의 beforeStep이나 afterStep이 실행될 때, 프록시를 통해 @BeforeStep나 @AfterStep가 달려있는 메소드를 실행해준다.
public abstract class AbstractListenerFactoryBean<T> implements FactoryBean<Object>, InitializingBean {

  @Override
  public Object getObject() {
    ...

    Set<Class<?>> listenerInterfaces = new HashSet<>();

    Map<String, Set<MethodInvoker>> invokerMap = new HashMap<>();
    boolean synthetic = false;
    for (Entry<String, String> entry : metaDataMap.entrySet()) {
        ...

      if (metaData.getAnnotation() != null) {
        invoker = getMethodInvokerByAnnotation(metaData.getAnnotation(), delegate, metaData.getParamTypes());
        if (invoker != null) {
            invokers.add(invoker);
            synthetic = true;
        }
      }

      if (!invokers.isEmpty()) {
        invokerMap.put(metaData.getMethodName(), invokers);
        listenerInterfaces.add(metaData.getListenerInterface());
      }

    }

    ...
    proxyFactory.setInterfaces(listenerInterfaces.toArray(a));
    proxyFactory.addAdvisor(new DefaultPointcutAdvisor(new MethodInvokerMethodInterceptor(invokerMap, ordered)));
    return proxyFactory.getProxy();
  }
    
}
  • AbstractStep에서 CompositeStepExecutionListener를 사용해 리스너를 등록해준다.
public class CompositeStepExecutionListener implements StepExecutionListener {

	private final OrderedComposite<StepExecutionListener> list = new OrderedComposite<>();

	public void register(StepExecutionListener stepExecutionListener) {
		list.add(stepExecutionListener);
	}

    protected StepExecutionListener getCompositeListener() {
      return stepExecutionListener;
    }
	
	...	
}
  • 이후 Step이 실행될 때, AbstractStep에서 CompositeStepExecutionListener을 가져와 beforeStep과 afterStep을 실행한다.
public abstract class AbstractStep implements Step, InitializingBean, BeanNameAware {

	@Override
	public final void execute(StepExecution stepExecution)
			throws JobInterruptedException, UnexpectedJobExecutionException {
		...
		
		getCompositeListener().beforeStep(stepExecution);
		
		...
		
		exitStatus = exitStatus.and(getCompositeListener().afterStep(stepExecution));
		
		...
	}
}
  • CompositeStepExecutionListener는 Step에 등록된 모든 StepExecutionListener의 beforeStep와 afterStep을 실행하게 된다. 위에서 설명했다시피 프록시 객체를 통해 @BeforeStep과 @AfterStep이 트리거되어 Tasklet에 구현한 메소드가 실행되는 것이다.

 

ItemReader, ItemWriter, ItemProcessor

  • ItemReader, ItemProcessor, ItemWriter는 따로 리스너로 등록하지 않아도 @BeforeStep나 @AfterStep을 사용할 수 있다. 
@Bean("memberStep")
public Step memberStep(
    JobRepository jobRepository, PlatformTransactionManager transactionManager) {
  return new StepBuilder("memberStep", jobRepository)
      .<Member, Member>chunk(chunkSize, transactionManager)
      .reader(memberReader())
      .writer(memberWriter())
      .build();
}
  • StepBuilder(정확히는 SimpleStepBuilder)에서 build()를 실행할 때 registerAsStreamsAndListeners() 메소드를 사용해 ItemReader, ItemProcessor, ItemWriter를 리스너로 자동 등록하기 때문이다.
@Override
public TaskletStep build() {

	registerStepListenerAsItemListener();
	registerAsStreamsAndListeners(reader, processor, writer);
	return super.build();
}

 

 

'Spring/Spring' 카테고리의 다른 글
  • HikariCP를 이해하면 풀 사이즈 설정이 보인다
  • [Spring Batch] 서로 다른 Step끼리 데이터 공유하기
  • [Spring] 스프링 이벤트로 유연한 설계 만들기
  • [Spring] RestClient URI Encoding 문제 (feat. 퍼센트 인코딩)
gakko
gakko
좌충우돌 개발기
  • gakko
    MYVELOP 마이벨롭
    gakko
  • 전체
    오늘
    어제
    • 분류 전체보기 (203)
      • Spring (23)
        • Spring (10)
        • Spring Boot (7)
        • Spring Security (1)
        • Hibernate (4)
      • Test (3)
      • 끄적끄적 (6)
      • 활동 (35)
        • 부스트캠프 (23)
        • 동아리 (3)
        • 컨퍼런스 (3)
        • 글또 (5)
        • 오픈소스 컨트리뷰션 (1)
      • 디자인패턴 (0)
      • Git & GitHub (22)
        • Git (13)
        • Github Actions (1)
        • 오류해결 (5)
        • 기타(마크다운 등) (3)
      • 리눅스 (6)
        • 기초 (6)
        • 리눅스 서버 구축하기 (0)
      • Infra (2)
        • Docker (1)
        • Elastic Search (0)
        • Jenkins (1)
        • AWS (1)
      • MySQL (7)
        • 기초 (6)
        • Real MySQL (1)
      • 후기 (3)
        • Udemy 리뷰 (3)
      • CS (26)
        • 웹 기본지식 (0)
        • 자료구조 (13)
        • 운영체제 OS (12)
        • 데이터베이스 (1)
        • 시스템 프로그래밍 (0)
        • 기타 (0)
      • Tools (1)
        • 이클립스 (1)
        • IntelliJ (0)
      • 프로젝트 (1)
        • 모여모여(부스트캠프) (1)
      • JAVA (32)
        • Maven (6)
        • 오류해결 (11)
        • 자바 클래스&메소드 (1)
        • JSP & Servlet (12)
      • Javascript (5)
        • 기초 (3)
        • React (2)
      • Python (28)
        • 파이썬 함수 (9)
        • 알고리즘 문제풀이 (16)
        • 데이터 사이언스 (2)
        • 웹 크롤링 (1)
      • 단순정보전달글 저장소 (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • 우진님
  • 공지사항

  • 인기 글

  • 태그

    파이썬
    부스트캠프 멤버십
    os
    자바스크립트
    Git
    java
    부스트캠프 7기
    오류해결
    알고리즘
    MySQL
    자바
    부스트캠프
    웹개발
    Spring
    jsp
    Python
    GitHub
    스프링부트
    운영체제
    스프링
  • 최근 댓글

  • hELLO· Designed By정상우.v4.10.0
gakko
[Spring Batch] Tasklet에서 왜 @BeforeStep과 @AfterStep이 동작하지 않을까?
상단으로

티스토리툴바