diff --git a/src/main/java/com/eactive/eai/rms/common/context/MonitoringContext.java b/src/main/java/com/eactive/eai/rms/common/context/MonitoringContext.java index 61cf3c1..f274bf3 100644 --- a/src/main/java/com/eactive/eai/rms/common/context/MonitoringContext.java +++ b/src/main/java/com/eactive/eai/rms/common/context/MonitoringContext.java @@ -254,6 +254,10 @@ public interface MonitoringContext { // 이중 로그인 허용여부 public static final String RMS_DUAL_LOGIN_ENABLED = "rms.DUAL_LOGIN_ENABLED"; + + // 웹훅 재전송 설정 + public static final String API_WEBHOOK_RETRY_COUNT = "api.webhook.retry_count"; + public static final String API_WEBHOOK_RETRY_TIME = "api.webhook.retry_time"; diff --git a/src/main/java/com/eactive/eai/rms/ext/djb/apistatus/ApiStatusService.java b/src/main/java/com/eactive/eai/rms/ext/djb/apistatus/ApiStatusService.java index 5bb77d0..885de37 100644 --- a/src/main/java/com/eactive/eai/rms/ext/djb/apistatus/ApiStatusService.java +++ b/src/main/java/com/eactive/eai/rms/ext/djb/apistatus/ApiStatusService.java @@ -76,12 +76,10 @@ public class ApiStatusService { String event = entry.getKey(); List apiIds = entry.getValue(); - Role role = new Role(); - role.setRoleId("api-monitor"); String message = getSwingChatMessage(event, apiIds); - ums.sendMessenger(role, MessageCode.API_STATUS_CHANGED, message); + ums.sendMessenger("api-monitor", MessageCode.API_STATUS_CHANGED, message); - //제휴사 webhook 발송 + //제휴사 웹훅 발송 ums.sendWebhook(event, apiIds); //개발자포탈 API상태모니터링 화면을 위한 처리 diff --git a/src/main/java/com/eactive/eai/rms/ext/djb/inflow/InflowTokenInsufficientEvent.java b/src/main/java/com/eactive/eai/rms/ext/djb/inflow/InflowTokenInsufficientEvent.java deleted file mode 100644 index dadcd80..0000000 --- a/src/main/java/com/eactive/eai/rms/ext/djb/inflow/InflowTokenInsufficientEvent.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.eactive.eai.rms.ext.djb.inflow; - -import com.eactive.eai.rms.data.entity.onl.djb.inflow.InflowTokenInsufficient; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -@Getter -@RequiredArgsConstructor -public class InflowTokenInsufficientEvent { - private final String eaisvcname; - private final String eaisvcdesc; - private final long cnt; - private final long rangeMinute; - - public static InflowTokenInsufficientEvent from(InflowTokenInsufficient summary, long rangeMinute) { - return new InflowTokenInsufficientEvent(summary.getEaisvcname(), summary.getEaisvcdesc(), summary.getCnt(), rangeMinute); - } -} \ No newline at end of file diff --git a/src/main/java/com/eactive/eai/rms/ext/djb/inflow/InflowTokenService.java b/src/main/java/com/eactive/eai/rms/ext/djb/inflow/InflowTokenService.java index fa7a12c..4859ee4 100644 --- a/src/main/java/com/eactive/eai/rms/ext/djb/inflow/InflowTokenService.java +++ b/src/main/java/com/eactive/eai/rms/ext/djb/inflow/InflowTokenService.java @@ -5,11 +5,11 @@ import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.eactive.apim.portal.template.entity.MessageCode; +import com.eactive.eai.rms.data.entity.man.role.Role; import com.eactive.eai.rms.data.entity.onl.djb.inflow.InflowTokenInsufficient; import com.eactive.eai.rms.ext.djb.util.UmsManager; @@ -32,6 +32,7 @@ public class InflowTokenService { for (InflowTokenInsufficient info : rows) { log.debug("유량제어 토큰 획득 실패: {}-{} 최근 {}분 동안 {}건", info.getEaisvcname(), info.getEaisvcdesc(), rangeMinute, info.getCnt()); String message = "유량제어 토큰 획득 실패하였습니다 \n" + info.getEaisvcname(); + ums.sendMessenger("api-monitor", MessageCode.INFLOW_TOKEN_FAILED, message); } } diff --git a/src/main/java/com/eactive/eai/rms/ext/djb/util/UmsManager.java b/src/main/java/com/eactive/eai/rms/ext/djb/util/UmsManager.java index 0bb9d21..a33ccbf 100644 --- a/src/main/java/com/eactive/eai/rms/ext/djb/util/UmsManager.java +++ b/src/main/java/com/eactive/eai/rms/ext/djb/util/UmsManager.java @@ -1,6 +1,5 @@ package com.eactive.eai.rms.ext.djb.util; -import java.security.SecureRandom; import java.util.HashMap; import java.util.List; @@ -36,10 +35,11 @@ public class UmsManager { * @param message * @return */ - public void sendMessenger(Role role, MessageCode messageCode, String message) { - List users = userInfoService.findAllByRoleId(role.getRoleId()); + public void sendMessenger(String roleId, MessageCode messageCode, String message) { + + List users = userInfoService.findAllByRoleId(roleId); if (users.isEmpty()) { - log.warn("sendMessenger: no users found for role '{}'", role.getRoleId()); + log.info("sendMessenger: no users found for role '{}'", roleId); return; } 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 60f5c36..6a2ffc1 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 @@ -22,6 +22,7 @@ import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.HttpServerErrorException; import org.springframework.web.client.RestTemplate; +import com.eactive.eai.rms.common.context.MonitoringContext; import com.eactive.eai.rms.data.entity.onl.djb.webhook.QWebhookReq; import com.eactive.eai.rms.data.entity.onl.djb.webhook.QWebhookReqApi; import com.eactive.eai.rms.data.entity.onl.djb.webhook.QWebhookReqEvent; @@ -44,12 +45,14 @@ public class WebhookService { private final WebhookSendLogRepository sendLogRepository; private final ObjectMapper objectMapper; private final RestTemplate restTemplate; + private final MonitoringContext monitoringContext; @PersistenceContext(unitName = "entityManagerFactoryForEMS") private EntityManager entityManager; private static final String HMAC_ALGORITHM = "HmacSHA256"; - private static final int MAX_RETRY = 3; + private static final int DEFAULT_RETRY_COUNT = 3; + private static final int DEFAULT_RETRY_TIME = 10000; @SuppressWarnings("unchecked") @@ -119,18 +122,20 @@ public class WebhookService { headers.set("X-Webhook-Event", eventType); headers.set("X-Webhook-Timestamp", String.valueOf(webhookPayload.getTimestamp())); - // 4. 발송 + // 4. 발송 (재시도 포함) + int retryCount = monitoringContext.getIntProperty(MonitoringContext.API_WEBHOOK_RETRY_COUNT, DEFAULT_RETRY_COUNT); + int retryTime = monitoringContext.getIntProperty(MonitoringContext.API_WEBHOOK_RETRY_TIME, DEFAULT_RETRY_TIME); sendLog.setSentAt(LocalDateTime.now()); - HttpEntity request = new HttpEntity<>(payloadJson, headers); - ResponseEntity response = restTemplate.postForEntity(targetUrl, request, String.class); + HttpEntity httpRequest = new HttpEntity<>(payloadJson, headers); + ResponseEntity response = sendWithRetry(targetUrl, httpRequest, sendLog, retryCount, retryTime); // 5. 성공 로그 sendLog.setStatusCode(response.getStatusCode().value()); sendLog.setResponseBody(response.getBody()); sendLog.setSuccess(response.getStatusCode().is2xxSuccessful()); - log.info("[Webhook] 발송 성공 - eventType: {}, url: {}, status: {}", - eventType, targetUrl, response.getStatusCode()); + log.info("[Webhook] 발송 성공 - eventType: {}, url: {}, status: {}, retryCount: {}", + eventType, targetUrl, response.getStatusCode(), sendLog.getRetryCount()); } catch (HttpClientErrorException | HttpServerErrorException e) { sendLog.setStatusCode(e.getStatusCode().value()); @@ -151,28 +156,33 @@ public class WebhookService { } + + /** - * 실패 건 재시도 - - @Transactional - public void retryFailedWebhooks() { - List failedLogs = - sendLogRepository.findFailedLogs(MAX_RETRY); - - for (WebhookSendLog failedLog : failedLogs) { + * 재시도 포함 HTTP 발송 + * - 4xx: 클라이언트 오류이므로 즉시 throw (재시도 불필요) + * - 5xx / 네트워크 오류: 최대 RETRY_COUNT회까지 exponential backoff 재시도 (1s → 2s → 4s) + */ + private ResponseEntity sendWithRetry(String targetUrl, HttpEntity request, WebhookSendLog sendLog, int retryCount, int retryTime) throws Exception { + Exception lastException = null; + for (int attempt = 0; attempt <= retryCount; attempt++) { try { - log.info("[Webhook] 재시도 - id: {}, retry: {}", failedLog.getId(), failedLog.getRetryCount()); - failedLog.setRetryCount(failedLog.getRetryCount() + 1); - sendLogRepository.save(failedLog); - - send(failedLog.getTargetUrl(), failedLog.getEventType(), - objectMapper.readValue(failedLog.getPayload(), Object.class)); - + if (attempt > 0) { + long delayMs = retryTime * (1L << (attempt - 1)); + log.info("[Webhook] 재시도 {}/{} - url: {}, delay: {}ms", attempt, retryCount, targetUrl, delayMs); + Thread.sleep(delayMs); + sendLog.setRetryCount(attempt); + } + return restTemplate.postForEntity(targetUrl, request, String.class); + } catch (HttpClientErrorException e) { + throw e; // 4xx는 재시도 불필요 } catch (Exception e) { - log.error("[Webhook] 재시도 실패 - id: {}", failedLog.getId(), e); + lastException = e; + log.warn("[Webhook] 발송 실패 (attempt {}/{}) - url: {}, error: {}", attempt, retryCount, targetUrl, e.getMessage()); } } - }*/ + throw lastException; + } /** * HMAC-SHA256 서명 생성 diff --git a/src/main/java/com/eactive/eai/rms/onl/common/service/ums/UmsDispatchService.java b/src/main/java/com/eactive/eai/rms/onl/common/service/ums/UmsDispatchService.java index 8d3e521..d257a68 100644 --- a/src/main/java/com/eactive/eai/rms/onl/common/service/ums/UmsDispatchService.java +++ b/src/main/java/com/eactive/eai/rms/onl/common/service/ums/UmsDispatchService.java @@ -1,19 +1,5 @@ package com.eactive.eai.rms.onl.common.service.ums; -import com.eactive.apim.portal.template.entity.MessageRequest; -import com.eactive.apim.portal.template.repository.MessageRequestRepository; -import com.eactive.ext.kjb.ums.KjbEmailSendModule; -import com.eactive.ext.kjb.ums.KjbUmsService; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.stereotype.Service; -import org.springframework.transaction.PlatformTransactionManager; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.transaction.support.TransactionTemplate; - -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; import java.time.LocalDateTime; import java.util.List; import java.util.concurrent.LinkedBlockingQueue; @@ -21,6 +7,23 @@ import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import javax.annotation.PostConstruct; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Service; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionTemplate; + +import com.eactive.apim.portal.template.entity.MessageRequest; +import com.eactive.apim.portal.template.repository.MessageRequestRepository; +import com.eactive.ext.djb.ums.DjbMessengerService; +import com.eactive.ext.kjb.ums.KjbEmailSendModule; +import com.eactive.ext.kjb.ums.KjbUmsService; + +import lombok.extern.slf4j.Slf4j; @Slf4j @@ -67,6 +70,10 @@ public class UmsDispatchService { // KjbEmailSendModule.getInstance().setRepository(messageRequestRepository); log.info("UmsDispatchService initialized - KjbUmsService and KjbEmailSendModule ready via singleton"); } + + private DjbMessengerService getMessengerService() { + return DjbMessengerService.getInstance(); + } private KjbUmsService getUmsService() { return KjbUmsService.getInstance(); @@ -131,54 +138,37 @@ public class UmsDispatchService { try { transactionTemplate.execute(status -> { try { - // 광주은행에서는 사용하지 않는 메소드 -// processMessage(message); - + if (DjbMessengerService.isAllowChannel(message)) { + getMessengerService().send(message); + } if (KjbUmsService.isAllowChannel(message)) { getUmsService().send(message); } - if (KjbEmailSendModule.isAllowChannel(message)) { getEmailModule().sendEmail(message); } - + updateMessageStatus(message, "SENT"); return null; } catch (Exception e) { - log.error("Failed to process message: {} - Error: {}", - message.getId(), e.getMessage(), e); - updateMessageStatus(message, "FAILED"); - status.setRollbackOnly(); - return null; + // checked exception을 RuntimeException으로 감싸 전파 → TransactionTemplate이 rollback 후 re-throw + throw new RuntimeException(e); } }); } catch (Exception e) { - log.error("Transaction execution failed for message: {} - Error: {}", - message.getId(), e.getMessage(), e); + log.error("Failed to process message: {} - Error: {}", message.getId(), e.getMessage(), e); + try { + transactionTemplate.execute(s -> { + updateMessageStatus(message, "FAILED"); + return null; + }); + } catch (Exception ex) { + log.error("Failed to update FAIL status for message: {}", message.getId(), ex); + } } }); } - /** - * kbank 방식으로 광주은행에서는 사용하지 않음 - * @param message - * @throws IOException - */ -// @Deprecated -// public void processMessage(MessageRequest message) throws IOException { -// try { -// String standardMessage = messageFactory.createMessage(message); -// log.debug("Processing message ID: {} - Type: {}", message.getId(), message.getMessageType()); -// -// umsSender.sendMessage(standardMessage); -// updateMessageStatus(message, "SENT"); -// log.debug("Successfully processed and sent message ID: {}", message.getId()); -// } catch (Exception e) { -// log.error("Message processing failed for ID: {} - Error: {}", -// message.getId(), e.getMessage(), e); -// updateMessageStatus(message, "FAILED"); -// throw e; -// } -// } + public void updateMessageStatus(MessageRequest message, String status) { message.setRequestStatus(status);