From 28ab6863da819965a6374ca56b50f2b14e1d1782 Mon Sep 17 00:00:00 2001 From: eastargh Date: Thu, 27 Aug 2026 16:54:01 +0900 Subject: [PATCH] =?UTF-8?q?=EC=9B=B9=ED=9B=85=20=EB=B0=9C=EC=86=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../onl/apim/webhook/webhookSendLogMan.jsp | 5 ++-- .../djb/webhook/service/WebhookService.java | 26 ++++++++++++++++--- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/WebContent/jsp/onl/apim/webhook/webhookSendLogMan.jsp b/WebContent/jsp/onl/apim/webhook/webhookSendLogMan.jsp index 1007a35..0c88f88 100644 --- a/WebContent/jsp/onl/apim/webhook/webhookSendLogMan.jsp +++ b/WebContent/jsp/onl/apim/webhook/webhookSendLogMan.jsp @@ -89,7 +89,7 @@ mtype: 'POST', url: url, postData: getSearchForJqgrid("cmd", "LIST"), - colNames: ['id', 'No.', '기관명', '이벤트유형', 'Target URL', 'Proxy URL', 'Status', '성공여부', '발송서버', '발송일시'], + colNames: ['id', 'No.', '기관명', '이벤트유형', 'Target URL', 'Proxy URL', 'Status', '성공여부', '재시도', '발송서버', '발송일시'], colModel: [ {name: 'id', align: 'center', key: true, hidden: true}, {name: 'rowNum', align: 'center', width: 45, sortable: false}, @@ -99,11 +99,12 @@ {name: 'proxyUrl', align: 'left', width: 300, formatter: formatTargetUrl}, {name: 'statusCode', align: 'center', width: 60}, {name: 'success', align: 'center', width: 70, formatter: formatSuccess}, + {name: 'retryCount', align: 'center', width: 60}, {name: 'sendBy', align: 'center', width: 100}, {name: 'sentAt', align: 'center', width: 140} ], jsonReader: {repeatitems: false}, - pager: $('#pager'), + pager: $('#pager'), page: '${param.page}', rowNum: '${rmsDefaultRowNum}', autoheight: true, diff --git a/src/main/java/com/eactive/eai/rms/ext/djb/webhook/service/WebhookService.java b/src/main/java/com/eactive/eai/rms/ext/djb/webhook/service/WebhookService.java index c0bfdae..35b54b4 100644 --- a/src/main/java/com/eactive/eai/rms/ext/djb/webhook/service/WebhookService.java +++ b/src/main/java/com/eactive/eai/rms/ext/djb/webhook/service/WebhookService.java @@ -17,7 +17,6 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.HttpServerErrorException; import org.springframework.web.client.RestTemplate; @@ -146,8 +145,12 @@ public class WebhookService { /** * 웹훅 발송 메인 메서드 + * + *

{@code @Transactional}을 두지 않는다 - 재시도(최대 retryCount회, Thread.sleep 포함)가 + * 이 메서드 안에서 동기로 도는데, 메서드 전체를 하나의 트랜잭션으로 감싸면 재시도 도중의 + * 실패 로그 저장(sendWithRetry 내부)이 커밋되지 않고 메서드가 끝날 때까지 대기하게 되어 + * "실패 시 즉시 로그부터 남긴다"는 의도가 무의미해진다. save() 호출마다 개별 커밋되도록 둔다.

*/ - @Transactional public void send(WebhookSendRequest req) { String targetUrl = req.getTargetUrl(); @@ -235,6 +238,8 @@ public class WebhookService { * 재시도 포함 HTTP 발송 * - 4xx: 클라이언트 오류이므로 즉시 throw (재시도 불필요) * - 5xx / 네트워크 오류: 최대 RETRY_COUNT회까지 선형 증가 backoff 재시도 (1s → 2s → 3s → 4s → 5s) + * - 실패할 때마다 즉시 로그를 저장한다 (최초 실패는 INSERT, 이후 재시도 실패는 같은 row를 UPDATE) - + * 재시도 도중 앱이 죽어도 마지막으로 저장된 실패 기록이 남도록 하기 위함 */ private ResponseEntity sendWithRetry(String proxyUrl, HttpEntity request, WebhookSendLog sendLog, int retryCount, int retryTime) throws Exception { Exception lastException = null; @@ -243,21 +248,34 @@ public class WebhookService { if (attempt > 0) { long delayMs = (long) retryTime * attempt; log.info("[Webhook] 재시도 {}/{} - url: {}, delay: {}ms", attempt, retryCount, proxyUrl, delayMs); - sendLog.setRetryCount(attempt); Thread.sleep(delayMs); } return restTemplate.postForEntity(proxyUrl, request, String.class); } catch (HttpClientErrorException e) { //throw e; // 4xx는 재시도 불필요 - lastException = e; //TODO: 재시도 테스트 + lastException = e; //TODO: 재시도 테스트 + recordAttemptFailure(sendLog, attempt, e.getStatusCode().value(), e.getResponseBodyAsString(), e.getMessage()); } catch (Exception e) { lastException = e; log.warn("[Webhook] 발송 실패 (attempt {}/{}) - url: {}, error: {}", attempt, retryCount, proxyUrl, e.getMessage()); + recordAttemptFailure(sendLog, attempt, null, null, e.getMessage()); } } throw lastException; } + // 실패한 시도 하나를 즉시 저장한다. sendLog에 id가 없으면(최초 실패) INSERT, 있으면(재시도 실패) 같은 row를 UPDATE한다. + private void recordAttemptFailure(WebhookSendLog sendLog, int attempt, Integer statusCode, String responseBody, String errorMessage) { + sendLog.setRetryCount(attempt); + sendLog.setSuccess(false); + sendLog.setErrorMessage(errorMessage); + if (statusCode != null) { + sendLog.setStatusCode(statusCode); + sendLog.setResponseBody(responseBody); + } + sendLogRepository.save(sendLog); + } + /** * HMAC-SHA256 서명 생성 * Java 17 미만은 HexFormat 미지원이므로 직접 변환