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별 배치를 돌리면서 처리되는시간을 통해 좀 더 최적화 할수 있는 방법에 대해 고민하게 된다.
'SPRING > 개발 TIP' 카테고리의 다른 글
[SPRING] 주문금액 집계 프로젝트 실습 part3 (성능개선과 성능비교) (0) | 2021.08.07 |
---|---|
[SPRING] 주문금액 집계 프로젝트 실습 part2 (0) | 2021.08.04 |
[SPRING] 애노테이션 직접 만들기 (0) | 2021.04.27 |
[SPRING] SpringBootServletInitializer 란 무엇이고 왜 상속받고 있는가? (0) | 2021.04.19 |
[SPRING] SpringBoot로 완성하는 URL Shortener (1) (0) | 2021.04.16 |