snp-batch-validation/src/main/java/com/snp/batch/service/BatchDateService.java
HYOJIN 81b29765a1 fix(배치): RECOLLECT 모드에서 Tasklet 자체 스킵으로 last_success_date 복원 로직 제거 (#50)
- 14개 LastExecutionUpdateTasklet에 executionMode 체크 추가
- ShipDetailUpdateJobConfig: retryModeDecider → emptyResponseDecider 변경
- RecollectionJobExecutionListener: last_success_date 저장/복원 로직 제거
- RecollectionHistoryService: 미사용 메서드 제거

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 16:42:49 +09:00

219 lines
9.2 KiB
Java

package com.snp.batch.service;
import com.snp.batch.global.model.BatchCollectionPeriod;
import com.snp.batch.global.repository.BatchCollectionPeriodRepository;
import com.snp.batch.global.repository.BatchLastExecutionRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.scope.context.StepContext;
import org.springframework.batch.core.scope.context.StepSynchronizationManager;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
@Slf4j
@Service
@RequiredArgsConstructor
public class BatchDateService {
private final BatchLastExecutionRepository repository;
private final BatchCollectionPeriodRepository collectionPeriodRepository;
/**
* 현재 Step의 Job 파라미터에서 executionMode를 확인
*/
private String getExecutionMode() {
try {
StepContext context = StepSynchronizationManager.getContext();
if (context != null && context.getStepExecution() != null) {
return context.getStepExecution().getJobExecution()
.getJobParameters().getString("executionMode", "NORMAL");
}
} catch (Exception e) {
log.debug("StepSynchronizationManager 컨텍스트 접근 실패, NORMAL 모드로 처리", e);
}
return "NORMAL";
}
/**
* 현재 Step의 Job 파라미터에서 apiKey 파라미터를 확인 (재수집용)
*/
private String getRecollectApiKey() {
try {
StepContext context = StepSynchronizationManager.getContext();
if (context != null && context.getStepExecution() != null) {
return context.getStepExecution().getJobExecution()
.getJobParameters().getString("apiKey");
}
} catch (Exception e) {
// ignore
}
return null;
}
/**
* 현재 Step의 Job 파라미터에서 executor를 확인.
* AUTO_RETRY/MANUAL_RETRY이면 실패건 재수집이므로 기간 테이블을 사용하지 않는다.
*/
private boolean isFailedRecordRetry() {
try {
StepContext context = StepSynchronizationManager.getContext();
if (context != null && context.getStepExecution() != null) {
String executor = context.getStepExecution().getJobExecution()
.getJobParameters().getString("executor");
return "AUTO_RETRY".equals(executor) || "MANUAL_RETRY".equals(executor);
}
} catch (Exception e) {
log.debug("executor 파라미터 확인 실패", e);
}
return false;
}
public Map<String, String> getDateRangeWithoutTimeParams(String apiKey) {
// 기간 재수집 모드: batch_collection_period에서 날짜 조회
// 실패건 재수집(AUTO_RETRY/MANUAL_RETRY)은 정상 모드와 동일하게 last_success_date 기반 사용
if ("RECOLLECT".equals(getExecutionMode()) && !isFailedRecordRetry()) {
return getCollectionPeriodDateParams(apiKey);
}
// 정상 모드: last_success_date ~ now()
return repository.findDateRangeByApiKey(apiKey)
.map(projection -> {
LocalDateTime toDate = LocalDateTime.now();
saveToDateToJobContext(toDate);
Map<String, String> params = new HashMap<>();
putDateParams(params, "from", projection.getLastSuccessDate());
putDateParams(params, "to", toDate);
params.put("shipsCategory", "0");
return params;
})
.orElseGet(() -> {
log.warn("해당 apiKey에 대한 데이터를 찾을 수 없습니다: {}", apiKey);
return new HashMap<>();
});
}
public Map<String, String> getDateRangeWithTimezoneParams(String apiKey) {
return getDateRangeWithTimezoneParams(apiKey, "fromDate", "toDate");
}
public Map<String, String> getDateRangeWithTimezoneParams(String apiKey, String dateParam1, String dateParam2) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSX");
// 기간 재수집 모드: batch_collection_period에서 날짜 조회
// 실패건 재수집(AUTO_RETRY/MANUAL_RETRY)은 정상 모드와 동일하게 last_success_date 기반 사용
if ("RECOLLECT".equals(getExecutionMode()) && !isFailedRecordRetry()) {
return getCollectionPeriodTimezoneParams(apiKey, dateParam1, dateParam2, formatter);
}
// 정상 모드: last_success_date ~ now()
return repository.findDateRangeByApiKey(apiKey)
.map(projection -> {
LocalDateTime toDate = LocalDateTime.now();
saveToDateToJobContext(toDate);
Map<String, String> params = new HashMap<>();
params.put(dateParam1, formatToUtc(projection.getLastSuccessDate(), formatter));
params.put(dateParam2, formatToUtc(toDate, formatter));
return params;
})
.orElseGet(() -> {
log.warn("해당 apiKey에 대한 데이터를 찾을 수 없습니다: {}", apiKey);
return new HashMap<>();
});
}
/**
* 재수집 모드: batch_collection_period에서 년/월/일 파라미터 생성
*/
private Map<String, String> getCollectionPeriodDateParams(String apiKey) {
String recollectApiKey = getRecollectApiKey();
String lookupKey = recollectApiKey != null ? recollectApiKey : apiKey;
Optional<BatchCollectionPeriod> opt = collectionPeriodRepository.findById(lookupKey);
if (opt.isEmpty()) {
log.warn("[RECOLLECT] 수집기간 설정을 찾을 수 없습니다: {}", lookupKey);
return new HashMap<>();
}
BatchCollectionPeriod cp = opt.get();
Map<String, String> params = new HashMap<>();
putDateParams(params, "from", cp.getRangeFromDate());
putDateParams(params, "to", cp.getRangeToDate());
params.put("shipsCategory", "0");
log.info("[RECOLLECT] batch_collection_period 날짜 사용: apiKey={}, range={}~{}",
lookupKey, cp.getRangeFromDate(), cp.getRangeToDate());
return params;
}
/**
* 재수집 모드: batch_collection_period에서 UTC 타임존 파라미터 생성
*/
private Map<String, String> getCollectionPeriodTimezoneParams(
String apiKey, String dateParam1, String dateParam2, DateTimeFormatter formatter) {
String recollectApiKey = getRecollectApiKey();
String lookupKey = recollectApiKey != null ? recollectApiKey : apiKey;
Optional<BatchCollectionPeriod> opt = collectionPeriodRepository.findById(lookupKey);
if (opt.isEmpty()) {
log.warn("[RECOLLECT] 수집기간 설정을 찾을 수 없습니다: {}", lookupKey);
return new HashMap<>();
}
BatchCollectionPeriod cp = opt.get();
Map<String, String> params = new HashMap<>();
params.put(dateParam1, formatToUtc(cp.getRangeFromDate(), formatter));
params.put(dateParam2, formatToUtc(cp.getRangeToDate(), formatter));
log.info("[RECOLLECT] batch_collection_period 날짜 사용 (UTC): apiKey={}, range={}~{}",
lookupKey, cp.getRangeFromDate(), cp.getRangeToDate());
return params;
}
/**
* 배치 시작 시 캡처한 toDate를 JobExecutionContext에 저장
* LastExecutionUpdateTasklet에서 이 값을 꺼내 LAST_SUCCESS_DATE로 사용
*/
private void saveToDateToJobContext(LocalDateTime toDate) {
try {
StepContext context = StepSynchronizationManager.getContext();
if (context != null && context.getStepExecution() != null) {
context.getStepExecution().getJobExecution()
.getExecutionContext().put("batchToDate", toDate.toString());
log.debug("batchToDate JobContext 저장 완료: {}", toDate);
}
} catch (Exception e) {
log.warn("batchToDate JobContext 저장 실패", e);
}
}
/**
* LocalDateTime에서 연, 월, 일을 추출하여 Map에 담는 헬퍼 메소드
*/
private void putDateParams(Map<String, String> params, String prefix, LocalDateTime dateTime) {
if (dateTime != null) {
params.put(prefix + "Year", String.valueOf(dateTime.getYear()));
params.put(prefix + "Month", String.valueOf(dateTime.getMonthValue()));
params.put(prefix + "Day", String.valueOf(dateTime.getDayOfMonth()));
}
}
/**
* 한국 시간(LocalDateTime)을 UTC 문자열로 변환
*/
private String formatToUtc(LocalDateTime localDateTime, DateTimeFormatter formatter) {
if (localDateTime == null) return null;
return localDateTime.atZone(ZoneId.of("Asia/Seoul"))
.withZoneSameInstant(ZoneOffset.UTC)
.format(formatter);
}
}