ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring Batch 살짝 알아보기-3
    개발/Spring 2022. 2. 13. 11:57

    블로그 글을 참고하며 휴면계정 배치처리 실습을 해보았다. 기본적인 흐름을 블로그를 참고하였다. 1년이 지난 회원을 휴면계정으로 돌리는 배치 실습을 계획하였다. DB는 간단하게 사용하고 싶어서 h2를 사용하였다. (내 실습레포) 스스로 실습해보며 마주했던 이슈들과 해결했던 과정 위주로 정리해보았다.

    실습 과정 중 Issues

    h2를 사용했기 때문에 기존 데이터를 셋팅해주어야 제대로 배치가 돌아가는 것인지 눈으로 확인해 볼 수 있다고 생각했다. 따라서 초기 데이터를 셋팅해주었다. 100명의 유저 데이터를 넣는데 처음 30명은 1년전 updatedTime을 넣어주고, 70명은 1년이 넘지 않도록 설정했다. 처음에 어떻게 데이터를 셋팅해줄까 고민하다가 for문을 돌려 User엔티티를 만들어 데이터로더를 사용해서 넣어주려고 했다.

    이슈1) 데이터 로더가 Configuration 보다 늦게 돈다. 따라서 Batch 작업이 돌고나서 DataLoader가 동작하여 데이터 셋팅에 아무런 의미가 없었다. 따라서 Batch Job에 Step을 여러개 넣을 수 있다는 점과 기본 실습에서 했던 csv 파일을 읽어 저장하는 예제에서 착안하여 파일을 이용하여 데이터를 셋팅하고 휴면계정 Step을 돌리는 것으로 계획하였다.

    100명의 유저를 csv 파일로 만든다음 이것을 저장하는 Step을 먼저 돌리고 휴면계정 배치가 돌아가게 계획하였다.

        @Bean
        public Job userJob(JobBuilderFactory jobBuilderFactory) {
            return jobBuilderFactory.get("userJob")
                    .preventRestart()
                    .start(dataSettingStep())
                    .next(inactiveUserStep())
                    .build();
        }

     

    이슈2) csv에서 날짜를 LocalDateTime으로 읽어들이지 못하는 이슈가 발생하였다. 자꾸 해당 setter, getter가 없고 Converter 예외가 발생했다. Spring batch에서는 기본 Converter에서 인식할 수 있는 타입이 4가지(String, Date, Double, Long) 정도라고 한다. (스프링 JobParameter Type

        @Bean
        public ConversionService dateConversionService() {
            DefaultConversionService testConversionService = new DefaultConversionService();
            DefaultConversionService.addDefaultConverters(testConversionService);
            testConversionService.addConverter(new Converter<String, LocalDateTime>() {
                @Override
                public LocalDateTime convert(String text) {
                    return LocalDateTime.parse(text, DateTimeFormatter.ISO_DATE_TIME);
                }
            });
    
            return testConversionService;
        }
        
        @Bean
        public FlatFileItemReader<FileUserDto> userFlatFileItemReader() {
            return new FlatFileItemReaderBuilder<FileUserDto>()
                    .name("userItemReader")
                    .resource(new ClassPathResource("sample-data.csv"))
                    .delimited()
                    .names(new String[]{"id", "name", "status", "updatedTime"})
                    .fieldSetMapper(new BeanWrapperFieldSetMapper<FileUserDto>() {{
                        setConversionService(dateConversionService()); // 추가
                        setTargetType(FileUserDto.class);
                    }})
                    .build();
        }

    위의 코드처럼 ConsersionService를 정의해서 reader에서 setter를 사용하여 추가해 주었다.

    그렇다면 Date? LocalDateTime?의 차이는 무엇일까? Date는 예전부터 사용되던 클래스이고 지금은 지양한다고 한다. LocalDateTime은 자바 8부터 추가되었다. Date는 불변이 아니고, 상수사용, 불규칙적이고 헷갈리는 월, 요일 상수 등이 있다고 한다. (참고한 블로그) 자세한 내용은 뭐가 많은 것 같고, Date는 과거 흔적이며 지양되니 LocalDateTime을 사용하라고 한다.

    이슈3) 모듈 이름 변경에 따른 path 인식 예외 발생하였다. 깃헙레포에 올리기 전에 기존에 있던 모듈과 디렉토리의 이름을 변경했더니 gradle 에서 path를 인식하지 못하는 이슈가 발생하였다. 아마 기존의 빌드되어 저장되어 있는 모듈과 이름의 충돌이 발생해서 있는 듯 하다. 사이드바에 gradle에 떠있는 모듈을 삭제하고 다시 변경된 이름으로 추가해주니 해당 문제가 해결되었다.

    이슈4) StepScope의 사용에 따른 실행시점이 달라지는 이슈를 발견하였다.  배치 작업 예시에 StepScope가 있어서 없어도 h2가 잘 작동하여 주석처리하고 동작했더니 Job, Step이 실행되기 전에 해당 내용들이 빈으로 등록되며 동작했다.

        @Bean
    //    @StepScope
        public ItemWriter<User> inactiveUserWriter() {
            return ((List<? extends User> users) -> userRepository.saveAll(users));
        }
    
        @Bean
    //    @StepScope
        public ItemProcessor<User, User> inactiveUserProcessor() {
            return user -> user.toInactive();
        }
    
        @Bean
    //    @StepScope
        public QueueItemReader<User> inactiveUserReader() {
            List<User> oldUsers = userRepository.findByUpdatedDateBeforeAndStatusEquals(
                    LocalDateTime.now().minusYears(1),
                    UserStatus.ACTIVE.name());
    
            log.info("old User Count: " + oldUsers.size());
            return new QueueItemReader<>(oldUsers);
        }

    StepScope 주석처리

    위의 결과 사진을 보면 oldUser가 0으로 나오고 그 이후에 job, step이 실행되는 것을 볼 수 있다. 위의 주석되어 있는 StepScope를 풀어주면 아래 사진처럼 dataSettingStep이 돌고 나서 inactiveUserStep이 동작하면서 원하는 대로 oldUser 의 수를 세어오는 것을 볼 수 있다.

    StepScope 적용

    기존 정리할 때 알아봤던 것 처럼 지연 동작하게 되는 것을 짐작할 수 있다.

    이슈5) enum 타입으로 저장시 해시값이 들어가는 이슈가 발생하였다. UserStatus를 INACTIVE, ACTIVE 타입으로 설정하였는데 h2 디비에는 해시값으로 들어가게 되었다. Converting 과정의 문제인가 하여 FileUserDto를 만들어 변환하는 작업을 추가로 해주었는데도 해당 문제는 해결되지 않았다. 엔티티에서 Enumerated(EnumType.ORDINAL)로 설정하면 변환시 문제가 발생하였다. 해당 내용이 제대로된 이넘값으로 들어가지 않았기 때문에 배치작업에 문제가 되었다. 배치에서도 enum을 지원해주고 h2에서도 문제가 되지 않는 듯 한데 어디부분에서 문제가 되어 해시값으로 들어가는지 알아내지 못했다.

    해결하는 방법을 찾지 못했기에 엔티티에서 String 타입으로 변환하여 해결하였다. 근본적인 enum 타입에 대한 문제를 해결하지는 못했지만 원하는 대로 배치는 동작하는 것은 볼 수 있었다.

    이슈6) Dto를 사용하여 entity의 불필요한 setter 제거하였다. 파일을 읽어들일때 ItemProcessor<User>로 사용하면 파일을 읽어들이는 부분에서 setter와 getter가 필요하다는 경고메세지가 나온다. ItemProcessor를 <dto,entity> 방식으로 변환해주는 프로세서를 중간에 재정의하여 넣어주면 엔티티에는 불필요하게 setter를 넣을 필요가 없고 dto 단에서 변환에 필요한 setter를 처리해줄 수 있다.

    public class UserItemProcessor implements ItemProcessor<FileUserDto, User> {
    
        @Override
        public User process(FileUserDto item) throws Exception {
    
            return new User(item.getId(), item.getName(), item.getStatus(), item.getUpdatedTime());
        }
    }

     

    정리

    따라하기 예제라고 생각하고 쉽게 끝날 줄 알았는데.. 아니었다. 작은 이슈들이 계속 나왔다. 설에 공부했던 내용이지만 이제와서야 실습을 마무리 해본다. enum 부분의 디버깅은 못해봤지만, 작은 실습을 해보며 마주했던 작은 이슈들을 정리해보았다. 앞으로 해결해나가는 과정을 기록하는 것이 좀 더 의미있다고 생각하기 때문에 이슈 발생 및 해결과정 위주로 글을 작성하고자 노력해보려고 한다.

    참고

    '개발 > Spring' 카테고리의 다른 글

    Spring Batch 살짝 알아보기-2  (2) 2022.02.07
    Spring Batch 살짝 알아보기-1  (0) 2022.02.07

    댓글

Designed by Tistory.