feat: 과태료 대상 비교 로직 병렬 처리 및 트랜잭션 관리 개선

- **병렬 처리 도입**
  - 대상 데이터 처리 작업을 병렬화하여 성능 최적화
  - CPU 코어 수의 2배 스레드 풀 생성 및 `CompletableFuture` 활용

- **트랜잭션 단위 변경**
  - 기존: 전체 트랜잭션으로 관리, 하나의 실패 시 전체 롤백
  - 변경: 개별 대상별 트랜잭션 적용, 실패 데이터만 롤백

- **통계 및 로그 추가**
  - 성공/실패, 유형별 처리 건수(상품용, 이첩, 정상 등) 통계 데이터 제공
  - 병렬 처리 상태 및 작업 내역 로그 추가

- **기타**
  - `TransactionTemplate` 도입 및 가독성을 위한 코드 리팩토링
  - 문서 업데이트 (`자동차과태료_비교로직_정리-[미필].md`, `자동차과태료_비교로직_정리-[지연].md`)
main
박성영 5 days ago
parent 914f61256b
commit dde9bf4c02

@ -6,9 +6,9 @@
### 구현 위치
- **Checker 클래스**: `src/main/java/go/kr/project/carInspectionPenalty/registrationOm/service/impl/om_checker/`
- `ProductUseOmChecker.java` - 1. 상품용
- `OwnerTransferOmChecker.java` - 2. 명의이전 소유자 확인
- `TransferOmChecker.java` - 3. 이첩
- `ProductUseOmChecker.java` - 1. 상품용 : 상품용
- `OwnerTransferOmChecker.java` - 2. 명의이전 소유자 확인 : 명의이전(25.9.5.) 이전소유자 상품용, 경상남도 창원시/ 현대캐피탈 주식회사, 미수검명의이전(25.5.19.)(37하1553)
- `TransferOmChecker.java` - 3. 이첩 : 경기도 과천시/ 이정호, 115일 도래지
### 기본 설정
- 비교로직에 사용되는 API: `ExternalVehicleApiServiceImpl.getBasicInfo`, `getLedgerInfo` 호출

@ -6,13 +6,13 @@
### 구현 위치
- **Checker 클래스**: `src/main/java/go/kr/project/carInspectionPenalty/registration/service/impl/delay_checker/`
- `ProductUseChecker.java` - 1. 상품용(명의이전)
- `ProductUseChnageChecker.java` - 1-1. 상품용(변경등록)
- `ProductCloseWithin31Checker.java` - 2. 내사종결(명의이전 이전소유자 상품용, 31일 이내)
- `OwnerCloseWithin31Checker.java` - 3. 내사종결(순수 명의이전, 31일 이내)
- `ProductLevyOver31Checker.java` - 4. 날짜수정후부과(명의이전 이전소유자 상품용, 31일 초과)
- `OwnerLevyOver31Checker.java` - 5. 날짜수정후부과(순수 명의이전, 31일 초과)
- `TransferCase115DayChecker.java` - 6. 이첩
- `ProductUseChecker.java` - 1. 상품용(명의이전) : 상품용
- `ProductUseChnageChecker.java` - 1-1. 상품용(변경등록) : 상품용
- `ProductCloseWithin31Checker.java` - 2. 내사종결(명의이전 이전소유자 상품용, 31일 이내) : 명의이전(25.9.11.) 이전소유자 상품용
- `OwnerCloseWithin31Checker.java` - 3. 내사종결(순수 명의이전, 31일 이내) : 명의이전(25.8.28.)
- `ProductLevyOver31Checker.java` - 4. 날짜수정후부과(명의이전 이전소유자 상품용, 31일 초과) : 경기도 고양시/ 장준혁, 미수검명의이전(25.8.19.)
- `OwnerLevyOver31Checker.java` - 5. 날짜수정후부과(순수 명의이전, 31일 초과) : 대구광역시 달서구/ 하나캐피탈(주), 미수검명의이전(25.9.3.)
- `TransferCase115DayChecker.java` - 6. 이첩 : 경상남도 창원시/ 현대캐피탈 주식회사, 115일 도래지, 인천광역시 부평구/ (주)우리카드, 검사일사용본거지
### 기본 설정
- 비교로직에 사용되는 API: `ExternalVehicleApiServiceImpl.getBasicInfo`, `getLedgerInfo` 호출

@ -15,6 +15,7 @@ import lombok.extern.slf4j.Slf4j;
import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.multipart.MultipartFile;
import java.io.BufferedReader;
@ -26,6 +27,11 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* Service
@ -38,6 +44,7 @@ public class CarFfnlgTrgtIncmpServiceImpl extends EgovAbstractServiceImpl implem
private final CarFfnlgTrgtIncmpMapper mapper;
private final CarFfnlgPrnParseConfig parseConfig;
private final ComparisonOmService comparisonOmService;
private final TransactionTemplate transactionTemplate;
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
@ -396,127 +403,202 @@ public class CarFfnlgTrgtIncmpServiceImpl extends EgovAbstractServiceImpl implem
}
/**
* API /
* API / ()
* = + OM_DAY_CD D (146)
* : , .
* , .
*
* @param targetList
* @return
*/
@Override
@Transactional
public Map<String, Object> compareWithApi(List<Map<String, String>> targetList) {
log.info("========== 미필 API 호출 및 비교 시작 ==========");
log.info("========== 미필 API 호출 및 비교 시작 (병렬처리) ==========");
log.info("선택된 데이터 건수: {}", targetList != null ? targetList.size() : 0);
if (targetList == null || targetList.isEmpty()) {
throw new IllegalArgumentException("선택된 데이터가 없습니다.");
}
List<Map<String, Object>> compareResults = new ArrayList<>();
int successCount = 0;
int productUseCount = 0;
int transferCount = 0;
int normalCount = 0;
// 가산일 조회 (OM_DAY_CD 코드의 D값)
// 가산일 조회 (OM_DAY_CD 코드의 D값) - 병렬처리 전에 미리 조회
String plusDayStr = mapper.selectOmDayPlusDay();
int plusDay = 146; // 기본값
final int plusDay;
if (plusDayStr != null && !plusDayStr.isEmpty()) {
int parsed = 146;
try {
plusDay = Integer.parseInt(plusDayStr);
parsed = Integer.parseInt(plusDayStr);
} catch (NumberFormatException e) {
log.warn("가산일 파싱 실패, 기본값 146 사용: {}", plusDayStr);
}
plusDay = parsed;
} else {
plusDay = 146; // 기본값
}
log.info("부과일자 가산일: {}일", plusDay);
for (Map<String, String> target : targetList) {
String carFfnlgTrgtIncmpId = target.get("carFfnlgTrgtIncmpId");
String vhclno = target.get("vhclno");
String inspVldPrd = target.get("inspVldPrd");
log.info("처리 중 - 차량번호: {}, 검사유효기간: {}", vhclno, inspVldPrd);
Map<String, Object> compareResult = new HashMap<>();
compareResult.put("carFfnlgTrgtIncmpId", carFfnlgTrgtIncmpId);
compareResult.put("vhclno", vhclno);
try {
// 1. 기존 데이터 조회
CarFfnlgTrgtIncmpVO existingData = new CarFfnlgTrgtIncmpVO();
existingData.setCarFfnlgTrgtIncmpId(carFfnlgTrgtIncmpId);
existingData = mapper.selectOne(existingData);
if (existingData == null) {
String errorMsg = String.format("기존 데이터를 찾을 수 없습니다. 차량번호: %s", vhclno);
log.error(errorMsg);
throw new MessageException(errorMsg);
}
// 2. 처리상태 검증 - 접수상태(01)가 아닌 경우 API 호출 불가
if (!TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_01_RCPT.equals(existingData.getTaskPrcsSttsCd())) {
String errorMsg = String.format("접수 상태(01)인 데이터만 API 호출이 가능합니다. 차량번호: %s, 현재 상태: %s",
vhclno, existingData.getTaskPrcsSttsCd());
log.error(errorMsg);
throw new MessageException(errorMsg);
}
// 3. 검사유효기간에서 부과일자 계산 (종료일 + 가산일)
String levyCrtrYmd = calculateLevyCrtrYmdFromInspVldPrd(inspVldPrd, plusDay);
existingData.setLevyCrtrYmd(levyCrtrYmd);
log.info("부과일자 계산 완료 - 검사유효기간: {}, 부과일자: {}", inspVldPrd, levyCrtrYmd);
// I/O 작업이므로 CPU 코어 수의 2배로 스레드 풀 생성
int threadPoolSize = Runtime.getRuntime().availableProcessors() * 2;
ExecutorService executor = Executors.newFixedThreadPool(threadPoolSize);
// 4. 비교 로직 실행
String statusCode = comparisonOmService.executeComparison(existingData);
log.info("병렬처리 스레드 풀 크기: {}", threadPoolSize);
// 결과 처리
if (statusCode != null) {
if (TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_02_PRODUCT_USE.equals(statusCode)) {
try {
// 병렬로 각 건 처리
List<CompletableFuture<Map<String, Object>>> futures = targetList.stream()
.map(target -> CompletableFuture.supplyAsync(() -> processOneTarget(target, plusDay), executor))
.collect(Collectors.toList());
// 모든 작업 완료 대기
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
// 결과 수집
List<Map<String, Object>> compareResults = futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
// 통계 집계
int successCount = 0;
int failCount = 0;
int productUseCount = 0;
int transferCount = 0;
int normalCount = 0;
for (Map<String, Object> result : compareResults) {
Boolean success = (Boolean) result.get("success");
if (Boolean.TRUE.equals(success)) {
successCount++;
String processStatus = (String) result.get("processStatus");
if ("상품용".equals(processStatus)) {
productUseCount++;
compareResult.put("processStatus", "상품용");
compareResult.put("message", "상품용으로 처리되었습니다.");
} else if (TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_03_TRANSFER.equals(statusCode)) {
} else if ("이첩".equals(processStatus)) {
transferCount++;
compareResult.put("processStatus", "이첩");
compareResult.put("message", "이첩으로 처리되었습니다.");
} else if (TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_04_INVESTIGATION_CLOSED.equals(statusCode)) {
} else if ("내사종결".equals(processStatus) || "정상".equals(processStatus)) {
normalCount++;
compareResult.put("processStatus", "내사종결");
compareResult.put("message", "내사종결로 처리되었습니다.");
} else {
normalCount++;
compareResult.put("processStatus", "기타");
compareResult.put("message", "기타 상태로 처리되었습니다.");
}
compareResult.put("success", true);
successCount++;
} else {
normalCount++;
compareResult.put("success", true);
compareResult.put("message", "정상 처리되었습니다.");
compareResult.put("processStatus", "정상");
successCount++;
failCount++;
}
} catch (Exception e) {
log.error("데이터 비교 중 오류 발생 - 차량번호: {}, 전체 롤백 처리", vhclno, e);
throw new MessageException(e.getMessage(), e);
}
compareResults.add(compareResult);
Map<String, Object> resultData = new HashMap<>();
resultData.put("compareResults", compareResults);
resultData.put("totalCount", targetList.size());
resultData.put("successCount", successCount);
resultData.put("failCount", failCount);
resultData.put("productUseCount", productUseCount);
resultData.put("transferCount", transferCount);
resultData.put("normalCount", normalCount);
log.info("========== 미필 API 호출 및 비교 완료 (병렬처리) ==========");
log.info("전체: {}건, 성공: {}건, 실패: {}건, 상품용: {}건, 이첩: {}건, 정상: {}건",
targetList.size(), successCount, failCount, productUseCount, transferCount, normalCount);
return resultData;
} finally {
// ExecutorService 종료
executor.shutdown();
try {
if (!executor.awaitTermination(120, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
/**
* ( )
*
* @param target
* @param plusDay
* @return
*/
private Map<String, Object> processOneTarget(Map<String, String> target, int plusDay) {
String carFfnlgTrgtIncmpId = target.get("carFfnlgTrgtIncmpId");
String vhclno = target.get("vhclno");
String inspVldPrd = target.get("inspVldPrd");
log.info("처리 중 - 차량번호: {}, 검사유효기간: {}", vhclno, inspVldPrd);
Map<String, Object> compareResult = new HashMap<>();
compareResult.put("carFfnlgTrgtIncmpId", carFfnlgTrgtIncmpId);
compareResult.put("vhclno", vhclno);
try {
// 개별 트랜잭션으로 실행
String statusCode = transactionTemplate.execute(status -> {
try {
// 1. 기존 데이터 조회
CarFfnlgTrgtIncmpVO existingData = new CarFfnlgTrgtIncmpVO();
existingData.setCarFfnlgTrgtIncmpId(carFfnlgTrgtIncmpId);
existingData = mapper.selectOne(existingData);
if (existingData == null) {
String errorMsg = String.format("기존 데이터를 찾을 수 없습니다. 차량번호: %s", vhclno);
log.error(errorMsg);
throw new MessageException(errorMsg);
}
Map<String, Object> resultData = new HashMap<>();
resultData.put("compareResults", compareResults);
resultData.put("totalCount", targetList.size());
resultData.put("successCount", successCount);
resultData.put("failCount", 0);
resultData.put("productUseCount", productUseCount);
resultData.put("transferCount", transferCount);
resultData.put("normalCount", normalCount);
// 2. 처리상태 검증 - 접수상태(01)가 아닌 경우 API 호출 불가
if (!TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_01_RCPT.equals(existingData.getTaskPrcsSttsCd())) {
String errorMsg = String.format("접수 상태(01)인 데이터만 API 호출이 가능합니다. 차량번호: %s, 현재 상태: %s",
vhclno, existingData.getTaskPrcsSttsCd());
log.error(errorMsg);
throw new MessageException(errorMsg);
}
// 3. 검사유효기간에서 부과일자 계산 (종료일 + 가산일)
String levyCrtrYmd = calculateLevyCrtrYmdFromInspVldPrd(inspVldPrd, plusDay);
existingData.setLevyCrtrYmd(levyCrtrYmd);
log.info("부과일자 계산 완료 - 검사유효기간: {}, 부과일자: {}", inspVldPrd, levyCrtrYmd);
// 4. 비교 로직 실행
return comparisonOmService.executeComparison(existingData);
log.info("========== 미필 API 호출 및 비교 완료 ==========");
log.info("성공: {}건, 상품용: {}건, 이첩: {}건, 정상: {}건",
successCount, productUseCount, transferCount, normalCount);
} catch (Exception e) {
// 트랜잭션 롤백
status.setRollbackOnly();
throw e;
}
});
// 결과 처리
if (statusCode != null) {
// 비교 규칙이 적용됨
if (TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_02_PRODUCT_USE.equals(statusCode)) {
compareResult.put("processStatus", "상품용");
compareResult.put("message", "상품용으로 처리되었습니다.");
} else if (TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_03_TRANSFER.equals(statusCode)) {
compareResult.put("processStatus", "이첩");
compareResult.put("message", "이첩으로 처리되었습니다.");
} else if (TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_04_INVESTIGATION_CLOSED.equals(statusCode)) {
compareResult.put("processStatus", "내사종결");
compareResult.put("message", "내사종결로 처리되었습니다.");
} else {
compareResult.put("processStatus", "기타");
compareResult.put("message", "기타 상태로 처리되었습니다.");
}
compareResult.put("success", true);
} else {
// 정상 처리 (비교 로직에 해당 안됨)
compareResult.put("success", true);
compareResult.put("message", "정상 처리되었습니다.");
compareResult.put("processStatus", "정상");
}
} catch (Exception e) {
log.error("데이터 비교 중 오류 발생 - 차량번호: {}", vhclno, e);
compareResult.put("success", false);
compareResult.put("message", "처리 실패: " + e.getMessage());
compareResult.put("processStatus", "실패");
}
return resultData;
return compareResult;
}
/**

Loading…
Cancel
Save