Skip to content

feat : 웹 푸시 기능 구현 - #40

Merged
oroi2009 merged 4 commits into
mainfrom
feat/#39
May 11, 2026
Merged

oroi2009 merged 4 commits into
mainfrom
feat/#39

Conversation

@oroi2009

Copy link
Copy Markdown
Contributor

🔍 관련 이슈


✅ 작업 분류

  • 버그 수정
  • 신규 기능
  • 프로젝트 구조 변경
  • 코드 리팩토링
  • 기능 수정

✨ 작업 내용

  1. Web Push 구독 정보 저장을 위한 PushSubscription 엔티티를 추가
  2. 알림 이력 저장을 위한 Notification 엔티티를 추가
  3. VAPID public key 조회 API를 추가했습니다.
    • GET /notifications/web-push/public-key
  4. Web Push 구독 등록/해제 API를 추가했습니다.
    • POST /notifications/push-subscriptions
    • DELETE /notifications/push-subscriptions
  5. 약속 장소 확정 시 PromiseConfirmedEvent를 발행하도록 변경
  6. 트랜잭션 커밋 이후 약속 멤버들에게 장소 확정 Web Push 알림을 발송하도록 구현
  7. Web Push 발송 성공/실패 여부를 확인할 수 있도록 로그를 추가

👥 전달사항

  1. Web Push를 받으려면 프론트에서 먼저 알림 권한을 받은 뒤 구독 등록 API를 호출해야 합니다.
  2. 구독 등록 전 프론트는 GET /notifications/web-push/public-key로 VAPID public key를 조회해야 합니다.
  3. VAPID private key는 서버에서만 사용하며 프론트로 전달하지 않습니다.
  4. 장소 확정 API의 요청 형식은 변경되지 않았습니다.
  5. 장소 확정 성공 시 서버 내부에서 약속 멤버들의 구독 정보를 조회해 알림을 발송합니다.
  6. 로컬 환경의 VAPID 키는 application-secret.properties에 추가되어 있습니다.

✅ 체크리스트

  • 코드가 컴파일 및 빌드됨
  • 모든 테스트가 통과함
  • 관련 문서가 업데이트됨
  • 커밋 메시지를 확인함

📸 스크린샷


💡 배운 것 / 시도한 것 / 고민한 점

  • Web Push는 서버가 브라우저에 직접 알림을 보내는 구조가 아니라, 브라우저 벤더의 Push Service를 거쳐 Service Worker가 알림을 표시하는 구조임을 확인
  • VAPID key는 발신 서버를 증명하기 위한 키이고, p256dh/auth는 실제 payload 암호화에 사용되는 브라우저 구독 키라는 점을 구분해서 설계
  • 장소 확정 트랜잭션이 롤백됐는데 알림이 먼저 발송되는 문제를 막기 위해 @TransactionalEventListener(phase = AFTER_COMMIT) 방식으로 처리

@oroi2009 oroi2009 self-assigned this May 11, 2026
@oroi2009 oroi2009 added the enhancement New feature or request label May 11, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements a Web Push notification system, including subscription management, event-driven triggers for promise confirmations, and integration with the web-push library. The review feedback identifies several areas for improvement: reusing the PushService instance to prevent resource leaks, utilizing a Spring-managed ObjectMapper for consistent serialization, and optimizing database performance by batching notification saves. Furthermore, the reviewer pointed out a structural mismatch in the fallback JSON payload and recommended moving the test HTML file out of the public static resources to avoid security risks in production environments.

Comment on lines +28 to +32
PushService pushService = new PushService(
properties.getVapidPublicKey(),
properties.getVapidPrivateKey(),
properties.getVapidSubject()
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

PushService를 매번 생성하면 내부적으로 새로운 CloseableHttpClient가 생성되지만 명시적으로 닫히지 않아 리소스 누수(File Descriptor 고갈)가 발생할 수 있습니다. PushService를 빈으로 등록하거나 멤버 변수로 유지하여 재사용하는 것을 권장합니다.

private final NotificationRepository notificationRepository;
private final PushSubscriptionRepository pushSubscriptionRepository;
private final WebPushSender webPushSender;
private final ObjectMapper objectMapper = new ObjectMapper();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

ObjectMapper를 직접 생성하는 대신 Spring에서 제공하는 빈을 주입받아 사용하는 것이 좋습니다. 이를 통해 프로젝트의 공통 직렬화 설정을 일관되게 유지할 수 있습니다.

Suggested change
private final ObjectMapper objectMapper = new ObjectMapper();
private final ObjectMapper objectMapper;

notification.markSent(now);
}

notificationRepository.save(notification);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

루프 내에서 notificationRepository.save()를 호출하면 멤버 수만큼 쿼리가 발생합니다. 알림 객체들을 리스트에 모은 뒤 루프 외부에서 saveAll()을 호출하여 성능을 최적화할 수 있습니다.

));
} catch (JsonProcessingException e) {
log.warn("Failed to serialize promise confirmed push payload. promiseId={}", event.promiseId(), e);
return "{\"title\":\"%s\",\"body\":\"%s\",\"url\":\"%s\"}".formatted(title, body, linkUrl);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

직렬화 실패 시 반환되는 폴백 JSON 문자열이 WebPushPayload 레코드의 구조(type, targetId 누락)와 일치하지 않습니다. 클라이언트에서 해당 필드들을 필수값으로 처리하고 있다면 런타임 에러가 발생할 수 있으므로 필드를 추가하거나 일관성을 맞추는 것이 좋습니다.

@@ -0,0 +1,317 @@
<!doctype html>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

테스트용 HTML 파일이 src/main/resources/static에 포함되어 있어 운영 환경에서도 외부 접근이 가능합니다. 보안 및 리소스 관리를 위해 이 파일을 src/test/resources로 옮기거나 빌드 시 제외되도록 설정하는 것을 권장합니다.

@oroi2009
oroi2009 merged commit 67c923f into main May 11, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat : 웹 푸시 기능 구현

1 participant