반응형
Web Push 알림 시스템 구현 (DB 설계 변경 이후 실제 코드 중심)

앞선 글에서는 Web Push 시스템의 DB 설계를 사용자 중심 → 구독(endpoint) 중심으로 변경한 배경을 정리했다.
이번 글에서는 그 설계가 실제 코드에서 어떻게 구현되었는지, Web Push 기술 특성과 어떤 식으로 맞물리는지를 코드 중심으로 정리해보았다.
전체 흐름 요약 (기술 관점)
관리자 --(발송 요청)-> Controller --> SendService
--> PushSender --> FCM/PushServer --> Browser Service Worker
큰 변경점은 사용자가 아니라 구독 단위로 발송이 이뤄진다는 점이다.
1. 구독 정보 도메인 (BDomain)
/*
* 편의성을 위하여 Lombok 사용
* Web Push 발송에 필요한 최소 정보만 보관
*/
public class BDomain {
private Long subsNumb;
// 브라우저가 발급한 Push Endpoint
private String endpoint;
// Web Push 암호화 키
private String p256Keyy;
private String authKeyy;
// 사용 여부 플래그
private String isActive;
}
기술적 포인트
- endpoint, p256dh, auth는 Web Push 표준 스펙 필수 요소
- 로그인 정보 없이도 푸시 발송 가능
2. 발송 메시지 도메인 (CDomain)
/*
* 편의성을 위하여 Lombok 사용
* 관리자가 작성한 발송 메시지 정보
*/
public class CDomain {
private Long mesgNumb;
private String titlText; //제목
private String bodyText; //본문
// 발송 여부, 예약 발송 확장 고려
private String sendStat;
}
3. 발송 서비스 (SendService)
발송 로직의 책임은 Service 레이어에서 조합만 담당한다.
@Service
@RequiredArgsConstructor
@Slf4j
public class SendServiceImpl implements SendService {
@Override
public void sendPush(Long mesgNumb) {
MesgDomain mesg = mesgDao.selectMesg(mesgNumb);
List<CDomain> cList = cDao.selectActiveSubs();
for (CDomain c : cList) {
log.info("PUSH SEND Endpoint TO: {}", c.getEndpoint());
log.info("PUSH SEND SubsNumb TO: {}", c.getSubsNumb());
pushSender.send(c, mesg);
}
}
}
기술적 포인트
- Service는 제어 흐름만 담당
- 실제 Web Push 프로토콜 처리 로직은 PushSender로 완전 분리
- 향후 조건별 발송/사용자 기준 발송으로 확장 용이
4. Web Push 핵심 구현부 (PushSender)
실제 Web Push 프로토콜을 다루는 핵심 클래스
@Component
@RequiredArgsConstructor
@Slf4j
public class PushSender {
@Value("${vapid.public-key}")
private String publicKey;
@Value("${vapid.private-key}")
private String privateKey;
@Value("${vapid.subject}")
private String subject;
public void send(CDomain c, MesgDomain mesg) {
// 필수 키 검증
if (c.getP256Keyy() == null || c.getAuthKeyy() == null) {
log.error("PUSH KEY NULL - cNumb={}", c.getCNumb());
return;
}
try {
Security.addProvider(new BouncyCastleProvider());
PushService pushService =
new PushService(publicKey, privateKey, subject);
Notification notification = new Notification(
c.getEndpoint(),
c.getP256Keyy(),
c.getAuthKeyy(),
createPayload(mesg)
);
pushService.send(notification);
} catch (Exception e) {
log.error("PUSH SEND FAIL - cNumb={}", c.getCNumb(), e);
}
}
private String createPayload(MesgDomain mesg) {
return """
{
"title": "%s",
"body": "%s"
}
""".formatted(
mesg.getTitlText(),
mesg.getBodyText()
);
}
}
기술적 포인트
- VAPID 기반 인증 사용
- BouncyCastle Provider 필수
- Web Push는 동기 방식으로도 충분히 안정적이라고 판단
- 비동기/큐 구조는 추후 고려 - 초기 단계에서는 구조를 억지로 넣지 않는 것이 디버깅과 장애 분석 측면에서 효율적이라 판단
5. Service Worker (수신 측)
브라우저는 Service Workder를 통해 푸시를 수신한다.
self.addEventListener("push", function (event) {
console.log("PUSH EVENT RECEIVED");
let data = {};
try {
if (event.data) {
data = event.data.json();
}
} catch (e) {
console.error("PUSH PAYLOAD PARSE ERROR", e);
}
const title = data.title || "알림";
const options = {
body: data.body || "새로운 알림이 도착했습니다.",
tag: "notify-sideproject",
renotify: true,
data: {
url: "/push"
}
};
event.waitUntil(
self.registration.showNotification(title, options)
);
});
기술적 포인트
- 서버는 JSON Payload만 전달
구현 후 느낀 점
- Web Push는 사용자 계정 중심 사고를 버리는 순간 설계가 단순해진다.
- endpoint 중심 설계는 테스트가 쉽고 장애 분석이 빠르다는 장점이 있었다.
다만, 5개의 환경에서 5건의 동일한 알림을 보냈을때, 유실되는 경우가 30% 확률로 발생했다.
매우 높은 실패 확률이라고 생각한다.
현재는 발송 가능여부 테스트가 우선이라 상세 내역은 분석하지 못했다.
추후 이러한 예외 사항들을 잡아가며 진행해야겠다.

반응형
'SideProject' 카테고리의 다른 글
| Docker 기반 웹 푸시 알림 시스템 - 3.5 (0) | 2026.01.29 |
|---|---|
| Docker 기반 웹 푸시 알림 시스템 - 3 (0) | 2026.01.27 |
| Docker 기반 웹 푸시 알림 시스템 - 2 (0) | 2026.01.19 |
| Docker 기반 웹 푸시 알림 시스템 - 1 (0) | 2026.01.14 |
| 단축URL 구현하기 (네이버 me2.do 서비스 종료) (2) | 2025.01.20 |