웹훅 발송
This commit is contained in:
@@ -89,7 +89,7 @@
|
|||||||
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', 'Proxy 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},
|
||||||
@@ -99,11 +99,12 @@
|
|||||||
{name: 'proxyUrl', 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: 'sendBy', align: 'center', width: 100},
|
{name: 'sendBy', align: 'center', width: 100},
|
||||||
{name: 'sentAt', align: 'center', width: 140}
|
{name: 'sentAt', align: 'center', width: 140}
|
||||||
],
|
],
|
||||||
jsonReader: {repeatitems: false},
|
jsonReader: {repeatitems: false},
|
||||||
pager: $('#pager'),
|
pager: $('#pager'),
|
||||||
page: '${param.page}',
|
page: '${param.page}',
|
||||||
rowNum: '${rmsDefaultRowNum}',
|
rowNum: '${rmsDefaultRowNum}',
|
||||||
autoheight: true,
|
autoheight: true,
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import org.springframework.http.HttpHeaders;
|
|||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
import org.springframework.web.client.HttpClientErrorException;
|
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;
|
||||||
@@ -146,8 +145,12 @@ public class WebhookService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 웹훅 발송 메인 메서드
|
* 웹훅 발송 메인 메서드
|
||||||
|
*
|
||||||
|
* <p>{@code @Transactional}을 두지 않는다 - 재시도(최대 retryCount회, Thread.sleep 포함)가
|
||||||
|
* 이 메서드 안에서 동기로 도는데, 메서드 전체를 하나의 트랜잭션으로 감싸면 재시도 도중의
|
||||||
|
* 실패 로그 저장(sendWithRetry 내부)이 커밋되지 않고 메서드가 끝날 때까지 대기하게 되어
|
||||||
|
* "실패 시 즉시 로그부터 남긴다"는 의도가 무의미해진다. save() 호출마다 개별 커밋되도록 둔다.</p>
|
||||||
*/
|
*/
|
||||||
@Transactional
|
|
||||||
public void send(WebhookSendRequest req) {
|
public void send(WebhookSendRequest req) {
|
||||||
|
|
||||||
String targetUrl = req.getTargetUrl();
|
String targetUrl = req.getTargetUrl();
|
||||||
@@ -235,6 +238,8 @@ public class WebhookService {
|
|||||||
* 재시도 포함 HTTP 발송
|
* 재시도 포함 HTTP 발송
|
||||||
* - 4xx: 클라이언트 오류이므로 즉시 throw (재시도 불필요)
|
* - 4xx: 클라이언트 오류이므로 즉시 throw (재시도 불필요)
|
||||||
* - 5xx / 네트워크 오류: 최대 RETRY_COUNT회까지 선형 증가 backoff 재시도 (1s → 2s → 3s → 4s → 5s)
|
* - 5xx / 네트워크 오류: 최대 RETRY_COUNT회까지 선형 증가 backoff 재시도 (1s → 2s → 3s → 4s → 5s)
|
||||||
|
* - 실패할 때마다 즉시 로그를 저장한다 (최초 실패는 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 proxyUrl, HttpEntity<String> request, WebhookSendLog sendLog, int retryCount, int retryTime) throws Exception {
|
||||||
Exception lastException = null;
|
Exception lastException = null;
|
||||||
@@ -243,21 +248,34 @@ public class WebhookService {
|
|||||||
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, proxyUrl, delayMs);
|
||||||
sendLog.setRetryCount(attempt);
|
|
||||||
Thread.sleep(delayMs);
|
Thread.sleep(delayMs);
|
||||||
}
|
}
|
||||||
return restTemplate.postForEntity(proxyUrl, request, String.class);
|
return restTemplate.postForEntity(proxyUrl, 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());
|
||||||
} 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, proxyUrl, e.getMessage());
|
||||||
|
recordAttemptFailure(sendLog, attempt, null, null, e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw lastException;
|
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 서명 생성
|
* HMAC-SHA256 서명 생성
|
||||||
* Java 17 미만은 HexFormat 미지원이므로 직접 변환
|
* Java 17 미만은 HexFormat 미지원이므로 직접 변환
|
||||||
|
|||||||
Reference in New Issue
Block a user