본문으로 바로가기

[SPRING] 주문금액 집계 프로젝트 실습 part1

category SPRING/개발 TIP 2021. 8. 4. 21:56

JPA를 통한 스프링 배치 실습 프로젝트를 진행하면서 아래의 요구사항이 있다.

 

User 등급을 4개로 구분

  • 일반(NORMAL), 실버(SILVER), 골드(GOLD), VIP

User 등급 상향 조건은주문 금액 기준으로 등급 상향

  • 200,000원 이상인 경우 실버로 상향
  • 300,000원 이상인 경우 골드로 상향
  • 500,000원 이상인 경우 VIP로 상향
  • 등급 하향은 없음

총 2개의 Step으로 회원 등급 Job 생성

  • saveUserStep : User 데이터 저장
  • userLevelUpStep : User 등급 상향

JobExecutionListener.afterJob 메소드에서 “총 데이터 처리 {}건, 처리 시간 : {}millis” 와 같은 로그 출력

 

1. 요구사항에 필요한 User와 Level별 엔티티를 아래와 같이 만든다.

 

User.java

@Getter
@Entity
@NoArgsConstructor
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String username;

    @Enumerated(EnumType.STRING)
    private Level level = Level.NORMAL;

    private int totalAmount;

    private LocalDateTime updateDate;

    @Builder
    private User(String username, int totalAmount) {
        this.username = username;
        this.totalAmount = totalAmount;
    }

    public boolean availabelLevelUp() {
        return Level.availabelLevelUp(this.getLevel(), this.getTotalAmount());
    }

    public Level levelUp() {
        Level nextLevel = Level.getNextLevel(this.getTotalAmount());
        this.level = nextLevel;
        this.updateDate = LocalDateTime.now();

        return nextLevel;
    }
}

Level.java

public enum Level {
    VIP(500_000, null),
    GOLD(500_000, VIP),
    SILVER(300_000, GOLD),
    NORMAL(200_000, SILVER);

    private final int nextAmount;
    private final Level nextLevel;

    Level(int nextAmount, Level nextLevel) {
        this.nextAmount = nextAmount;
        this.nextLevel = nextLevel;
    }

    public static boolean availabelLevelUp(Level level, int totalAmount) {
        if(Objects.isNull(level)) {
            return false;
        }

        if(Objects.isNull(level.nextLevel)) {
            return false;
        }

        return totalAmount >= level.nextAmount;
    }

    static Level getNextLevel(int totalAmount) {
        if(totalAmount >= Level.VIP.nextAmount) {
            return VIP;
        }

        // GOLD.nextLevel == VIP
        if(totalAmount >= Level.GOLD.nextAmount) {
            return GOLD.nextLevel;
        }

        // SILVER.nextLevel == GOLD
        if(totalAmount >= Level.SILVER.nextAmount) {
            return SILVER.nextLevel;
        }

        // NORMAL.nextLevel == SILVER
        if(totalAmount >= Level.NORMAL.nextAmount) {
            return NORMAL.nextLevel;
        }

        return NORMAL;
    }
}

2. 100건당 등급 상향조건에 맞는 Tasklet 생성

public class SaveUserTasklet implements Tasklet {

    private final int ZERO = 0;
    private final int SIZE = 100;
    private final UserRepository userRepository;

    public SaveUserTasklet(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
        List<User> users = createUsers();

        Collections.shuffle(users);

        userRepository.saveAll(users);

        return RepeatStatus.FINISHED;
    }

    private List<User> createUsers() {
        List<User> users = new ArrayList<>();

        for (int i = ZERO; i < SIZE; i++) {
            users.add(User.builder()
                    .orders(Collections.singletonList(Orders.builder()
                            .amount(1_000)
                            .createdDate(LocalDate.of(2021,8,1))
                            .itemName("item" +i)
                            .build()))
                    .username("test username" + i)
                    .build());
        }

        // SILVER 등급
       for (int i = ZERO; i < SIZE; i++) {
            users.add(User.builder()
                    .orders(Collections.singletonList(Orders.builder()
                            .amount(200_000)
                            .createdDate(LocalDate.of(2021,9,2))
                            .itemName("item" +i)
                            .build()))
                    .username("test username" + i)
                    .build());
        }

        // GOLD 등급
        for (int i = ZERO; i < SIZE; i++) {
            users.add(User.builder()
                    .orders(Collections.singletonList(Orders.builder()
                            .amount(300_000)
                            .createdDate(LocalDate.of(2021,8,3))
                            .itemName("item" +i)
                            .build()))
                    .username("test username" + i)
                    .build());
        }

        // VIP 등급
        for (int i = ZERO; i < SIZE; i++) {
            users.add(User.builder()
                    .orders(Collections.singletonList(Orders.builder()
                            .amount(500_000)
                            .createdDate(LocalDate.of(2021,8,4))
                            .itemName("item" +i)
                            .build()))
                    .username("test username" + i)
                    .build());
        }

        return users;
    }
}

3. 배치 작업에 필요한 Configuration를 생성합니다.

@Configuration
@Slf4j
public class UserConfiguration {

    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;
    private final UserRepository userRepository;
    private final EntityManagerFactory entityManagerFactory;

    public UserConfiguration(JobBuilderFactory jobBuilderFactory, StepBuilderFactory stepBuilderFactory, UserRepository userRepository, EntityManagerFactory entityManagerFactory) {
        this.jobBuilderFactory = jobBuilderFactory;
        this.stepBuilderFactory = stepBuilderFactory;
        this.userRepository = userRepository;
        this.entityManagerFactory = entityManagerFactory;
    }

    @Bean
    public Job userJob() throws Exception {
        return this.jobBuilderFactory.get("userJob")
                .incrementer(new RunIdIncrementer())
                .start(this.saveUserStep())
                .next(this.userLevelUpStep())
                .listener(new LevelUpJobExecutionListener(userRepository))
                .build();
    }

    @Bean
    public Step saveUserStep() {
        return this.stepBuilderFactory.get("saveUserStep")
                .tasklet(new SaveUserTasklet(userRepository))
                .build();
    }

    @Bean
    public Step userLevelUpStep() throws Exception {
        return this.stepBuilderFactory.get("userLevelUpStep")
                .<User, User>chunk(100)
                .reader(itemReader())
                .processor(itemProcessor())
                .writer(itemWriter())
                .build();
    }

    private ItemWriter<? super User> itemWriter() {
        return users -> users.forEach(x -> {
                x.levelUp();
                userRepository.save(x);
        });
    }

    private ItemProcessor<? super User, ? extends User> itemProcessor() {
        return user -> {
            if (user.availableLeveUp()) {
                return user;
            }

            return null;
        };
    }

    private ItemReader<? extends User> itemReader() throws Exception {
        JpaPagingItemReader<User> itemReader = new JpaPagingItemReaderBuilder<User>()
                .queryString("select u from User u")
                .entityManagerFactory(entityManagerFactory)
                .pageSize(100)
                .name("userItemReader")
                .build();

        itemReader.afterPropertiesSet();

        return itemReader;
    }
}

 

1. Junit4 실습 위주의 단위 테스트 였으나 요즘 대세인 Junit5기반으로 셋팅을 해보았다.

 

* 메인메서드 셋팅은 아래와 같이 진행합니다.

 

* 등급별 자세한 로그를 확인하기 위해 아래와 같이 구현합니다.

@Slf4j
public class LevelUpJobExecutionListener implements JobExecutionListener {

    private final UserRepository userRepository;

    public LevelUpJobExecutionListener(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public void beforeJob(JobExecution jobExecution) {

    }

    @Override
    public void afterJob(JobExecution jobExecution) {
        Collection<User> users = userRepository.findAllByUpdatedDate(LocalDate.now());

        long time = jobExecution.getEndTime().getTime() - jobExecution.getStartTime().getTime();

        log.info("회원등급 업데이트 배치 프로그램");
        log.info("--------------------------");
        log.info("총 데이터 처리 {}건, 처리 시간 {}mills", users.size(), time);
    }
}

* 결과

 

느낀점 : step별 배치를 돌리면서 처리되는시간을 통해 좀 더 최적화 할수 있는 방법에 대해 고민하게 된다.