nl.martijndwars.webpush의 410 Gone 응답 처리
문제: WebPush 구독이 만료되었을 때 자동으로 삭제하려 했으나 만료된 구독이 DB에 계속 남아있는 문제 발생
기대 동작:
- Push 전송 시 410 Gone 응답을 받으면 해당 구독 자동 삭제
- 만료된 구독에 불필요한 요청 발생 방지
실제 동작:
- 410 Gone 응답을 받아도 구독이 삭제되지 않음
- 만료된 구독에 계속 Push 전송 시도
원인 — 실제 동작 확인
nl.martijndwars.webpush 라이브러리의 실제 동작을 테스트로 확인했다.
@Test
public void test410Response() {
try {
nl.martijndwars.webpush.Notification notification =
new nl.martijndwars.webpush.Notification(
"https://fcm.googleapis.com/fcm/send/INVALID_TOKEN",
"BDxcO0vK8Hjrl...",
"OXde8QZMgpvnN+05Dljhgw==",
"test message".getBytes()
);
HttpResponse response = pushService.send(notification);
int statusCode = response.getStatusLine().getStatusCode();
System.out.println("Status Code: " + statusCode); // 410
System.out.println("예외 발생하지 않음");
} catch (IOException e) {
System.out.println("IOException 발생: " + e.getMessage());
}
}
테스트 결과
========================================
=== 전송 완료 (예외 안 던짐) ===
Status Code: 410
Reason: Gone
========================================
결론: 410 Gone 응답이 왔지만 예외를 던지지 않음
HttpResponse 객체로 반환됨
원인 분석: nl.martijndwars.webpush는 4xx/5xx HTTP 응답을 예외로 던지지 않는다.
- Apache HttpClient 기반으로 동작
- OkHttp와 유사한 방식: 모든 HTTP 응답을 정상적인
HttpResponse로 반환 - Spring의
RestTemplate/WebClient와 다른 동작 방식
해결 — HttpResponse의 statusCode 직접 확인
private void sendPushNotificationInternal(WebPushSubscription subscription,
String title, String body)
throws GeneralSecurityException, IOException, JoseException,
ExecutionException, InterruptedException {
nl.martijndwars.webpush.Notification notification =
createNotification(subscription, title, body);
// HttpResponse 받기
org.apache.http.HttpResponse response = pushService.send(notification);
int statusCode = response.getStatusLine().getStatusCode();
// statusCode를 직접 확인
if (statusCode == 410) {
// 410 Gone - 구독 만료
deleteExpiredSubscription(subscription.getId());
log.warn("410 Gone - 만료된 웹 구독 삭제: endpoint={}",
subscription.getEndpoint());
return;
}
if (statusCode == 404) {
// 404 Not Found - 구독 없음
deleteExpiredSubscription(subscription.getId());
log.warn("404 Not Found - 존재하지 않는 웹 구독 삭제: endpoint={}",
subscription.getEndpoint());
return;
}
if (statusCode >= 200 && statusCode < 300) {
// 성공 (2xx)
log.debug("웹 Push 전송 성공 - statusCode: {}, endpoint: {}",
statusCode, subscription.getEndpoint());
return;
}
// 기타 에러
log.error("웹 Push 전송 실패 - statusCode: {}, endpoint: {}, reason: {}",
statusCode, subscription.getEndpoint(),
response.getStatusLine().getReasonPhrase());
}
HTTP 클라이언트별 에러 처리 비교
| 라이브러리 | 4xx/5xx 처리 방식 | 410 확인 방법 |
|---|---|---|
| Spring RestTemplate | 예외 던짐 | catch (HttpClientErrorException e) → e.getStatusCode() == 410 |
| Spring WebClient | 예외 던짐 | catch (WebClientResponseException e) → e.getStatusCode() == 410 |
| Firebase Admin SDK | 예외 던짐 | catch (FirebaseMessagingException) → errorCode == UNREGISTERED |
| OkHttp | 예외 안 던짐 | response.code() == 410 직접 확인 |
| nl.martijndwars.webpush | 예외 안 던짐 | response.getStatusLine().getStatusCode() == 410 직접 확인 |
// IOException catch는 진짜 네트워크 에러용
catch (IOException e) {
log.error("네트워크 에러", e); // 연결 실패, 타임아웃 등
}
// HTTP 에러는 statusCode로 처리
if (statusCode == 410) {
deleteExpiredSubscription(); // 비즈니스 로직
}
결과
- 만료된 구독에 410 응답 수신 시 DB에서 즉시 자동 삭제
- 만료 구독으로의 불필요한 재전송 차단
- 네트워크 에러(
IOException)와 HTTP 에러(statusCode)를 분리해 처리
참고 — 410 Gone
HTTP 410 Gone은 요청한 리소스가 서버에서 영구적으로 삭제됐음을 나타내는 클라이언트 에러 응답이다. 클라이언트는 410 응답을 받은 리소스에 대해 요청을 반복하지 않아야 하며, 해당 링크나 엔드포인트는 제거하거나 교체해야 한다. 일시적인지 영구적인지 불명확할 경우에는 404를 사용하는 것이 적절하다.
'[트러블슈팅]' 카테고리의 다른 글
| [오늘의 옷장] FCM 스레드 풀 고갈 2차해결 — 비정상 토큰 유입 시 전체 서비스 응답 불가 (0) | 2026.05.22 |
|---|---|
| [오늘의 옷장] FCM 무한대기 문제 1차해결 (0) | 2026.05.22 |
| [Career Coach]AI Agent로 뉴스 검색용 키워드 생성한 효과 (0) | 2026.05.22 |
| [shoppay]결제 확정 후 DB 미저장 문제 — 네트워크 단절 시 결제 유실 (0) | 2026.05.22 |
| [CareerCoach프로젝트] 포트폴리오가이드 평가 시스템 최적화 (portfolio_standard 사용이유) (0) | 2026.05.22 |