웹훅 발송

This commit is contained in:
eastargh
2026-08-27 16:54:01 +09:00
parent 6e4abf9f27
commit 28ab6863da
2 changed files with 25 additions and 6 deletions
@@ -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 {
/**
* 웹훅 발송 메인 메서드
*
* <p>{@code @Transactional}을 두지 않는다 - 재시도(최대 retryCount회, Thread.sleep 포함)가
* 이 메서드 안에서 동기로 도는데, 메서드 전체를 하나의 트랜잭션으로 감싸면 재시도 도중의
* 실패 로그 저장(sendWithRetry 내부)이 커밋되지 않고 메서드가 끝날 때까지 대기하게 되어
* "실패 시 즉시 로그부터 남긴다"는 의도가 무의미해진다. save() 호출마다 개별 커밋되도록 둔다.</p>
*/
@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<String> sendWithRetry(String proxyUrl, HttpEntity<String> 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 미지원이므로 직접 변환