Tasklet 방식의 Step

이다은·2024년 9월 10일

스프링 배치

목록 보기
3/5

특정 그룹에 포함되는 사용자들에게 이용권을 일괄적으로 지급하려고 함

Tasklet 특징

  • Tasklet 인터페이스는 execute 단일 메서드를 제공
  • 데이터 처리과정이 tasklet안에서 한번에 이루어짐
  • Step 내에서 구성되고 실행되는 도메인 객체로 단일 task를 수행하기 위해 사용됨
  • Tasklet은 구현 클래스, 익명 클래스, 사용자 정의 클래스를 통해 실행할 수 있음
  • Tasklet.execute 메서드가 RepeatStaus.FINISHED를 반환할 때까지 트랜잭션 범위 내에서 반복적으로 실행되는 코드 블록을 만들 수 있음

ReapeatStatus

Tasklet execute 메소드의 return 타입

AddPassesJobConfig

@Configuration
@RequiredArgsConstructor
public class AddPassesJobConfig {

    private final AddPassesTasklet addPassesTasklet;
    private final PlatformTransactionManager transactionManager;
    private final JobRepository jobRepository;

    @Bean
    public Job addPassesJob(){
        return new JobBuilder("addPassesJob",jobRepository)
                .start(addPassesStep())
                .build();

    }
    @Bean
    public Step addPassesStep(){
        return new StepBuilder("addPassesStep",jobRepository)
                .tasklet(addPassesTasklet,transactionManager)
                .build();
    }
}

AddPassesTasklet

@Component
@RequiredArgsConstructor
@Slf4j
public class AddPassesTasklet implements Tasklet {

    private final PassRepository passRepository;
    private final BulkPassRepository bulkPassRepository;
    private final UserGroupMappingRepository userGroupMappingRepository;

    private final UserRepository userRepository;


    @Override
    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
        // 이용권 시작 일시 1일 전 user group 내 사용자에게 이용권을 추가해줌
        final LocalDateTime startedAt = LocalDateTime.now().minusDays(1);
        final List<BulkPassEntity> bulkPassEntities = bulkPassRepository.findByStatusAndStartedAtGreaterThan(BulkPassStatus.READY,startedAt);

        int count = 0;

        // 대량 이용권 정보를 돌면서 user group에 속한 userId를 조회하고 해당 userId로 이용권 추가
        for(BulkPassEntity bulkPassEntity : bulkPassEntities){
            final List<String> userIds = userGroupMappingRepository.findByUserGroupId(bulkPassEntity.getUserGroupId())
                    .stream()
                    .map(UserGroupMappingEntity::getUserId)
                    .collect(Collectors.toList());
            count += addPasses(bulkPassEntity,userIds);
            bulkPassEntity.addPassStatus();
        }

        log.info("AddPassesTasklet - execute: 이용권 {}건 추가 완료, startedAt={}",count,startedAt);
        return RepeatStatus.FINISHED;
    }

    private int addPasses(BulkPassEntity bulkPassEntity,List<String> userIds){
        List<UserEntity> users = userRepository.findAllById(userIds);
        Map<String, UserEntity> userMap = users.stream()
                .collect(Collectors.toMap(UserEntity::getUserId, Function.identity()));

        List<PassEntity> passEntities = userIds.stream()
                .map(userMap::get)
                .filter(Objects::nonNull)
                .map(
                        user -> PassEntity.of(
                        bulkPassEntity.getPackaze(),
                        user,
                        PassStatus.READY,
                        bulkPassEntity.getCount(),
                        bulkPassEntity.getStartedAt(),
                        bulkPassEntity.getEndedAt(),
                                PassType.GENERAL

                ))
                .collect(Collectors.toList());

        return passRepository.saveAll(passEntities).size();
    }
}

AddPasses 테스트

@Slf4j
@ExtendWith(MockitoExtension.class)
public class AddPassesTaskletTest {

  @Mock
  private StepContribution stepContribution;

  @Mock
  private ChunkContext chunkContext;

  @Mock
  private PassRepository passRepository;

  @Mock
  private PackageRepository packageRepository;

  @Mock
  private BulkPassRepository bulkPassRepository;

  @Mock
  private UserRepository userRepository;

  @Mock
  private UserGroupMappingRepository userGroupMappingRepository;


  //@InjectMocks 클래스의 인스턴스를 생성하고 @Mock 으로 생성된 객체를 주입
  @InjectMocks
  private AddPassesTasklet addPassesTasklet;


  @Test
  public void test_execute() throws Exception {

      final String userGroupId = "GROUP";
      final String userId = "A1000000";
      final Integer packageSeq = 1;
      final Integer count = 10;

      final LocalDateTime now = LocalDateTime.now();
      PackageEntity packageEntity = PackageEntity.of(
              1,
              "name",
              10
      );

      UserEntity userEntity = UserEntity.of("A1000000");

      

      final BulkPassEntity bulkPassEntity = BulkPassEntity.of(
              1,
              packageEntity,
              userGroupId,
              BulkPassStatus.READY,
              count,
              now,
              now.plusDays(60)
      );
     

      final UserGroupMappingEntity userGroupMapping = UserGroupMappingEntity.of(
              userGroupId,
              userId
      );

      when(bulkPassRepository.findByStatusAndStartedAtGreaterThan(eq(BulkPassStatus.READY),any()))
              .thenReturn(List.of(bulkPassEntity));
      when(userGroupMappingRepository.findByUserGroupId(eq("GROUP")))
              .thenReturn(List.of(userGroupMapping));
      when(userRepository.findAllById(List.of(userId)))
              .thenReturn(List.of(userEntity));
      when(passRepository.saveAll(any())).thenAnswer(invocation -> invocation.getArgument(0));

      RepeatStatus repeatStatus = addPassesTasklet.execute(stepContribution,chunkContext);
      Assertions.assertEquals(RepeatStatus.FINISHED,repeatStatus);

      ArgumentCaptor<List> passEntitiesCaptor = ArgumentCaptor.forClass(List.class);
      verify(passRepository,times(1)).saveAll(passEntitiesCaptor.capture());
      final List<PassEntity> passEntities = passEntitiesCaptor.getValue();

      assertEquals(1, passEntities.size());
      final PassEntity passEntity = passEntities.get(0);
      assertEquals(packageSeq, passEntity.getPackaze().getPackageSeq());
      assertEquals(userId, passEntity.getUser().getUserId());
      assertEquals(PassStatus.READY, passEntity.getStatus());
      assertEquals(count, passEntity.getRemainingCount();
  }
}
- `@ExtendWith(MockitoExtension.class)`  → 단위 테스트에 공통적으로 사용할 확장 기능을 선언, 개발자가 동작을 직접 제어할 수 있는 가짜(Mock) 객체를 지원하는 테스트 프레임워크
- `@InjectMocks`  → `@Mock` 또는 `@Spy`로 생성된 가짜 객체를 자동으로 주입시켜주는 객체
- `StepContribution`  → 아직 커밋되지 않은 현재 트랜잭션에 대한 정보
- `ChunkContext` → 실행 시점의 잡 상태 제공
- `ArgumentCaptor` → 메소드에 들어가는 인자값 검증
- `verify(passRepository,times(1)).saveAll(passEntitiesCaptor.capture())` → 1회만 실행되는지를 검증

0개의 댓글