시나리오
- 고객이 유료 API를 사용
- 서비스팀은 유료 API 사용 이력 남김
- 유료 API 사용 이력을 파일로 정산팀에게 전달 (임시로 랜덤 데이터 생성) // 고객번호, url, 상태, 생성일
- 정산팀은 1일 단위로 정산
- 매주 금요일 1주일치 1일 정산을 집계해서 DB에 저장 후, 고객사에 이메일 보냄 // 고객번호, 서비스번호, 횟수, 요금
해야할 것
- API호출 이력 파일 만드는 배치
- 일 단위 정산 배치
- 주 단위 정산 배치
먼저 결제 도메인을 만들었다.
ApiOrder.java
@Data
@NoArgsConstructor
public class ApiOrder {
public String id;
public Long customerId;
private String url;
private State state;
private String createdAt;
public enum State{
SUCCESS, FAIL
}
public ApiOrder(String id, Long customerId, String url, State state, String createdAt) {
this.id = id;
this.customerId = customerId;
this.url = url;
this.state = state;
this.createdAt = createdAt;
}
}
그리고 프로세서에서 랜덤으로 어떤 결제서비스를 이용했는지 리스트로 뽑아오기 위해 enum을 만들어줬다.
ServicePolicy.java
@Getter
public enum ServicePolicy {
A(1L, "/payment/services/a", 10),
B(2L, "/payment/services/b", 10),
C(3L, "/payment/services/c", 10),
D(4L, "/payment/services/d", 15),
E(5L, "/payment/services/e", 15),
F(6L, "/payment/services/f",10),
G(7L, "/payment/services/g",10),
H(8L, "/payment/services/h",10),
I(9L, "/payment/services/i",10),
J(10L, "/payment/services/j",10),
K(11L, "/payment/services/k",10),
L(12L, "/payment/services/l",12),
M(13L, "/payment/services/m",12),
N(14L, "/payment/services/n",12),
O(15L, "/payment/services/o",10),
P(16L, "/payment/services/p",10),
Q(17L, "/payment/services/q",10),
R(18L, "/payment/services/r",10),
S(19L, "/payment/services/s",10),
T(20L, "/payment/services/t",10),
U(21L, "/payment/services/u",10),
V(22L, "/payment/services/v",10),
W(23L, "/payment/services/w",19),
X(24L, "/payment/services/x",19),
Y(25L, "/payment/services/y",19),
Z(26L, "/payment/services/z",19);
private final Long id;
private final String url;
private final Integer fee;
ServicePolicy(Long id, String url, Integer fee) {
this.id = id;
this.url = url;
this.fee = fee;
}
}
이 데이터들을 이용해서 Spring Batch를 활용해본다.
먼저 Batch 설정 파일이다.
incrementer로 Batch가 계속 실행되게 해준다.
그리고 Step에서는 Chunk기반으로 데이터를 처리해주는데, 입력으로 Boolean타입을 받고, 출력으로 내가 만든 ApiOrder타입을 받는다.
writer(apiOrderGenerateWriter(null)) 부분은 NPE가 발생하지 않을까 싶지만, 밑에 ItemReader에서 어노테이션으로 @StepScope라고 설정을 해놨으므로, 지연로딩이 되며 올바른 값이 인자로 전달된다.
ApiOrderGenerateJobConfiguration.java
@Configuration
@RequiredArgsConstructor
public class ApiOrderGenerateJobConfiguration {
private final JobRepository jobRepository;
private final PlatformTransactionManager platformTransactionManager;
@Bean
public Job apiOrderGenerateJob(Step step){
return new JobBuilder("apiOrderGenerateJob", jobRepository)
.start(step)
.incrementer(new RunIdIncrementer())
.build();
}
@Bean
public Step apiOrderGenerateStep(
ApiOrderGenerateReader apiOrderGenerateReader,
ApiOrderGenerateProcessor apiOrderGenerateProcessor
){
return new StepBuilder("apiOrderGenerateStep",jobRepository)
.<Boolean, ApiOrder>chunk(1000, platformTransactionManager)
.reader(apiOrderGenerateReader)
.processor(apiOrderGenerateProcessor)
.writer(apiOrderGenerateWriter(null))
.build();
}
@Bean
@StepScope //lazy로딩이 일어나 null이 인자로 들어가는것이 아니라, 맞는 데이터가 들어감
public FlatFileItemWriter<ApiOrder> apiOrderGenerateWriter(
@Value("#{jobParameters['targetDate']}") String targetDate
){
String fileName = targetDate + "_api_orders_csv";
return new FlatFileItemWriterBuilder<ApiOrder>()
.name("apiOrderGenerateWriter")
.resource(new PathResource("/FastCampus/spring-payment/spring-payment/src/main/resources/datas/" + fileName))
.delimited() //기본값으로 두면 쉼표로구분
.names("id", "customerId", "url", "state", "createdAt")
.headerCallback(writer -> writer.write("id, customerId, url, state, createdAt"))
.build();
}
}
마찬가지로 @StepScope 어노테이션을 사용해서 @Value로 인자를 Lazy Fetch로 받아온다.
incrementAndGet 메서드로 current 값을 +1 해주고 얻어온 후, 나의 경우 파라미터로 totalCount를 100으로 설정해줬으므로, 100번 반복해서 읽어온다. 단순히 Boolean타입을 읽어오기 때문에 로직은 간단하다.
ApiOrderGenerateReader.java
@Component
@StepScope
public class ApiOrderGenerateReader implements ItemReader<Boolean> {
private Long totalCount;
private AtomicLong current;
public ApiOrderGenerateReader(
@Value("#{jobParameters['totalCount']}") String totalCount
){
this.totalCount = Long.parseLong(totalCount);
this.current = new AtomicLong(0);
}
@Override
public Boolean read() throws Exception, UnexpectedInputException, ParseException, NonTransientResourceException {
if(current.incrementAndGet() > totalCount) return null;
return true;
}
}
100번 반복해서 읽어오는 Reader를 읽어서 랜덤하게 ApiOrder객체로 변환시켜준다.
ApiOrderGenerateProcessor.java
@Component
public class ApiOrderGenerateProcessor implements ItemProcessor<Boolean, ApiOrder> {
private final List<Long> customerIds = LongStream.range(0,20).boxed().toList();
private final List<ServicePolicy> servicePolicies = Arrays.stream(ServicePolicy.values()).toList();
private final ThreadLocalRandom random = ThreadLocalRandom.current();
private final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
@Override
public ApiOrder process(Boolean item) throws Exception {
Long randomCustomerId = customerIds.get(random.nextInt(customerIds.size()));
ServicePolicy randomservicePolicy = servicePolicies.get(random.nextInt(servicePolicies.size()));
//80% 확률
ApiOrder.State randomState = random.nextInt(5) % 5 == 1 ?
ApiOrder.State.FAIL : ApiOrder.State.SUCCESS;
return new ApiOrder(
UUID.randomUUID().toString(),
randomCustomerId,
randomservicePolicy.getUrl(),
randomState,
LocalDateTime.now().format(dateTimeFormatter)
);
}
}
이제 실행을 시켜보자.
이렇게 100개의 랜덤 ApiOrder를 임의로 생성해봤는데, 이제는 Batch를 사용했기때문에 더 많은 데이터로 테스트 할 필요가 있다. 나의 코드에선 totalCount를 늘려주면 되는데
이렇게 직접 argument를 넣어줄수도 있지만, 인자가 많아지거나, 오타를 치는 등 실수 할 가능성이있다.
그래서 나는 이러한 실수를 방지하기 위해 Builder패턴에서 validator를 활용했다.
나는 requiredKeys를 설정하도록 하겠다. 필수값인 부분이다.
참고로 validator는 제일 앞 단에서 잡아주는 것이 좋다. Job이 아니라, Step, Item 단에서 설정해주면 데이터가 꼬일 수도있고, 판단도 이상하게 할 수있기 때문이다.
@Bean
public Job apiOrderGenerateJob(Step step){
return new JobBuilder("apiOrderGenerateJob", jobRepository)
.start(step)
.incrementer(new RunIdIncrementer())
.validator(new DefaultJobParametersValidator(new String[]{"targetDate", "totalCount"}, new String[0]))
.build();
}
validator가 추가된 Job부분 옵션부분은 0으로 채워서 무시했다.
그리고 totalCount를 1만으로 바꿔서 실행했다.
1초도 걸리지 않았다. 50만개도 넣어서 실행시켜봤다.
시간이 많이 늘어났다. 그렇다면 여기서 더 성능을 높일 순 없을까? Chunk 사이즈를 늘려보자!
1000에서 5000으로 늘려봤다.
역시 시간이 줄어든 것을 볼 수있다. 하지만 롤백이 되는 경우도 고려해줘야한다.
결제 서비스를 만들기 위한 1일단위의 배치가 끝났다. 다음글에서는 일주일치 정산까지 하는 기능으로 확장에 대해 다뤄보겠다.
2024.03.27 - [Spring/결제] - Spring으로 결제서비스 만들기(2)
'Spring > 결제' 카테고리의 다른 글
Spring으로 결제서비스 만들기(4) (1) | 2024.03.31 |
---|---|
Spring으로 결제서비스 만들기(3) (0) | 2024.03.28 |
Spring으로 결제서비스 만들기(2) (0) | 2024.03.27 |