From ad168358cb42ff0b781ad5c49244c53b11707b2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=B1=EC=98=81?= Date: Thu, 11 Dec 2025 10:50:06 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EA=B3=BC=ED=83=9C=EB=A3=8C=20=EB=8C=80?= =?UTF-8?q?=EC=83=81=20=EB=B9=84=EA=B5=90=20=EB=A1=9C=EC=A7=81=20=EB=B3=91?= =?UTF-8?q?=EB=A0=AC=20=EC=B2=98=EB=A6=AC=20=EA=B5=AC=ED=98=84=20=EB=B0=8F?= =?UTF-8?q?=20=EC=84=B1=EB=8A=A5=20=EC=B5=9C=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - **병렬 처리 도입** - 대상 데이터별로 처리 작업을 병렬화하여 성능 개선 - I/O 작업 처리 시 CPU 코어 수의 2배 스레드 풀 적용 - **트랜잭션 단위 변경** - 기존: 전체가 하나의 트랜잭션으로 관리되어 하나의 대상 실패 시 전체 롤백 처리 - 변경: 개별 데이터에 대해 독립적인 트랜잭션 적용, 실패 데이터만 롤백 - **통계 데이터 추가** - 성공/실패, 유형별 처리 건수(상품용, 이첩, 정상) 통계 제공 - **기타** - `TransactionTemplate` 및 `CompletableFuture` 활용 - 병렬 처리 로그 및 예외 처리 추가 - 코드 주석 및 가독성 개선 --- .../service/impl/CarFfnlgTrgtServiceImpl.java | 227 ++++++++++++------ 1 file changed, 148 insertions(+), 79 deletions(-) diff --git a/src/main/java/go/kr/project/carInspectionPenalty/registration/service/impl/CarFfnlgTrgtServiceImpl.java b/src/main/java/go/kr/project/carInspectionPenalty/registration/service/impl/CarFfnlgTrgtServiceImpl.java index db8d8f8..824adf0 100644 --- a/src/main/java/go/kr/project/carInspectionPenalty/registration/service/impl/CarFfnlgTrgtServiceImpl.java +++ b/src/main/java/go/kr/project/carInspectionPenalty/registration/service/impl/CarFfnlgTrgtServiceImpl.java @@ -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 compareWithApi(List> targetList) { - log.info("========== API 호출 및 비교 시작 =========="); + log.info("========== API 호출 및 비교 시작 (병렬처리) =========="); log.info("선택된 데이터 건수: {}", targetList != null ? targetList.size() : 0); if (targetList == null || targetList.isEmpty()) { throw new IllegalArgumentException("선택된 데이터가 없습니다."); } - List> compareResults = new ArrayList<>(); - int successCount = 0; - int productUseCount = 0; // 상품용 건수 - int transferCount = 0; // 이첩 건수 - int normalCount = 0; // 정상 처리 건수 - - for (Map 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 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>> 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> 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 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 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 processOneTarget(Map target) { + String carFfnlgTrgtId = target.get("carFfnlgTrgtId"); + String vhclno = target.get("vhclno"); + String inspYmd = target.get("inspYmd"); + + log.info("처리 중 - 차량번호: {}, 검사일자: {}", vhclno, inspYmd); + + Map 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 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; } /**