웹훅 발송시 리버스프록시 제거, 직접 발송
eapim-admin CI / build (push) Waiting to run

This commit is contained in:
eastargh
2026-09-23 10:33:08 +09:00
parent e75a4e4c22
commit a45e749331
12 changed files with 64 additions and 99 deletions
@@ -730,16 +730,6 @@
</div> </div>
</td> </td>
</tr> </tr>
<tr>
<th><%= localeMessage.getString("portalOrg.reverseProxyPath") %></th>
<td colspan="3">
<%=reverseProxyUrl %>/<input type="text" name="reverseProxyPath" id="reverseProxyPath" style="width:100px; "/>
</td>
<th></th>
<td>
</td>
</tr>
<tr><%--사업자등록증--%> <tr><%--사업자등록증--%>
<th><%= localeMessage.getString("portalOrg.compRegFile") %></th> <th><%= localeMessage.getString("portalOrg.compRegFile") %></th>
<td colspan="5" style="padding-top: 5px; padding-bottom: 5px;"> <td colspan="5" style="padding-top: 5px; padding-bottom: 5px;">
@@ -89,14 +89,13 @@
mtype: 'POST', mtype: 'POST',
url: url, url: url,
postData: getSearchForJqgrid("cmd", "LIST"), postData: getSearchForJqgrid("cmd", "LIST"),
colNames: ['id', 'No.', '기관명', '이벤트유형', 'Target URL', 'Proxy URL', 'Status', '성공여부', '재시도', '발송서버', '발송일시'], colNames: ['id', 'No.', '기관명', '이벤트유형', 'Target URL', 'Status', '성공여부', '재시도', '발송서버', '발송일시'],
colModel: [ colModel: [
{name: 'id', align: 'center', key: true, hidden: true}, {name: 'id', align: 'center', key: true, hidden: true},
{name: 'rowNum', align: 'center', width: 45, sortable: false}, {name: 'rowNum', align: 'center', width: 45, sortable: false},
{name: 'orgName', align: 'left', width: 140}, {name: 'orgName', align: 'left', width: 140},
{name: 'eventType', align: 'center', width: 120, formatter: formatEventType}, {name: 'eventType', align: 'center', width: 120, formatter: formatEventType},
{name: 'targetUrl', align: 'left', width: 300, formatter: formatTargetUrl}, {name: 'targetUrl', align: 'left', width: 300, formatter: formatTargetUrl},
{name: 'proxyUrl', align: 'left', width: 300, formatter: formatTargetUrl},
{name: 'statusCode', align: 'center', width: 60}, {name: 'statusCode', align: 'center', width: 60},
{name: 'success', align: 'center', width: 70, formatter: formatSuccess}, {name: 'success', align: 'center', width: 70, formatter: formatSuccess},
{name: 'retryCount', align: 'center', width: 60}, {name: 'retryCount', align: 'center', width: 60},
@@ -37,7 +37,6 @@
$('#eventType').text(data.eventTypeName || data.eventType || '') $('#eventType').text(data.eventTypeName || data.eventType || '')
.attr('title', data.eventType || ''); .attr('title', data.eventType || '');
$('#targetUrl').text(data.targetUrl || ''); $('#targetUrl').text(data.targetUrl || '');
$('#proxyUrl').text(data.proxyUrl || '');
$('#statusCode').text(data.statusCode != null ? data.statusCode : ''); $('#statusCode').text(data.statusCode != null ? data.statusCode : '');
$('#retryCount').text(data.retryCount != null ? data.retryCount : ''); $('#retryCount').text(data.retryCount != null ? data.retryCount : '');
$('#sendBy').text(data.sendBy || ''); $('#sendBy').text(data.sendBy || '');
@@ -99,9 +98,7 @@
</tr> </tr>
<tr> <tr>
<th>Target URL</th> <th>Target URL</th>
<td><span id="targetUrl"></span></td> <td colspan="3"><span id="targetUrl"></span></td>
<th>Proxy URL</th>
<td><span id="proxyUrl"></span></td>
</tr> </tr>
<tr> <tr>
<th>성공여부</th> <th>성공여부</th>
+20 -4
View File
@@ -272,7 +272,6 @@ DB 테이블 `PTL_PROPERTY` 에 그룹명 `'Portal'` 로 저장된다.
- `rms.webhook.enabled` - 웹훅 발송 여부 - `rms.webhook.enabled` - 웹훅 발송 여부
- `rms.webhook.retry_count` - 웹훅 재발송 횟수 - `rms.webhook.retry_count` - 웹훅 재발송 횟수
- `rms.webhook.retry_time` - 웹훅 재발송 시간간격 - `rms.webhook.retry_time` - 웹훅 재발송 시간간격
- `rms.webhook.reverse_proxy.url` - 리버스 프록시 경로
> Portal 프라퍼티 (파트너포탈 > 포탈관리 > Property관리) > Portal 프라퍼티 (파트너포탈 > 포탈관리 > Property관리)
@@ -296,6 +295,12 @@ DB 테이블 `PTL_PROPERTY` 에 그룹명 `'Portal'` 로 저장된다.
- `api.status.delay.range_minute` - API 지연 상태 판단을 위한 로그 구간(분) - `api.status.delay.range_minute` - API 지연 상태 판단을 위한 로그 구간(분)
- `api.status.delay.avg_resp_time` - API 지연 상태 판단을 위한 평균 응답속도 (ex: 10분동안 평균 응답속도가 10초이상이면 지연으로 판단) - `api.status.delay.avg_resp_time` - API 지연 상태 판단을 위한 평균 응답속도 (ex: 10분동안 평균 응답속도가 10초이상이면 지연으로 판단)
**API 점검 사전 공지 웹훅 발송**
- `ApiIncidentPrenoticeJob` - API 점검이 시작되기 전에 사전 공지를 웹훅으로 발송한다
- 실행간격(10분) CLUSTERED로 구성한다
- 공지사항에 점검 게시물을 등록할때 사전발송일에 일시가 등록된 경우 발송된다.
- 웹훅 신청시 `점검시작(CHECK_NOTICE)` 이벤트유형을 선택한 제휴사에게 웹훅을 발송한다
**유량제어토큰실패감시** **유량제어토큰실패감시**
- `InflowTokenMonitorJob` - 유량제어 토큰 획득 실패(TSEAIFR11) 발생시 직원에게 메신저를 발송한다 - `InflowTokenMonitorJob` - 유량제어 토큰 획득 실패(TSEAIFR11) 발생시 직원에게 메신저를 발송한다
- `api.inflow.fail.range_minute` - 최근 몇분동안 감시하는지 설정 - `api.inflow.fail.range_minute` - 최근 몇분동안 감시하는지 설정
@@ -320,7 +325,7 @@ DB 테이블 `PTL_PROPERTY` 에 그룹명 `'Portal'` 로 저장된다.
**토큰발급이력삭제** **토큰발급이력삭제**
- `TOKENISSUANCELOGCLEANUPJOB` - 7일이 지난 토큰 발급내역을 삭제한다 - `TOKENISSUANCELOGCLEANUPJOB` - 7일이 지난 토큰 발급내역을 삭제한다
## IIM 연동 - 제주은행 커스텀 ## 레이아웃 연동 - IIM(제주은행 커스텀 버전)
**인터페이스** **인터페이스**
- 최초 연동시 APIM에 등록되지 않은 경우에만 인터페이스를 생성한다. - 최초 연동시 APIM에 등록되지 않은 경우에만 인터페이스를 생성한다.
@@ -336,9 +341,20 @@ DB 테이블 `PTL_PROPERTY` 에 그룹명 `'Portal'` 로 저장된다.
- 전문이 등록된 후 API 관리화면에서 필요한 정보를 입력하고 **수정버튼을 클릭하여 G/W에 반영된다** - 전문이 등록된 후 API 관리화면에서 필요한 정보를 입력하고 **수정버튼을 클릭하여 G/W에 반영된다**
**연동이력** **연동이력**
- 연동 이력은 `환경정보 > EMS관리 > 연동 배포 이력` 메뉴에서 조회한다. - 연동 이력은 `환경정보 > EMS관리 > 연동 배포 이력` 메뉴에서 조회한다.(서비스명 : MessageSync)
## 배포 ## 인터페이스 운영 배포
**eCams**
- 제주은행 형상관리 시스템으로 개발서버의 인터페이스를 운영서버로 배포한다
- eCams에서 실행시 아래 폴더에 json을 생성한다. (스펙정보 포함)
- `iomap.download.path` - 인터페이스.json 파일 다운로드 개발서버 경로
- `iomap.upload.path` - 인터페이스.json 파일 업로드 운영서버 경로
**배포이력**
- 배포 이력은 `환경정보 > EMS관리 > 연동 배포 이력` 메뉴에서 조회한다.(서비스명 : interfaceDeploy)
## 소스배포
| 대상 | 비고 | | 대상 | 비고 |
|---|---| |---|---|
@@ -260,7 +260,6 @@ public interface MonitoringContext {
// 웹훅 // 웹훅
public static final String RMS_WEBHOOK_ENABLED = "rms.webhook.enabled"; public static final String RMS_WEBHOOK_ENABLED = "rms.webhook.enabled";
public static final String RMS_WEBHOOK_REVERSE_PROXY_URL = "rms.webhook.reverse_proxy.url";
public static final String RMS_WEBHOOK_RETRY_COUNT = "rms.webhook.retry_count"; public static final String RMS_WEBHOOK_RETRY_COUNT = "rms.webhook.retry_count";
public static final String RMS_WEBHOOK_RETRY_TIME = "rms.webhook.retry_time"; public static final String RMS_WEBHOOK_RETRY_TIME = "rms.webhook.retry_time";
@@ -29,9 +29,6 @@ public class WebhookSendLog {
@Column(name = "TARGET_URL", length = 500) @Column(name = "TARGET_URL", length = 500)
private String targetUrl; private String targetUrl;
@Column(name = "PROXY_URL", length = 500)
private String proxyUrl;
@Column(name = "EVENT_TYPE", length = 100) @Column(name = "EVENT_TYPE", length = 100)
private String eventType; private String eventType;
@@ -147,7 +147,8 @@ public class ApiIncidentPrenoticeService {
List<String> failures = new ArrayList<>(); List<String> failures = new ArrayList<>();
for (WebhookSendRequest req : sendList) { for (WebhookSendRequest req : sendList) {
req.setEventType(NOTICE_EVENT); req.setEventType(NOTICE_EVENT);
req.setData(buildNoticeData(incident, req.getData())); req.setExtra(buildNoticeExtra(incident));
// data(apiIds 배열)는 findSendList 가 이미 채워둔 값을 그대로 사용한다 - 장애/지연 웹훅과 동일한 형식
WebhookSendLog result = webhookService.send(req); WebhookSendLog result = webhookService.send(req);
if (result != null && Boolean.TRUE.equals(result.getSuccess())) { if (result != null && Boolean.TRUE.equals(result.getSuccess())) {
@@ -168,15 +169,12 @@ public class ApiIncidentPrenoticeService {
incidentId, targetCnt, sentCnt, result); incidentId, targetCnt, sentCnt, result);
} }
/** 원래 org 기준 apiId 목록(req.getData())은 유지하고, 점검 예정 일정을 덧붙인다. */ /** 점검 예정 시작/종료일시 - payload 최상위(data와 형제)에 실린다. */
private Map<String, Object> buildNoticeData(DjbApistatusIncident incident, Object apiIds) { private Map<String, Object> buildNoticeExtra(DjbApistatusIncident incident) {
Map<String, Object> data = new LinkedHashMap<>(); Map<String, Object> extra = new LinkedHashMap<>();
data.put("incidentId", incident.getIncidentId()); extra.put("scheduledStartAt", formatNoticeDate(incident.getStartedAt()));
data.put("title", incident.getTitle()); extra.put("scheduledEndAt", formatNoticeDate(incident.getEndAt()));
data.put("scheduledStartAt", formatNoticeDate(incident.getStartedAt())); return extra;
data.put("scheduledEndAt", formatNoticeDate(incident.getEndAt()));
data.put("apiIds", apiIds);
return data;
} }
private String formatNoticeDate(LocalDateTime dateTime) { private String formatNoticeDate(LocalDateTime dateTime) {
@@ -1,11 +1,14 @@
package com.eactive.eai.rms.ext.djb.webhook.dto; package com.eactive.eai.rms.ext.djb.webhook.dto;
import com.fasterxml.jackson.annotation.JsonAnyGetter;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
@Getter @Getter
@@ -21,13 +24,26 @@ public class WebhookPayload {
private long timestamp; private long timestamp;
private Object data; private Object data;
/** 이벤트별 추가 정보(예: 점검 예정 시작/종료일시) - JSON 최상위(data와 형제)로 펼쳐진다. */
private Map<String, Object> extra;
@JsonAnyGetter
public Map<String, Object> getExtra() {
return extra;
}
public static WebhookPayload of(String eventType, String message, Object data) { public static WebhookPayload of(String eventType, String message, Object data) {
return of(eventType, message, data, null);
}
public static WebhookPayload of(String eventType, String message, Object data, Map<String, Object> extra) {
return WebhookPayload.builder() return WebhookPayload.builder()
.eventType(eventType) .eventType(eventType)
.eventId(UUID.randomUUID().toString()) .eventId(UUID.randomUUID().toString())
.message(message) .message(message)
.timestamp(System.currentTimeMillis()) .timestamp(System.currentTimeMillis())
.data(data) .data(data)
.extra(extra)
.build(); .build();
} }
} }
@@ -20,7 +20,6 @@ public class WebhookSendLogUI {
/** 모니터링 공통코드(EVENT_TYPE) 코드명. 상세 조회에서만 채워진다(목록은 화면에서 콤보로 변환). */ /** 모니터링 공통코드(EVENT_TYPE) 코드명. 상세 조회에서만 채워진다(목록은 화면에서 콤보로 변환). */
private String eventTypeName; private String eventTypeName;
private String targetUrl; private String targetUrl;
private String proxyUrl;
private String payload; private String payload;
private String signature; private String signature;
@@ -1,5 +1,7 @@
package com.eactive.eai.rms.ext.djb.webhook.dto; package com.eactive.eai.rms.ext.djb.webhook.dto;
import java.util.Map;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
@@ -8,10 +10,10 @@ import lombok.Setter;
public class WebhookSendRequest { public class WebhookSendRequest {
private String orgId; private String orgId;
private String targetUrl; private String targetUrl;
private String proxyUrl;
private String eventType; private String eventType;
private String secret; private String secret;
private String reverseProxyPath;
private String userSecret; private String userSecret;
private Object data; private Object data;
/** 이벤트별 웹훅 payload 최상위에 실을 추가 정보 (예: CHECK_NOTICE의 점검 예정 시작/종료일시) */
private Map<String, Object> extra;
} }
@@ -154,7 +154,6 @@ public class WebhookSendLogManService extends BaseService {
ui.setEventType(e.getEventType()); ui.setEventType(e.getEventType());
ui.setEventTypeName(eventTypeName); ui.setEventTypeName(eventTypeName);
ui.setTargetUrl(e.getTargetUrl()); ui.setTargetUrl(e.getTargetUrl());
ui.setProxyUrl(e.getProxyUrl());
ui.setPayload(e.getPayload()); ui.setPayload(e.getPayload());
ui.setSignature(e.getSignature()); ui.setSignature(e.getSignature());
ui.setUserSecret(e.getUserSecret()); ui.setUserSecret(e.getUserSecret());
@@ -21,7 +21,6 @@ import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException; import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import com.eactive.apim.portal.portalorg.entity.QPortalOrg;
import com.eactive.eai.rms.common.context.MonitoringContext; import com.eactive.eai.rms.common.context.MonitoringContext;
import com.eactive.eai.rms.common.util.ContainerUtil; import com.eactive.eai.rms.common.util.ContainerUtil;
import com.eactive.eai.rms.data.entity.onl.djb.webhook.QWebhookReq; import com.eactive.eai.rms.data.entity.onl.djb.webhook.QWebhookReq;
@@ -61,16 +60,12 @@ public class WebhookService {
QWebhookReq req = QWebhookReq.webhookReq; QWebhookReq req = QWebhookReq.webhookReq;
QWebhookReqEvent evt = QWebhookReqEvent.webhookReqEvent; QWebhookReqEvent evt = QWebhookReqEvent.webhookReqEvent;
QWebhookReqApi api = QWebhookReqApi.webhookReqApi; QWebhookReqApi api = QWebhookReqApi.webhookReqApi;
QPortalOrg org = QPortalOrg.portalOrg;
String reverseProxyUrl = monitoringContext.getProperty(MonitoringContext.RMS_WEBHOOK_REVERSE_PROXY_URL);
List<Tuple> tuples = new JPAQueryFactory(entityManager) List<Tuple> tuples = new JPAQueryFactory(entityManager)
.select(req.orgId, req.targetUrl, req.secret, req.userSecret, org.reverseProxyPath, api.id.apiId) .select(req.orgId, req.targetUrl, req.secret, req.userSecret, api.id.apiId)
.from(req) .from(req)
.join(evt).on(req.id.eq(evt.id.webhookReqId)) .join(evt).on(req.id.eq(evt.id.webhookReqId))
.join(api).on(req.id.eq(api.id.webhookReqId)) .join(api).on(req.id.eq(api.id.webhookReqId))
.join(org).on(req.orgId.eq(org.id))
.where(evt.id.eventType.eq(event) .where(evt.id.eventType.eq(event)
.and(api.id.apiId.in(apiIds))) .and(api.id.apiId.in(apiIds)))
.fetch(); .fetch();
@@ -83,7 +78,6 @@ public class WebhookService {
WebhookSendRequest wr = new WebhookSendRequest(); WebhookSendRequest wr = new WebhookSendRequest();
wr.setOrgId(t.get(req.orgId)); wr.setOrgId(t.get(req.orgId));
wr.setTargetUrl(t.get(req.targetUrl)); wr.setTargetUrl(t.get(req.targetUrl));
wr.setProxyUrl(this.getProxyUrl(reverseProxyUrl, t.get(org.reverseProxyPath)));
wr.setSecret(t.get(req.secret)); wr.setSecret(t.get(req.secret));
wr.setUserSecret(t.get(req.userSecret)); wr.setUserSecret(t.get(req.userSecret));
wr.setEventType(event); wr.setEventType(event);
@@ -103,46 +97,6 @@ public class WebhookService {
return new ArrayList<>(resultMap.values()); return new ArrayList<>(resultMap.values());
} }
//reverse proxy 서버로 발송하기 위한 url 만든다
private String getProxyUrl(String proxyUrl, String path) {
String base = stripTrailingSlash(proxyUrl);
String normalizedPath = stripSlashes(path);
// // scheme://host[:port] 부분만 제거하고 이후 path/query/fragment는 그대로 유지
// String targetPath = targetUrl.replaceFirst("^[a-zA-Z][a-zA-Z0-9+.-]*://[^/]+", "");
StringBuilder url = new StringBuilder(base);
if (!normalizedPath.isEmpty()) {
url.append('/').append(normalizedPath);
}
// url.append(targetPath);
return url.toString();
}
private String stripTrailingSlash(String value) {
if (value == null) {
return "";
}
int end = value.length();
while (end > 0 && value.charAt(end - 1) == '/') {
end--;
}
return value.substring(0, end);
}
private String stripSlashes(String value) {
if (value == null) {
return "";
}
String result = value.trim();
while (result.startsWith("/")) {
result = result.substring(1);
}
while (result.endsWith("/")) {
result = result.substring(0, result.length() - 1);
}
return result;
}
/** /**
* 웹훅 발송 메인 메서드 * 웹훅 발송 메인 메서드
* *
@@ -156,24 +110,23 @@ public class WebhookService {
public WebhookSendLog send(WebhookSendRequest req) { public WebhookSendLog send(WebhookSendRequest req) {
String targetUrl = req.getTargetUrl(); String targetUrl = req.getTargetUrl();
String proxyUrl = req.getProxyUrl();
String secret = req.getSecret(); String secret = req.getSecret();
String userSecret = req.getUserSecret(); String userSecret = req.getUserSecret();
String eventType = req.getEventType(); String eventType = req.getEventType();
String message = getMessage(eventType); String message = getMessage(eventType);
Object data = req.getData(); Object data = req.getData();
Map<String, Object> extra = req.getExtra();
WebhookSendLog sendLog = new WebhookSendLog(); WebhookSendLog sendLog = new WebhookSendLog();
sendLog.setOrgId(req.getOrgId()); sendLog.setOrgId(req.getOrgId());
sendLog.setTargetUrl(targetUrl); sendLog.setTargetUrl(targetUrl);
sendLog.setProxyUrl(proxyUrl);
sendLog.setEventType(eventType); sendLog.setEventType(eventType);
sendLog.setUserSecret(userSecret); sendLog.setUserSecret(userSecret);
sendLog.setSendBy(ContainerUtil.getInstanceName()); sendLog.setSendBy(ContainerUtil.getInstanceName());
try { try {
// 1. Payload 생성 // 1. Payload 생성
WebhookPayload webhookPayload = WebhookPayload.of(eventType, message, data); WebhookPayload webhookPayload = WebhookPayload.of(eventType, message, data, extra);
String payloadJson = objectMapper.writeValueAsString(webhookPayload); String payloadJson = objectMapper.writeValueAsString(webhookPayload);
sendLog.setPayload(payloadJson); sendLog.setPayload(payloadJson);
@@ -194,7 +147,7 @@ public class WebhookService {
int retryTime = monitoringContext.getIntProperty(MonitoringContext.RMS_WEBHOOK_RETRY_TIME, DEFAULT_RETRY_TIME); int retryTime = monitoringContext.getIntProperty(MonitoringContext.RMS_WEBHOOK_RETRY_TIME, DEFAULT_RETRY_TIME);
sendLog.setSentAt(LocalDateTime.now()); sendLog.setSentAt(LocalDateTime.now());
HttpEntity<String> httpRequest = new HttpEntity<>(payloadJson, headers); HttpEntity<String> httpRequest = new HttpEntity<>(payloadJson, headers);
ResponseEntity<String> response = sendWithRetry(proxyUrl, httpRequest, sendLog, retryCount, retryTime); ResponseEntity<String> response = sendWithRetry(targetUrl, httpRequest, sendLog, retryCount, retryTime);
// 5. 성공 로그 // 5. 성공 로그
sendLog.setStatusCode(response.getStatusCode().value()); sendLog.setStatusCode(response.getStatusCode().value());
@@ -202,7 +155,7 @@ public class WebhookService {
sendLog.setSuccess(response.getStatusCode().is2xxSuccessful()); sendLog.setSuccess(response.getStatusCode().is2xxSuccessful());
log.info("[Webhook] 발송 성공 - eventType: {}, url: {}, status: {}, retryCount: {}", log.info("[Webhook] 발송 성공 - eventType: {}, url: {}, status: {}, retryCount: {}",
eventType, proxyUrl, response.getStatusCode(), sendLog.getRetryCount()); eventType, targetUrl, response.getStatusCode(), sendLog.getRetryCount());
} catch (HttpClientErrorException | HttpServerErrorException e) { } catch (HttpClientErrorException | HttpServerErrorException e) {
sendLog.setStatusCode(e.getStatusCode().value()); sendLog.setStatusCode(e.getStatusCode().value());
@@ -210,13 +163,13 @@ public class WebhookService {
sendLog.setSuccess(false); sendLog.setSuccess(false);
sendLog.setErrorMessage(e.getMessage()); sendLog.setErrorMessage(e.getMessage());
log.error("[Webhook] HTTP 오류 - eventType: {}, url: {}, status: {}", log.error("[Webhook] HTTP 오류 - eventType: {}, url: {}, status: {}",
eventType, proxyUrl, e.getStatusCode()); eventType, targetUrl, e.getStatusCode());
} catch (Exception e) { } catch (Exception e) {
sendLog.setSuccess(false); sendLog.setSuccess(false);
sendLog.setErrorMessage(e.getMessage()); sendLog.setErrorMessage(e.getMessage());
log.error("[Webhook] 발송 실패 - eventType: {}, url: {}, error: {}", log.error("[Webhook] 발송 실패 - eventType: {}, url: {}, error: {}",
eventType, proxyUrl, e.getMessage()); eventType, targetUrl, e.getMessage());
} }
return sendLogRepository.save(sendLog); return sendLogRepository.save(sendLog);
@@ -243,29 +196,29 @@ public class WebhookService {
* - 실패할 때마다 즉시 로그를 저장한다 (최초 실패는 INSERT, 이후 재시도 실패는 같은 row를 UPDATE) - * - 실패할 때마다 즉시 로그를 저장한다 (최초 실패는 INSERT, 이후 재시도 실패는 같은 row를 UPDATE) -
* 재시도 도중 앱이 죽어도 마지막으로 저장된 실패 기록이 남도록 하기 위함 * 재시도 도중 앱이 죽어도 마지막으로 저장된 실패 기록이 남도록 하기 위함
*/ */
private ResponseEntity<String> sendWithRetry(String proxyUrl, HttpEntity<String> request, WebhookSendLog sendLog, int retryCount, int retryTime) throws Exception { private ResponseEntity<String> sendWithRetry(String targetUrl, HttpEntity<String> request, WebhookSendLog sendLog, int retryCount, int retryTime) throws Exception {
Exception lastException = null; Exception lastException = null;
for (int attempt = 0; attempt <= retryCount; attempt++) { for (int attempt = 0; attempt <= retryCount; attempt++) {
try { try {
if (attempt == 0) { if (attempt == 0) {
// 배포 반영 확인용: HttpComponentsClientHttpRequestFactory 여야 SSL 우회 RestTemplate 사용 중. // 배포 반영 확인용: HttpComponentsClientHttpRequestFactory 여야 SSL 우회 RestTemplate 사용 중.
// SimpleClientHttpRequestFactory 로 찍히면 기본 new RestTemplate() 이 주입된 것 → PKIX 원인. // SimpleClientHttpRequestFactory 로 찍히면 기본 new RestTemplate() 이 주입된 것 → PKIX 원인.
log.info("[Webhook] POST 시도 - url: {}, requestFactory: {}", proxyUrl, log.info("[Webhook] POST 시도 - url: {}, requestFactory: {}", targetUrl,
restTemplate.getRequestFactory().getClass().getSimpleName()); restTemplate.getRequestFactory().getClass().getSimpleName());
} }
if (attempt > 0) { if (attempt > 0) {
long delayMs = (long) retryTime * attempt; long delayMs = (long) retryTime * attempt;
log.info("[Webhook] 재시도 {}/{} - url: {}, delay: {}ms", attempt, retryCount, proxyUrl, delayMs); log.info("[Webhook] 재시도 {}/{} - url: {}, delay: {}ms", attempt, retryCount, targetUrl, delayMs);
Thread.sleep(delayMs); Thread.sleep(delayMs);
} }
return restTemplate.postForEntity(proxyUrl, request, String.class); return restTemplate.postForEntity(targetUrl, request, String.class);
} catch (HttpClientErrorException e) { } catch (HttpClientErrorException e) {
//throw e; // 4xx는 재시도 불필요 //throw e; // 4xx는 재시도 불필요
lastException = e; //TODO: 재시도 테스트 lastException = e; //TODO: 재시도 테스트
recordAttemptFailure(sendLog, attempt, e.getStatusCode().value(), e.getResponseBodyAsString(), e.getMessage()); recordAttemptFailure(sendLog, attempt, e.getStatusCode().value(), e.getResponseBodyAsString(), e.getMessage());
} catch (Exception e) { } catch (Exception e) {
lastException = e; lastException = e;
log.warn("[Webhook] 발송 실패 (attempt {}/{}) - url: {}, error: {}", attempt, retryCount, proxyUrl, e.getMessage()); log.warn("[Webhook] 발송 실패 (attempt {}/{}) - url: {}, error: {}", attempt, retryCount, targetUrl, e.getMessage());
recordAttemptFailure(sendLog, attempt, null, null, e.getMessage()); recordAttemptFailure(sendLog, attempt, null, null, e.getMessage());
} }
} }