feat: 과태료 대상 비교 로직 병렬 처리 구현 및 성능 최적화

- **병렬 처리 도입**
  - 대상 데이터별로 처리 작업을 병렬화하여 성능 개선
  - I/O 작업 처리 시 CPU 코어 수의 2배 스레드 풀 적용

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

- **통계 데이터 추가**
  - 성공/실패, 유형별 처리 건수(상품용, 이첩, 정상) 통계 제공

- **기타**
  - `TransactionTemplate` 및 `CompletableFuture` 활용
  - 병렬 처리 로그 및 예외 처리 추가
  - 코드 주석 및 가독성 개선
main
박성영 5 days ago
parent 0dbd34c40c
commit ad168358cb

@ -16,6 +16,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;
@ -27,6 +28,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
@ -40,6 +46,7 @@ public class CarFfnlgTrgtServiceImpl extends EgovAbstractServiceImpl implements
private final CarFfnlgTxtParseConfig parseConfig;
private final ExternalVehicleApiService service;
private final ComparisonService comparisonService;
private final TransactionTemplate transactionTemplate;
// 날짜 형식 (YYYYMMDD)
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
@ -899,117 +906,179 @@ public class CarFfnlgTrgtServiceImpl extends EgovAbstractServiceImpl implements
}
/**
* API /
* : 1
* API / ()
* : , .
* , .
*
* @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; // 정상 처리 건수
for (Map<String, String> target : targetList) {
String carFfnlgTrgtId = target.get("carFfnlgTrgtId");
String vhclno = target.get("vhclno");
String inspYmd = target.get("inspYmd");
String rgtr = target.get("rgtr"); // 등록자 (사용자 ID)
// I/O 작업이므로 CPU 코어 수의 2배로 스레드 풀 생성
int threadPoolSize = Runtime.getRuntime().availableProcessors() * 2;
ExecutorService executor = Executors.newFixedThreadPool(threadPoolSize);
log.info("처리 중 - 차량번호: {}, 검사일자: {}", vhclno, inspYmd);
log.info("병렬처리 스레드 풀 크기: {}", threadPoolSize);
Map<String, Object> compareResult = new HashMap<>();
compareResult.put("carFfnlgTrgtId", carFfnlgTrgtId);
compareResult.put("vhclno", vhclno);
try {
// 1. 기존 데이터 조회
CarFfnlgTrgtVO existingData = new CarFfnlgTrgtVO();
existingData.setCarFfnlgTrgtId(carFfnlgTrgtId);
existingData = mapper.selectOne(existingData);
try {
// 병렬로 각 건 처리
List<CompletableFuture<Map<String, Object>>> futures = targetList.stream()
.map(target -> CompletableFuture.supplyAsync(() -> processOneTarget(target), executor))
.collect(Collectors.toList());
if (existingData == null) {
String errorMsg = String.format("기존 데이터를 찾을 수 없습니다. 차량번호: %s", vhclno);
log.error(errorMsg);
throw new MessageException(errorMsg);
}
// 모든 작업 완료 대기
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
// 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);
}
// 결과 수집
List<Map<String, Object>> compareResults = futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
// 3. 비교 로직 실행
String statusCode = comparisonService.executeComparison(existingData);
// 통계 집계
int successCount = 0;
int failCount = 0;
int productUseCount = 0;
int transferCount = 0;
int normalCount = 0;
// 결과 처리
if (statusCode != null) {
// 비교 규칙이 적용됨
if (TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_02_PRODUCT_USE.equals(statusCode)) {
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
* @return
*/
private Map<String, Object> processOneTarget(Map<String, String> target) {
String carFfnlgTrgtId = target.get("carFfnlgTrgtId");
String vhclno = target.get("vhclno");
String inspYmd = target.get("inspYmd");
log.info("처리 중 - 차량번호: {}, 검사일자: {}", vhclno, inspYmd);
Map<String, Object> compareResult = new HashMap<>();
compareResult.put("carFfnlgTrgtId", carFfnlgTrgtId);
compareResult.put("vhclno", vhclno);
try {
// 개별 트랜잭션으로 실행
String statusCode = transactionTemplate.execute(status -> {
try {
// 1. 기존 데이터 조회
CarFfnlgTrgtVO existingData = new CarFfnlgTrgtVO();
existingData.setCarFfnlgTrgtId(carFfnlgTrgtId);
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. 비교 로직 실행
return comparisonService.executeComparison(existingData);
Map<String, Object> resultData = new HashMap<>();
resultData.put("compareResults", compareResults);
resultData.put("totalCount", targetList.size());
resultData.put("successCount", successCount);
resultData.put("failCount", 0); // 1건이라도 실패하면 전체 롤백되므로 실패건수는 항상 0
resultData.put("productUseCount", productUseCount);
resultData.put("transferCount", transferCount);
resultData.put("normalCount", 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", "정상");
}
log.info("========== API 호출 및 비교 완료 ==========");
log.info("성공: {}건, 상품용: {}건, 이첩: {}건, 정상: {}건",
successCount, productUseCount, transferCount, normalCount);
} 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